Implement map crosshairs.

These are coordinates that are supplied in the fragment part of the
URL. When available, a marker is shown at the given positions.
Labels may also be given and are also shown.
This commit is contained in:
D. Berge
2025-08-08 18:51:54 +02:00
parent 3a769e7fd0
commit 17e6564e70

View File

@@ -305,6 +305,14 @@
<label><!-- No lines available --></label>
<label><!-- No heatmap available --></label>
<template v-if="crosshairsPositions.length">
<span>Markers</span>
<label title="Show crosshair markers"><v-icon small left class="mx-0">mdi-crosshairs</v-icon> <input type="checkbox" value="crosshairs" v-model="layerSelection"/></label>
<label title="Show marker labels" v-if="crosshairsLabels.length"><v-icon small left class="mx-0">mdi-format-text-variant</v-icon> <input type="checkbox" value="labels" v-model="layerSelection"/></label>
<label v-else><!-- No labels provided --></label>
<label><!-- No heatmap available --></label>
</template>
</div>
<!--
<input id="lyr-psl" type="checkbox" value="psl" v-model="layerSelection"/>
@@ -331,12 +339,6 @@
<input id="lyr-nav" type="checkbox" value="nav" v-model="layerSelection"/>
<label for="lyr-nav" title="Vessel track">Navigation trail</label>
-->
<form>
<template v-if="crosshairsPosition.length">
<input id="lyr-crosshairs" type="checkbox" value="crosshairs" v-model="layerSelection"/>
<label for="lyr-crosshairs" title="Show or hide the crosshairs position marker">Crosshairs marker</label>
</template>
</form>
<!-- QC data: This section is meant to show (some) QC results in a graphical way,
as 3D columns with lengths proportional to the QC values. Not implemented
@@ -576,7 +578,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import zoomFitIcon from '@/assets/zoom-fit-best.svg'
import { Deck, FlyToInterpolator } from '@deck.gl/core';
import { IconLayer } from '@deck.gl/layers';
import { IconLayer, TextLayer } from '@deck.gl/layers';
import { DougalBinaryBundle, DougalBinaryChunkSequential, DougalBinaryChunkInterleaved } from '@dougal/binary';
let deck;
@@ -620,7 +622,8 @@ export default {
heatmapValue: "total_error",
isFullscreen: false,
crosshairsPosition: [],
crosshairsPositions: [],
crosshairsLabels: [],
searchText: "",
searchVisible: false,
@@ -1296,19 +1299,31 @@ export default {
}
if (hash) {
const [ view, layers, crosshairs ] = hash.split(":");
const [ view, layers, crosshairs, labels ] = hash.split(":");
if (view) {
const key = {x: "longitude", y: "latitude", z: "zoom", p: "pitch", b: "bearing"};
const entries = view
.split(/([xyzpb])/)
.filter( v => v.length )
.reduce( (acc, cur, idx) =>
(idx % 2 ? acc[acc.length-1].push(cur) : acc.push([cur]), acc), []
).map( ([k, v]) => [ key[k], Number(v) ] )
.filter( ([k, v]) => k && !isNaN(v) );
if (crosshairs) {
this.crosshairsPositions = crosshairs
.split(";")
.map(p => p
.split(",", 2)
.filter( v => v.trim().length )
.map( v => Number(v) )
.filter( v => !isNaN(v) )
)
.filter( v => v.length == 2);
this.viewState = Object.fromEntries(entries);
if (this.crosshairsPositions.length) {
if (!this.layerSelection.includes("crosshairs")) {
this.layerSelection.push("crosshairs");
}
}
if (labels) {
this.crosshairsLabels = labels.split("\u001F").map( i => decodeURIComponent(i) );
if (!this.layerSelection.includes("labels")) {
this.layerSelection.push("labels");
}
}
}
@@ -1343,15 +1358,18 @@ export default {
}
}
if (crosshairs) {
const [ x, y ] = crosshairs.split(",").map(i => Number(i));
if (view) {
const key = {x: "longitude", y: "latitude", z: "zoom", p: "pitch", b: "bearing"};
const entries = view
.split(/([xyzpb])/)
.filter( v => v.length )
.reduce( (acc, cur, idx) =>
(idx % 2 ? acc[acc.length-1].push(cur) : acc.push([cur]), acc), []
).map( ([k, v]) => [ key[k], Number(v) ] )
.filter( ([k, v]) => k && !isNaN(v) );
this.viewState = Object.fromEntries(entries);
if (!isNaN(x) && !isNaN(y)) {
this.crosshairsPosition = [ x, y ];
if (!this.layerSelection.includes("crosshairs")) {
this.layerSelection.push("crosshairs");
}
}
}
}
},
@@ -1484,12 +1502,12 @@ export default {
this.layersAvailable.seqfp = this.finalSequencesPointsLayer;
this.layersAvailable.seqrh = this.heatmapLayer;
this.layersAvailable.crosshairs = (options = {}) => {
return new IconLayer({
id: 'crosshairs',
data: [{position: [...this.crosshairsPosition]}],
getSize: 24,
data: this.crosshairsPositions,
getPosition: d => d,
getSize: 32,
getColor: [ 255, 0, 0 ],
getIcon: (d) => {
return {
@@ -1502,6 +1520,21 @@ export default {
});
};
this.layersAvailable.labels = (options = {}) => {
return new TextLayer({
id: 'labels',
data: this.crosshairsPositions,
getPosition: d => d,
getSize: 18,
getColor: [ 255, 0, 0 ],
getText: (d, {index}) => this.crosshairsLabels[index],
getPixelOffset: [ 0, -24 ],
getTextAnchor: "start",
getAlignmentBase: "bottom",
...options
});
};
deck = new Deck({
parent: document.getElementById("map"),
mapStyle: 'https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json',