Added dynamic SVG map marker.

This commit is contained in:
Fabio Manganiello 2025-03-23 23:43:39 +01:00
parent 2f51d3843c
commit 595d9528d5
Signed by: blacklight
GPG key ID: D90FBA7F76362774
6 changed files with 121 additions and 22 deletions

View file

@ -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() {

View file

@ -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>

View file

@ -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
View 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;

View file

@ -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;

View file

@ -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",