Add a polygon around each accommodation on the public map

I can not use <a> in that map because Leaflet handles the mouse over
before the anchors sees it, thus it is impossible to click on them; i
have to use a Leaflet layer.

Fortunately, i can just use the <path>’s coordinates as
the polygon points, because with CRS.Simple the coordinates map to
pixel, except for the reversed Y/latitude coordinate. Unfortunately,
<path> coordinates are not straightforward to get: I have to follow the
drawing coordinates, taking into account the current transformation
(CTM), and keeping the last point around for relative coordinates.
Bézier curves are simplified to a straight line from start to end.

There is one single accommodation that started with a relative move
command (m), which apparently have to be treated as an absolute
move (M), but subsequent pairs are treated as relative coordinates[0].

It was easier for me to convert that relative move to absolute and add
a relative lineto command (l) to the next pair.

For now, all i do is highlight the accommodation and zoom it on click,
because i do not know how i should the accommodation’s information.

[0]: https://www.w3.org/TR/SVG11/paths.html#PathDataMovetoCommands
This commit is contained in:
jordi fita mas 2024-01-25 04:28:51 +01:00
parent 0da6845bde
commit 8d49857dae
3 changed files with 134 additions and 25 deletions

132
web/static/map.js Normal file
View File

@ -0,0 +1,132 @@
(function () {
'use strict';
const container = document.getElementById('campground_map');
container.style.height = '80vh';
const svg = container.childNodes[0];
svg.remove();
const latLngBounds = L.latLngBounds([[0, 0], [1192, 1500]]);
const map = L.map(container, {
center: latLngBounds.getCenter(),
minZoom: -2,
maxZoom: 2,
zoom: -1,
scrollWheelZoom: false,
maxBounds: latLngBounds,
maxBoundsViscosity: 1.0,
zoomSnap: 0,
crs: L.CRS.Simple,
});
map.fitBounds(latLngBounds);
L.svgOverlay(svg, latLngBounds).addTo(map);
// from https://github.com/jkroso/parse-svg-path/
const parse = (function () {
'use strict';
const length = {a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0}
const segment = /([astvzqmhlc])([^astvzqmhlc]*)/ig;
const number = /-?[0-9]*\.?[0-9]+(?:e[-+]?\d+)?/ig;
function parseValues(args) {
const numbers = args.match(number)
return numbers ? numbers.map(Number) : []
}
return function (path) {
const data = []
path.replace(segment, function (_, command, args) {
let type = command.toLowerCase()
args = parseValues(args)
// overloaded moveTo
if (type === 'm' && args.length > 2) {
data.push([command].concat(args.splice(0, 2)))
type = 'l'
command = command === 'm' ? 'l' : 'L'
}
while (true) {
if (args.length === length[type]) {
args.unshift(command)
return data.push(args)
}
if (args.length < length[type]) throw new Error('malformed path data')
data.push([command].concat(args.splice(0, length[type])))
}
})
return data
}
})();
function highlightAccommodation(e) {
const layer = e.target;
layer.setStyle({
color: '#ffe673',
});
layer.bringToFront();
}
function resetHighlight(e) {
const layer = e.target;
layer.setStyle({
color: 'transparent',
});
}
function zoomToAccommodation(e) {
map.fitBounds(e.target.getBounds());
}
// XXX: this is from the “parceles” layer.
const ctm = [1, 0, 0, 1, 83.2784, 66.1766];
const transform = function (x, y) {
return [ctm[0] * x + ctm[2] * y + ctm[4], ctm[1] * x + ctm[3] * y + ctm[5]];
}
const prefix = 'cp_';
for (const campsite of Array.from(container.querySelectorAll(`[id^="${prefix}"]`))) {
const label = campsite.id.substring(prefix.length);
if (!label) {
continue;
}
const path = campsite.firstElementChild;
const commands = parse(path.getAttribute('d'));
const points = [];
const p0 = [0, 0];
for (const cmd of commands) {
switch (cmd[0]) {
case 'M':
case 'L':
const cmdM = transform(cmd[1], cmd[2]);
p0[0] = cmdM[0];
p0[1] = latLngBounds.getNorth() - cmdM[1];
break;
case 'm':
case 'l':
p0[0] += cmd[1];
p0[1] -= cmd[2];
break;
case 'C':
const cmdC = transform(cmd[5], cmd[6]);
p0[0] = cmdC[0];
p0[1] = latLngBounds.getNorth() - cmdC[1];
break;
case 'Z':
case 'z':
continue;
default:
console.error(cmd);
}
points.push([p0[1], p0[0]]);
}
const accommodation = L.polygon(points, {color: 'transparent'}).addTo(map);
accommodation.on({
mouseover: highlightAccommodation,
mouseout: resetHighlight,
click: zoomToAccommodation,
})
}
})();

View File

