Added dynamic SVG map marker.
This commit is contained in:
parent
2f51d3843c
commit
595d9528d5
6 changed files with 121 additions and 22 deletions
|
@ -295,7 +295,7 @@ export default {
|
|||
})
|
||||
|
||||
if (point) {
|
||||
this.selectedPoint = point
|
||||
this.selectPoint(point)
|
||||
// @ts-ignore
|
||||
this.$refs.popup.setPosition(event.coordinate)
|
||||
// Center the map on the selected point
|
||||
|
@ -307,10 +307,16 @@ export default {
|
|||
})
|
||||
},
|
||||
|
||||
selectPoint(point: GPSPoint) {
|
||||
this.selectedPoint = point
|
||||
this.highlightPoint(this.pointsLayer as VectorLayer, point)
|
||||
},
|
||||
|
||||
clearSelectedPoint() {
|
||||
this.selectedPoint = null
|
||||
this.selectedPointIndex = null
|
||||
this.selectedFeature = null
|
||||
this.removePointHighlight(this.pointsLayer as VectorLayer)
|
||||
},
|
||||
|
||||
refreshShowMetricsFromURL() {
|
||||
|
|
|
@ -4,7 +4,7 @@ import Map from 'ol/Map';
|
|||
import Point from 'ol/geom/Point';
|
||||
import VectorLayer from 'ol/layer/Vector';
|
||||
import VectorSource from 'ol/source/Vector';
|
||||
import { Circle, Fill, Style, Stroke } from 'ol/style';
|
||||
import { Icon, Style } from 'ol/style';
|
||||
import { useGeographic } from 'ol/proj';
|
||||
|
||||
import GPSPoint from '../models/GPSPoint';
|
||||
|
@ -13,24 +13,29 @@ import Units from './Units.vue';
|
|||
|
||||
const minZoom = 2
|
||||
const maxZoom = 18
|
||||
const iconSize = 32
|
||||
|
||||
useGeographic()
|
||||
|
||||
const pointStyles = {
|
||||
default: new Style({
|
||||
image: new Circle({
|
||||
radius: 6,
|
||||
fill: new Fill({ color: 'aquamarine' }),
|
||||
stroke: new Stroke({ color: 'blue', width: 1 }),
|
||||
image: new Icon({
|
||||
anchor: [0.5, iconSize - 2],
|
||||
anchorXUnits: 'fraction',
|
||||
anchorYUnits: 'pixels',
|
||||
// TODO Apply favourite accent color
|
||||
src: `/icons/poi.svg?size=${iconSize}&color=3468db&border=1f2d3d&fill=cff0ff`,
|
||||
}),
|
||||
zIndex: Infinity, // Ensure that points are always displayed above other layers
|
||||
zIndex: 100,
|
||||
}),
|
||||
|
||||
highlighted: new Style({
|
||||
image: new Circle({
|
||||
radius: 10,
|
||||
fill: new Fill({ color: 'rgba(255, 0, 0, 0.5)' }),
|
||||
stroke: new Stroke({ color: '#FF0000', width: 2 }),
|
||||
image: new Icon({
|
||||
anchor: [0.5, iconSize - 2],
|
||||
anchorXUnits: 'fraction',
|
||||
anchorYUnits: 'pixels',
|
||||
src: `/icons/poi.svg?size=${iconSize}&color=dd3300&border=3d2d1f&fill=ffcc00`,
|
||||
scale: 1.3,
|
||||
}),
|
||||
zIndex: Infinity, // Ensure that points are always displayed above other layers
|
||||
}),
|
||||
|
@ -89,17 +94,10 @@ export default {
|
|||
createPointsLayer(points: Point[]): VectorLayer {
|
||||
const pointFeatures = points.map((point: Point) => new Feature(point))
|
||||
return new VectorLayer({
|
||||
style: pointStyles.default,
|
||||
source: new VectorSource({
|
||||
features: pointFeatures,
|
||||
}),
|
||||
style: new Style({
|
||||
image: new Circle({
|
||||
radius: 6,
|
||||
fill: new Fill({ color: 'aquamarine' }),
|
||||
stroke: new Stroke({ color: 'blue', width: 1 }),
|
||||
}),
|
||||
zIndex: Infinity, // Ensure that points are always displayed above other layers
|
||||
}),
|
||||
})
|
||||
},
|
||||
|
||||
|
@ -194,6 +192,19 @@ export default {
|
|||
this.highlightedFeature = feature
|
||||
}
|
||||
},
|
||||
|
||||
removePointHighlight(layer: VectorLayer | null) {
|
||||
if (!layer || !this.highlightedPointId) {
|
||||
return
|
||||
}
|
||||
|
||||
const feature = this.highlightedFeature
|
||||
if (feature) {
|
||||
feature.setStyle(pointStyles.default)
|
||||
this.highlightedPointId = null
|
||||
this.highlightedFeature = null
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -35,9 +35,11 @@ export default defineConfig((env) => {
|
|||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
// proxy requests with the API path to the server
|
||||
// Proxy requests with the API path to the server
|
||||
// <http://localhost:5173/api> -> <http://localhost:3000/api>
|
||||
[serverAPIPath]: serverURL.origin,
|
||||
// Proxy requests to /icons/poi.svg to the server
|
||||
'/icons/poi.svg': `${serverURL.origin}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
68
src/routes/Icons.ts
Normal file
68
src/routes/Icons.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import { Request, Response } from 'express';
|
||||
|
||||
import Route from './Route';
|
||||
import Routes from './Routes';
|
||||
|
||||
const circleSVGTemplate = `
|
||||
<circle class="{{CLASS}}" cx="50" cy="34.902" r="18.597"></circle>
|
||||
`
|
||||
|
||||
const poiSVGTemplate = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="{{SIZE}}" height="{{SIZE}}" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="{{CLASS}}" preserveAspectRatio="xMidYMid meet">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
.with-border path {
|
||||
background-size: cover;
|
||||
box-sizing: border-box;
|
||||
stroke: #{{BORDER}};
|
||||
stroke-width: 1px;
|
||||
stroke-dasharray: none;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.with-fill circle {
|
||||
fill: #{{FILL}};
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path d="M50.002 0C30.763 0 15 15.718 15 34.902c0 7.432 2.374 14.34 6.392 20.019L45.73 96.994c3.409 4.453 5.675 3.607 8.51-.235l26.843-45.683c.542-.981.967-2.026 1.338-3.092A34.446 34.446 0 0 0 85 34.902C85 15.718 69.24 0 50.002 0zm0 16.354c10.359 0 18.597 8.218 18.597 18.548c0 10.33-8.238 18.544-18.597 18.544c-10.36 0-18.601-8.215-18.601-18.544c0-10.33 8.241-18.548 18.6-18.548z" fill="#{{COLOR}}"></path>{{CIRCLE}}
|
||||
</svg>
|
||||
`
|
||||
|
||||
class PoiIconRoute extends Route {
|
||||
protected version: string;
|
||||
|
||||
constructor() {
|
||||
super('/icons/poi.svg');
|
||||
}
|
||||
|
||||
get = async (req: Request, res: Response) => {
|
||||
const { color, size, classes, border, fill } = req.query;
|
||||
let classesArray = new Set(classes?.toString().split(' ') || []);
|
||||
|
||||
if (border) {
|
||||
classesArray.add('with-border');
|
||||
}
|
||||
|
||||
if (fill) {
|
||||
classesArray.add('with-fill');
|
||||
}
|
||||
|
||||
const svg = poiSVGTemplate
|
||||
.replaceAll('{{COLOR}}', color?.toString() || '000000')
|
||||
.replaceAll('{{SIZE}}', size?.toString() || '100')
|
||||
.replaceAll('{{CLASS}}', classesArray.size ? Array.from(classesArray).join(' ') : '')
|
||||
.replaceAll('{{BORDER}}', border?.toString() || '000000')
|
||||
.replaceAll('{{FILL}}', fill?.toString() || '000000')
|
||||
.replaceAll('{{CIRCLE}}', fill ? circleSVGTemplate : '');
|
||||
|
||||
res.setHeader('Content-Type', 'image/svg+xml');
|
||||
res.send(svg);
|
||||
}
|
||||
}
|
||||
|
||||
class IconRoutes extends Routes {
|
||||
public routes = [new PoiIconRoute()];
|
||||
}
|
||||
|
||||
export default IconRoutes;
|
|
@ -1,9 +1,15 @@
|
|||
import ApiRoutes from "./api";
|
||||
import IconRoutes from "./Icons";
|
||||
import Routes from "./Routes";
|
||||
|
||||
class AllRoutes extends Routes {
|
||||
private api: ApiRoutes = new ApiRoutes();
|
||||
public routes = [...this.api.routes];
|
||||
private icons: IconRoutes = new IconRoutes();
|
||||
|
||||
public routes = [
|
||||
...this.api.routes,
|
||||
...this.icons.routes,
|
||||
];
|
||||
}
|
||||
|
||||
export default AllRoutes;
|
||||
|
|
|
@ -2,7 +2,13 @@
|
|||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"module": "commonjs",
|
||||
"lib": ["dom", "es6", "es2017", "esnext.asynciterable"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"es6",
|
||||
"es2017",
|
||||
"ES2021.String",
|
||||
"esnext.asynciterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
|
|
Loading…
Add table
Reference in a new issue