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 lines available --></label>
<label><!-- No heatmap 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> </div>
<!-- <!--
<input id="lyr-psl" type="checkbox" value="psl" v-model="layerSelection"/> <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"/> <input id="lyr-nav" type="checkbox" value="nav" v-model="layerSelection"/>
<label for="lyr-nav" title="Vessel track">Navigation trail</label> <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, <!-- 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 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 zoomFitIcon from '@/assets/zoom-fit-best.svg'
import { Deck, FlyToInterpolator } from '@deck.gl/core'; 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'; import { DougalBinaryBundle, DougalBinaryChunkSequential, DougalBinaryChunkInterleaved } from '@dougal/binary';
let deck; let deck;
@@ -620,7 +622,8 @@ export default {
heatmapValue: "total_error", heatmapValue: "total_error",
isFullscreen: false, isFullscreen: false,
crosshairsPosition: [], crosshairsPositions: [],
crosshairsLabels: [],
searchText: "", searchText: "",
searchVisible: false, searchVisible: false,
@@ -1296,19 +1299,31 @@ export default {
} }
if (hash) { if (hash) {
const [ view, layers, crosshairs ] = hash.split(":"); const [ view, layers, crosshairs, labels ] = hash.split(":");
if (view) { if (crosshairs) {
const key = {x: "longitude", y: "latitude", z: "zoom", p: "pitch", b: "bearing"}; this.crosshairsPositions = crosshairs
const entries = view .split(";")
.split(/([xyzpb])/) .map(p => p
.filter( v => v.length ) .split(",", 2)
.reduce( (acc, cur, idx) => .filter( v => v.trim().length )
(idx % 2 ? acc[acc.length-1].push(cur) : acc.push([cur]), acc), [] .map( v => Number(v) )
).map( ([k, v]) => [ key[k], Number(v) ] ) .filter( v => !isNaN(v) )
.filter( ([k, v]) => k && !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) { if (view) {
const [ x, y ] = crosshairs.split(",").map(i => Number(i)); 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.seqfp = this.finalSequencesPointsLayer;
this.layersAvailable.seqrh = this.heatmapLayer; this.layersAvailable.seqrh = this.heatmapLayer;
this.layersAvailable.crosshairs = (options = {}) => { this.layersAvailable.crosshairs = (options = {}) => {
return new IconLayer({ return new IconLayer({
id: 'crosshairs', id: 'crosshairs',
data: [{position: [...this.crosshairsPosition]}], data: this.crosshairsPositions,
getSize: 24, getPosition: d => d,
getSize: 32,
getColor: [ 255, 0, 0 ], getColor: [ 255, 0, 0 ],
getIcon: (d) => { getIcon: (d) => {
return { 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({ deck = new Deck({
parent: document.getElementById("map"), parent: document.getElementById("map"),
mapStyle: 'https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json', mapStyle: 'https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json',