@ -139,7 +139,7 @@
</g>
</g>
<g id="cp_93">
<path d="m 205.22991,189.4224 10.03493,3.534 18.2341,2.351 30.94188,-4.546 -0.70438,28.184 -30.99376,-0.648 -35.62613,1.214 z" style="fill:rgb(239,237,236);stroke:rgb(82,84,83);stroke-width:1px;stroke-miterlimit:2;"/>
<path d="M 205.22991,189.4224 l 10.03493,3.534 18.2341,2.351 30.94188,-4.546 -0.70438,28.184 -30.99376,-0.648 -35.62613,1.214 z" style="fill:rgb(239,237,236);stroke:rgb(82,84,83);stroke-width:1px;stroke-miterlimit:2;"/>
<g transform="matrix(1,0,0,1,-13.4495,0.21286)">
<path d="M240.23,208.399L241.25,208.094C241.301,208.684 241.496,209.134 241.836,209.445C242.176,209.755 242.637,209.911 243.219,209.911C243.894,209.911 244.431,209.622 244.827,209.046C245.224,208.47 245.424,207.653 245.428,206.594L245.416,206.166C245.228,206.655 244.934,207.042 244.531,207.33C244.129,207.617 243.664,207.76 243.137,207.76C242.309,207.76 241.64,207.502 241.13,206.987C240.62,206.471 240.365,205.832 240.365,205.071C240.365,204.657 240.437,204.273 240.582,203.919C240.727,203.566 240.924,203.267 241.174,203.023C241.424,202.779 241.72,202.587 242.061,202.449C242.403,202.31 242.769,202.241 243.16,202.241C244.277,202.241 245.094,202.641 245.609,203.442C246.129,204.239 246.389,205.27 246.389,206.536C246.389,207.008 246.352,207.455 246.277,207.877C246.203,208.299 246.082,208.695 245.914,209.064C245.746,209.433 245.539,209.75 245.293,210.016C245.047,210.282 244.742,210.492 244.379,210.646C244.016,210.8 243.609,210.877 243.16,210.877C242.348,210.877 241.679,210.642 241.153,210.171C240.628,209.701 240.32,209.11 240.23,208.399ZM245.041,205C245.041,204.473 244.869,204.038 244.525,203.694C244.193,203.35 243.758,203.178 243.219,203.178C242.687,203.178 242.252,203.354 241.912,203.705C241.576,204.053 241.408,204.496 241.408,205.036C241.408,205.579 241.576,206.012 241.912,206.336C242.248,206.661 242.684,206.823 243.219,206.823C243.75,206.823 244.186,206.654 244.528,206.316C244.87,205.978 245.041,205.539 245.041,205Z" style="fill:rgb(82,84,83);fill-rule:nonzero;"/>
<path d="M247.139,208.457L248.135,208.012C248.209,208.59 248.433,209.051 248.806,209.395C249.179,209.739 249.654,209.911 250.232,209.911C250.775,209.911 251.235,209.738 251.612,209.392C251.989,209.046 252.178,208.598 252.178,208.047C252.178,207.457 252.005,206.995 251.659,206.659C251.313,206.323 250.852,206.155 250.273,206.155C250.043,206.155 249.82,206.192 249.605,206.266C249.391,206.34 249.213,206.442 249.072,206.571L248.469,205.827L251.305,203.33L247.496,203.33L247.496,202.358L252.887,202.358L252.887,203.155L250.262,205.422C250.398,205.395 250.545,205.381 250.701,205.381C251.139,205.381 251.546,205.483 251.923,205.686C252.3,205.889 252.608,206.192 252.849,206.594C253.089,206.996 253.209,207.457 253.209,207.977C253.209,208.836 252.928,209.535 252.365,210.072C251.803,210.609 251.092,210.877 250.232,210.877C249.42,210.877 248.739,210.657 248.19,210.215C247.642,209.774 247.291,209.188 247.139,208.457Z" style="fill:rgb(82,84,83);fill-rule:nonzero;"/>

Before

Width:  |  Height:  |  Size: 372 KiB

After

Width:  |  Height:  |  Size: 372 KiB

View File

@ -17,28 +17,5 @@
<div id="campground_map">{{ template "campground_map.svg" }}</div>
<script src="/static/leaflet@1.9.4/leaflet.js"></script>
<script>(function () {
'use strict';
const container = document.getElementById('campground_map');
container.style.height = '80vh';
const svg = container.childNodes[0];
svg.remove();
const latLngBounds = L.latLngBounds([[0, 0], [1192, 1500]]);
const map = L.map(container, {
center: latLngBounds.getCenter(),
minZoom: -2,
maxZoom: 3,
zoom: -1,
scrollWheelZoom: false,
maxBounds: latLngBounds,
maxBoundsViscosity: 1.0,
zoomSnap: 0,
crs: L.CRS.Simple,
});
map.fitBounds(latLngBounds);
L.svgOverlay(svg, latLngBounds).addTo(map);
})();
</script>
<script src="/static/map.js?v={{ camperVersion }}"></script>
{{- end }}