mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 09:37:08 +00:00
It may or may not be permanenet, once tasks #322, #323, #324, #325 are implemented. Closes #326
1297 lines
40 KiB
Vue
1297 lines
40 KiB
Vue
<template>
|
||
<div>
|
||
<v-overlay :value="error">
|
||
<v-alert type="error">{{ error }}</v-alert>
|
||
</v-overlay>
|
||
<div id="map">
|
||
</div>
|
||
<div> <!-- This div holds the map overlays -->
|
||
<div class="map-overlay top left expand-on-hover">
|
||
<div class="map-overlay-icon">
|
||
<v-icon>mdi-menu</v-icon>
|
||
</div>
|
||
<div class="map-overlay-inner">
|
||
<h3>Layers</h3>
|
||
<form>
|
||
<input id="lyr-osm" type="checkbox" value="osm" v-model="layerSelection"/>
|
||
<label for="lyr-osm">OpenStreetMap</label>
|
||
|
||
<input id="lyr-sea" type="checkbox" value="sea" v-model="layerSelection"/>
|
||
<label for="lyr-sea">OpenSeaMap</label>
|
||
|
||
<input id="lyr-nau" type="checkbox" value="nau" v-model="layerSelection"/>
|
||
<label for="lyr-nau" title="Scan of Norway's nautical charts">Nautical charts (NO)</label>
|
||
</form>
|
||
|
||
<hr class="my-2"/>
|
||
|
||
<div class="lines-points">
|
||
|
||
<span>Vessel track</span>
|
||
<label title="Show points"><v-icon small left class="mx-0">mdi-vector-point</v-icon> <input type="checkbox" value="navp" v-model="layerSelection"/></label>
|
||
<label title="Show lines" disabled><v-icon small left class="mx-0">mdi-vector-line</v-icon> <input type="checkbox" value="navl" v-model="layerSelection"/></label>
|
||
<label><!-- No heatmap available --></label>
|
||
|
||
<span>Sail lines</span>
|
||
<label title="Show points"><v-icon small left class="mx-0">mdi-vector-point</v-icon> <input type="checkbox" value="pslp" v-model="layerSelection"/></label>
|
||
<label title="Show lines" disabled><v-icon small left class="mx-0">mdi-vector-line</v-icon> <input type="checkbox" value="psll" v-model="layerSelection"/></label>
|
||
<label><!-- No heatmap available --></label>
|
||
|
||
<span>Preplots</span>
|
||
<label title="Show points"><v-icon small left class="mx-0">mdi-vector-point</v-icon> <input type="checkbox" value="pplp" v-model="layerSelection"/></label>
|
||
<label title="Show lines"><v-icon small left class="mx-0">mdi-vector-line</v-icon> <input type="checkbox" value="ppll" v-model="layerSelection"/></label>
|
||
<label><!-- No heatmap available --></label>
|
||
|
||
<span>Plan</span>
|
||
<label title="Show points"><v-icon small left class="mx-0">mdi-vector-point</v-icon> <input type="checkbox" value="planp" v-model="layerSelection"/></label>
|
||
<label title="Show lines"><v-icon small left class="mx-0">mdi-vector-line</v-icon> <input type="checkbox" value="planl" v-model="layerSelection"/></label>
|
||
<label><!-- No heatmap available --></label>
|
||
|
||
<span>Raw data</span>
|
||
<label title="Show points"><v-icon small left class="mx-0">mdi-vector-point</v-icon> <input type="checkbox" value="seqrp" v-model="layerSelection"/></label>
|
||
<label title="Show lines"><v-icon small left class="mx-0">mdi-vector-line</v-icon> <input type="checkbox" value="seqrl" v-model="layerSelection"/></label>
|
||
<label title="Show position error"><v-icon small left class="mx-0">mdi-dots-grid</v-icon> <input type="checkbox" value="seqrh" v-model="layerSelection"/></label>
|
||
|
||
<span>Final data</span>
|
||
<label title="Show points"><v-icon small left class="mx-0">mdi-vector-point</v-icon> <input type="checkbox" value="seqfp" v-model="layerSelection"/></label>
|
||
<label title="Show lines"><v-icon small left class="mx-0">mdi-vector-line</v-icon> <input type="checkbox" value="seqfl" v-model="layerSelection"/></label>
|
||
<label title="Show position error"><v-icon small left class="mx-0">mdi-dots-grid</v-icon> <input type="checkbox" value="seqfh" v-model="layerSelection"/></label>
|
||
|
||
<span>Events</span>
|
||
<label title="Show points"><v-icon small left class="mx-0">mdi-vector-point</v-icon> <input type="checkbox" value="log" v-model="layerSelection"/></label>
|
||
<label><!-- No lines available --></label>
|
||
<label><!-- No heatmap available --></label>
|
||
|
||
</div>
|
||
<!--
|
||
<input id="lyr-psl" type="checkbox" value="psl" v-model="layerSelection"/>
|
||
<label for="lyr-psl" title="Vessel preplots">Sail lines</label>
|
||
|
||
<input id="lyr-ppl" type="checkbox" value="ppl" v-model="layerSelection"/>
|
||
<label for="lyr-ppl" title="Source preplots">Preplot lines</label>
|
||
|
||
<input id="lyr-plan" type="checkbox" value="plan" v-model="layerSelection"/>
|
||
<label for="lyr-plan" title="Sequences in the planner">Planned lines</label>
|
||
|
||
<input id="lyr-preplots" type="checkbox" value="preplots" v-model="layerSelection"/>
|
||
<label for="lyr-preplots" title="Shotpoint deviation from preplot position">Preplot error</label>
|
||
|
||
<input id="lyr-peh" type="checkbox" value="peh" v-model="layerSelection"/>
|
||
<label for="lyr-peh" title="Shotpoint deviation from preplot position">Preplot error (heatmap)</label>
|
||
|
||
<input id="lyr-seq" type="checkbox" value="seq" v-model="layerSelection"/>
|
||
<label for="lyr-seq" title="Raw and/or final shot positions">Sequence data</label>
|
||
|
||
<input id="lyr-log" type="checkbox" value="log" v-model="layerSelection"/>
|
||
<label for="lyr-log" title="Event positions">Event log data</label>
|
||
|
||
<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>
|
||
<!--
|
||
<h3 class="mt-3" title="At least one of raw or final will always be selected">Sequence data</h3>
|
||
<form>
|
||
<input id="sd-raw" type="checkbox" value="R" v-model="sequenceDataTypes"/>
|
||
<label for="sd-raw">Show raw</label>
|
||
<input id="sd-final" type="checkbox" value="F" v-model="sequenceDataTypes"/>
|
||
<label for="sd-final">Show final</label>
|
||
</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
|
||
at this time. -->
|
||
<!-- BEGIN QC data
|
||
<h3
|
||
class="mt-3"
|
||
title="The selected attribute will be displayed as different length ‘columns’ in 3D view. Use Ctrl or Shift + Left Click to tilt the map."
|
||
>QC data aspect</h3>
|
||
<v-select
|
||
class="mt-1"
|
||
style="max-width: 20ex;"
|
||
dense
|
||
hide-details
|
||
clearable
|
||
:items="[ 'Depth', 'Pressure', 'Delta' ]"
|
||
>
|
||
</v-select>
|
||
END QC data -->
|
||
</div>
|
||
</div>
|
||
|
||
<div class="map-overlay top right">
|
||
<!-- TODO Implement search -->
|
||
<!--
|
||
<div>
|
||
<input v-show="searchVisible"
|
||
type="text"
|
||
class="mr-2"
|
||
placeholder="Search for a point or event"
|
||
v-model="searchText"
|
||
/>
|
||
<v-icon
|
||
class="my-1"
|
||
title="Find element"
|
||
@click="searchVisible = !searchVisible"
|
||
>mdi-crosshairs</v-icon>
|
||
</div>
|
||
-->
|
||
<!-- TODO Implement filtering -->
|
||
<!--
|
||
<div>
|
||
<input v-show="filterVisible"
|
||
type="text"
|
||
class="mr-2"
|
||
placeholder="Filter by sequence or line"
|
||
v-model="filterText"
|
||
/>
|
||
<v-icon
|
||
class="my-1"
|
||
title="Filter items"
|
||
@click="filterVisible = !filterVisible"
|
||
>mdi-filter-outline</v-icon>
|
||
</div>
|
||
-->
|
||
<div>
|
||
<v-icon
|
||
class="my-1 mt-4"
|
||
title="Fit view"
|
||
@click="zoomReset"
|
||
>mdi-magnify-scan</v-icon>
|
||
</div>
|
||
<div>
|
||
<v-icon
|
||
class="my-1"
|
||
title="Zoom in"
|
||
@click="zoomIn"
|
||
>mdi-magnify-plus-outline</v-icon>
|
||
</div>
|
||
<div>
|
||
<v-icon
|
||
class="my-1"
|
||
title="Zoom out"
|
||
@click="zoomOut"
|
||
>mdi-magnify-minus-outline</v-icon>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="map-overlay bottom right">
|
||
<div>
|
||
<v-progress-circular v-if="loading || loadingProgress !== null"
|
||
size="42"
|
||
color="primary"
|
||
title="Loading data…"
|
||
:indeterminate="loadingProgress === null"
|
||
:value="loadingProgress"
|
||
>{{ Math.round(loadingProgress) }}%</v-progress-circular>
|
||
<div v-else>
|
||
<v-icon
|
||
class="my-1"
|
||
title="Manual refresh"
|
||
@click="refresh"
|
||
>mdi-cloud-refresh-variant-outline</v-icon>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="map-overlay bottom left">
|
||
<div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
#map {
|
||
height: 100%;
|
||
background-color: #dae4f7;
|
||
}
|
||
|
||
.theme--dark #map {
|
||
background-color: #111;
|
||
}
|
||
|
||
.theme--dark .map-overlay-inner, .theme--dark .map-overlay-icon {
|
||
background-color: #222;
|
||
}
|
||
|
||
.map-overlay {
|
||
font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||
position: absolute;
|
||
z-index: 1;
|
||
}
|
||
|
||
.top {
|
||
top: 8px;
|
||
}
|
||
|
||
.bottom {
|
||
bottom: 8px;
|
||
}
|
||
|
||
.left {
|
||
left: 8px;
|
||
max-width: 20%;
|
||
}
|
||
|
||
.right {
|
||
right: 8px;
|
||
max-width: 20%;
|
||
text-align: right;
|
||
}
|
||
|
||
.map-overlay-icon {
|
||
background-color: white;
|
||
border: 1px solid black;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.map-overlay-inner {
|
||
padding: 10px;
|
||
background-color: white;
|
||
border: 1px solid black;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.map-overlay-inner form {
|
||
display: grid;
|
||
grid-template-columns: 36px 1fr;
|
||
}
|
||
|
||
.map-overlay-inner .lines-points {
|
||
display: grid;
|
||
grid-template-columns: minmax(auto, 1fr) 36px 36px 36px;
|
||
grid-column-gap: 4px;
|
||
}
|
||
|
||
.map-overlay-inner .lines-points :nth-child(4n+1) {
|
||
margin-right: 6px;
|
||
}
|
||
|
||
.map-overlay input[type=text] {
|
||
background-color: white;
|
||
border: 1px solid black;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.expand-on-hover:hover .map-overlay-icon {
|
||
display: none;
|
||
}
|
||
|
||
.expand-on-hover:not(:hover) .map-overlay-inner {
|
||
display: none;
|
||
}
|
||
|
||
.expand-on-hover:hover .map-overlay-inner {
|
||
display: block;
|
||
}
|
||
|
||
/* BEGIN
|
||
* This is meant to be a possible solution for keeping
|
||
* the layers menu open while selecting from a v-select
|
||
* in the context of displaying QC values. There may be
|
||
* better solutions.
|
||
|
||
.expand-on-hover:focus-within .map-overlay-icon {
|
||
display: none;
|
||
}
|
||
|
||
.expand-on-hover:focus-within .map-overlay-inner {
|
||
display: block;
|
||
}
|
||
|
||
* END
|
||
*/
|
||
|
||
</style>
|
||
|
||
<script>
|
||
|
||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||
import ftstamp from '@/lib/FormatTimestamp'
|
||
import zoomFitIcon from '@/assets/zoom-fit-best.svg'
|
||
import { markdown, markdownInline } from '@/lib/markdown';
|
||
|
||
import { Deck, WebMercatorViewport, FlyToInterpolator, CompositeLayer } from '@deck.gl/core';
|
||
import { GeoJsonLayer, LineLayer, BitmapLayer, ScatterplotLayer, IconLayer } from '@deck.gl/layers';
|
||
import {HeatmapLayer} from '@deck.gl/aggregation-layers';
|
||
import { TileLayer, MVTLayer } from '@deck.gl/geo-layers';
|
||
import * as d3a from 'd3-array';
|
||
|
||
//import { json } from 'd3-fetch';
|
||
|
||
import { SequenceDataLayer } from '@/lib/deck.gl';
|
||
import { unbundle, unpack, toJSON, isLittleEndian } from '@/lib/binary';
|
||
|
||
// Important info about performance:
|
||
// https://deck.gl/docs/developer-guide/performance#supply-attributes-directly
|
||
|
||
const endianness = isLittleEndian();
|
||
let deck;
|
||
|
||
/* Decoded SVG:
|
||
* <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||
<path style="fill:#ff0000;fill-opacity:1;stroke:none" d="M 7 3 L 7 4.03125 A 4.5 4.5 0 0 0 3.0332031 8 L 2 8 L 2 9 L 3.03125 9 A 4.5 4.5 0 0 0 7 12.966797 L 7 14 L 8 14 L 8 12.96875 A 4.5 4.5 0 0 0 11.966797 9 L 13 9 L 13 8 L 11.96875 8 A 4.5 4.5 0 0 0 8 4.0332031 L 8 3 L 7 3 z M 7 5.0390625 L 7 8 L 4.0410156 8 A 3.5 3.5 0 0 1 7 5.0390625 z M 8 5.0410156 A 3.5 3.5 0 0 1 10.960938 8 L 8 8 L 8 5.0410156 z M 4.0390625 9 L 7 9 L 7 11.958984 A 3.5 3.5 0 0 1 4.0390625 9 z M 8 9 L 10.958984 9 A 3.5 3.5 0 0 1 8 11.960938 L 8 9 z " />
|
||
</svg>
|
||
*/
|
||
const hashMarkerIconURL = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KPHBhdGggc3R5bGU9ImZpbGw6I2ZmMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgZD0iTSA3IDMgTCA3IDQuMDMxMjUgQSA0LjUgNC41IDAgMCAwIDMuMDMzMjAzMSA4IEwgMiA4IEwgMiA5IEwgMy4wMzEyNSA5IEEgNC41IDQuNSAwIDAgMCA3IDEyLjk2Njc5NyBMIDcgMTQgTCA4IDE0IEwgOCAxMi45Njg3NSBBIDQuNSA0LjUgMCAwIDAgMTEuOTY2Nzk3IDkgTCAxMyA5IEwgMTMgOCBMIDExLjk2ODc1IDggQSA0LjUgNC41IDAgMCAwIDggNC4wMzMyMDMxIEwgOCAzIEwgNyAzIHogTSA3IDUuMDM5MDYyNSBMIDcgOCBMIDQuMDQxMDE1NiA4IEEgMy41IDMuNSAwIDAgMSA3IDUuMDM5MDYyNSB6IE0gOCA1LjA0MTAxNTYgQSAzLjUgMy41IDAgMCAxIDEwLjk2MDkzOCA4IEwgOCA4IEwgOCA1LjA0MTAxNTYgeiBNIDQuMDM5MDYyNSA5IEwgNyA5IEwgNyAxMS45NTg5ODQgQSAzLjUgMy41IDAgMCAxIDQuMDM5MDYyNSA5IHogTSA4IDkgTCAxMC45NTg5ODQgOSBBIDMuNSAzLjUgMCAwIDEgOCAxMS45NjA5MzggTCA4IDkgeiAiIC8+Cjwvc3ZnPgo=";
|
||
|
||
|
||
export default {
|
||
name: "Map",
|
||
|
||
components: {
|
||
|
||
},
|
||
|
||
data () {
|
||
return {
|
||
layerSelection: [],
|
||
layersAvailable: {},
|
||
sequenceDataTypes: [ "P", "F" ],
|
||
sequenceDataElements: [],
|
||
sequenceDataTStamp: null,
|
||
loadingProgress: null,
|
||
viewState: {},
|
||
viewStateDefaults: {
|
||
//maxZoom: 18,
|
||
//maxPitch: 85
|
||
},
|
||
|
||
crosshairsPosition: [],
|
||
|
||
searchText: "",
|
||
searchVisible: false,
|
||
filterText: "",
|
||
filterVisible: false,
|
||
|
||
error: null,
|
||
/*
|
||
layerDefinitions: [
|
||
{
|
||
id: "osm",
|
||
name: "OpenStreetMap"
|
||
},
|
||
{
|
||
id: "sea",
|
||
name: "OpenSeaMap"
|
||
},
|
||
{
|
||
id: "nau",
|
||
name: "Nautical charts (NO)",
|
||
title: "Scan of Norway's nautical charts"
|
||
disabled: true
|
||
},
|
||
{
|
||
|
||
|
||
]
|
||
*/
|
||
};
|
||
},
|
||
|
||
computed: {
|
||
|
||
sequenceDataPreplots () {
|
||
return {
|
||
tstamp: this.sequenceDataTStamp,
|
||
sequences: this.sequenceDataElements.filter(i => i.type == "P")
|
||
};
|
||
},
|
||
|
||
sequenceDataRaw () {
|
||
return {
|
||
tstamp: this.sequenceDataTStamp,
|
||
sequences: this.sequenceDataElements.filter(i => i.type == "R")
|
||
};
|
||
},
|
||
|
||
sequenceDataFinal () {
|
||
return {
|
||
tstamp: this.sequenceDataTStamp,
|
||
sequences: this.sequenceDataElements.filter(i => i.type == "F")
|
||
};
|
||
},
|
||
|
||
sequenceDataPostplots () {
|
||
const fn = i => i.type != "P" && this.sequenceDataTypes.includes(i.type);
|
||
return {
|
||
tstamp: this.sequenceDataTStamp,
|
||
sequences: this.sequenceDataElements.filter(fn)
|
||
};
|
||
},
|
||
|
||
fullProspectRaw () {
|
||
// Get raw data, so type = "R"
|
||
const sequences = this.sequenceDataElements.filter( i => i.type == "R" );
|
||
return [...sequences.map( i => toJSON(i.data) )].flat();
|
||
},
|
||
|
||
fullProspectFinal () {
|
||
// Get raw data, so type = "R"
|
||
const sequences = this.sequenceDataElements.filter( i => i.type == "F" );
|
||
return [...sequences.map( i => toJSON(i.data) )].flat();
|
||
},
|
||
|
||
...mapGetters(['user', 'loading', 'serverEvent', 'lineName', 'serverEvent']),
|
||
...mapState({projectSchema: state => state.project.projectSchema})
|
||
},
|
||
|
||
watch: {
|
||
layerSelection (newVal, oldVal) {
|
||
this.render();
|
||
},
|
||
|
||
viewState: {
|
||
handler () {
|
||
this.setViewState();
|
||
},
|
||
deep: true
|
||
},
|
||
|
||
sequenceDataTypes (val, old) {
|
||
// Ensure that at least one of raw or final is always selected
|
||
if (!val.includes("R") && !val.includes("F")) {
|
||
if (old.includes("F")) {
|
||
this.sequenceDataTypes.push("R");
|
||
} else {
|
||
this.sequenceDataTypes.push("F");
|
||
}
|
||
}
|
||
},
|
||
|
||
sequenceDataElements () {
|
||
//console.log("seq data changed");
|
||
this.sequenceDataTStamp = Date.now();
|
||
//this.render();
|
||
},
|
||
|
||
sequenceDataPreplots () {
|
||
this.render();
|
||
},
|
||
|
||
sequenceDataPostplots () {
|
||
this.render();
|
||
},
|
||
|
||
$route (to, from) {
|
||
if (to.name == "map") {
|
||
this.decodeURLHash();
|
||
}
|
||
},
|
||
|
||
},
|
||
|
||
methods: {
|
||
initView () {
|
||
},
|
||
|
||
render () {
|
||
if (deck) {
|
||
const layers = [];
|
||
for (const name in this.layersAvailable) {
|
||
//console.log("Visible", name, this.layerSelection.includes(name));
|
||
const fn = this.layersAvailable[name];
|
||
layers.push(fn({
|
||
visible: this.layerSelection.includes(name)
|
||
}));
|
||
}
|
||
|
||
//console.log("setProps", this.sequenceDataTStamp, deck.layerManager && deck.layerManager.getLayers().length);
|
||
|
||
//deck.setProps({layers, initialViewState: this.viewState});
|
||
deck.setProps({layers});
|
||
this.updateURL();
|
||
}
|
||
},
|
||
|
||
zoomReset () {
|
||
if (deck) {
|
||
const bounds = this.sequencesBBox();
|
||
if (bounds) {
|
||
const layer = deck.layerManager.layers[0]; // Get the first layer we come across
|
||
const initialViewState = {...this.viewStateDefaults, ...layer.context.viewport.fitBounds(bounds)};
|
||
initialViewState.transitionDuration = 500;
|
||
deck.setProps({initialViewState});
|
||
}
|
||
} else {
|
||
console.warn("Unable to calculate bounding box");
|
||
}
|
||
},
|
||
|
||
zoomIn () {
|
||
if (deck) {
|
||
const viewState = deck.getViewports()[0];
|
||
const initialViewState = {...this.viewStateDefaults, ...viewState};
|
||
initialViewState.zoom += 1;
|
||
initialViewState.transitionDuration = 300;
|
||
deck.setProps({initialViewState});
|
||
}
|
||
},
|
||
|
||
zoomOut () {
|
||
if (deck) {
|
||
const viewState = deck.getViewports()[0];
|
||
const initialViewState = {...this.viewStateDefaults, ...viewState};
|
||
initialViewState.zoom -= 1;
|
||
initialViewState.transitionDuration = 300;
|
||
deck.setProps({initialViewState});
|
||
}
|
||
},
|
||
|
||
async refresh () {
|
||
const cache = await caches.open("dougal");
|
||
for (const key of await cache.keys()) {
|
||
cache.delete(key);
|
||
}
|
||
await this.$root.sleep(300);
|
||
this.getSequenceData();
|
||
},
|
||
|
||
sequencesBBox (margin = 0.1) {
|
||
let [ λ0, φ0, λ1, φ1 ] = [ +Infinity, +Infinity, -Infinity, -Infinity ];
|
||
|
||
// FIXME Temporary fix to get this working again, but this.fullProspectRaw /
|
||
// this.fullProspectFinal are not intended to remain as JSON data.
|
||
|
||
const data = this.fullProspectRaw ?? this.fullProspectFinal;
|
||
|
||
if (data) {
|
||
for (const i of data) {
|
||
const λ = i.longitude;
|
||
const φ = i.latitude;
|
||
λ0 = Math.min(λ0, λ);
|
||
φ0 = Math.min(φ0, φ);
|
||
λ1 = Math.max(λ1, λ);
|
||
φ1 = Math.max(φ1, φ);
|
||
}
|
||
|
||
const δλ = λ1 - λ0;
|
||
const δφ = φ1 - φ0;
|
||
const mλ = δλ * margin;
|
||
const mφ = δφ * margin;
|
||
|
||
return [ [λ0-mλ, φ0-mφ], [λ1+mλ, φ1+mφ] ];
|
||
}
|
||
},
|
||
|
||
async getSequenceData () {
|
||
|
||
const types = ["P", "R", "F"];
|
||
const sequenceList = await this.api([`/project/${this.$route.params.project}/sequence`]);
|
||
const sequenceNumbers = sequenceList.map(i => i.sequence);
|
||
const sequenceCount = sequenceNumbers.length * types.length;
|
||
let count = 0;
|
||
|
||
const self = this;
|
||
const iterator = async function* () {
|
||
|
||
const download = (url) => {
|
||
|
||
const init = {
|
||
format: "arrayBuffer",
|
||
headers: {
|
||
Accept: "application/dougal-map-sequence+octet-stream"
|
||
}
|
||
};
|
||
|
||
return new Promise( async (resolve, reject) => {
|
||
const cb = (err, res) => {
|
||
if (res && !err) {
|
||
const etag = res.headers.get("ETag");
|
||
res.arrayBuffer().then(data => {
|
||
resolve({etag, data});
|
||
})
|
||
} else {
|
||
reject(err);
|
||
}
|
||
};
|
||
|
||
// Technically we do not need to await but this is
|
||
// to slow down the cient and avoid firing too many
|
||
// requests at once.
|
||
await self.api([url, init, cb, {cache: true}]);
|
||
});
|
||
|
||
}
|
||
|
||
for (const sequence of sequenceNumbers) {
|
||
for (const type of types) {
|
||
try {
|
||
const url = `/project/${self.$route.params.project}/sequence/${sequence}?geometry=${type}${endianness ? "" : "&endianness=big"}`;
|
||
|
||
const sequenceData = await download(url);
|
||
if (sequenceData) {
|
||
yield {
|
||
id: `${sequence}-${sequenceData.etag}`,
|
||
sequence: sequence,
|
||
type,
|
||
data: sequenceData.data
|
||
};
|
||
}
|
||
} catch (err) {
|
||
console.error(`Error downloading sequence ${sequence}`);
|
||
console.error(err);
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
for await (const value of iterator()) {
|
||
this.loadingProgress = ++count/sequenceCount*100;
|
||
//console.log("PRG", count, sequenceCount, this.loadingProgress, );
|
||
this.sequenceDataElements.push(Object.freeze(value));
|
||
}
|
||
this.loadingProgress = null;
|
||
console.log("passed for await", deck);
|
||
},
|
||
|
||
async initLayers (gl) {
|
||
console.log("SHOULD BE INITIALISING LAYERS HERE", gl);
|
||
this.decodeURL();
|
||
this.decodeURLHash();
|
||
deck.onViewStateChange = this.updateURL;
|
||
},
|
||
|
||
setViewState () {
|
||
const viewport = deck.getViewports()[0];
|
||
if (deck && viewport && this.viewState) {
|
||
const initialViewState = {
|
||
...this.initialViewState,
|
||
...viewport,
|
||
...this.viewState
|
||
};
|
||
deck.setProps({initialViewState});
|
||
}
|
||
},
|
||
|
||
updateURL ({viewState} = {}) {
|
||
if (!viewState && deck?.viewManager) {
|
||
viewState = deck.getViewports()[0];
|
||
}
|
||
|
||
if (viewState) {
|
||
const key = {x: "longitude", y: "latitude", z: "zoom", p: "pitch", b: "bearing"};
|
||
const view = Object.keys(key).map(k => [k, viewState[key[k]]]).filter(i => i[1]).map(i => i.join("")).join("");
|
||
const layers = this.layerSelection.join(";")
|
||
|
||
const value = [view, layers].join(":");
|
||
|
||
if (view.length && layers.length) {
|
||
localStorage.setItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/view/v1`, value);
|
||
}
|
||
|
||
}
|
||
},
|
||
|
||
decodeURL () {
|
||
const value = localStorage.getItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/view/v1`);
|
||
|
||
if (value) {
|
||
this.decodeURLHash(value);
|
||
}
|
||
},
|
||
|
||
decodeURLHash (hash) {
|
||
if (!hash) {
|
||
hash = document.location.hash.substring(1);
|
||
}
|
||
|
||
if (hash) {
|
||
const [ view, layers, crosshairs ] = hash.split(":");
|
||
|
||
if (view) {
|
||
const rx0 = /[xyzpb][+-]?[0-9]+\.?[0-9]*/g;
|
||
const rx1 = /([xyzpb])([+-]?[0-9]+\.?[0-9]*)/;
|
||
|
||
const key = {x: "longitude", y: "latitude", z: "zoom", p: "pitch", b: "bearing"};
|
||
|
||
this.viewState = Object.fromEntries(view.match(rx0).map(i => i.match(rx1).slice(1, 3)).map(i => {i[0] = key[i[0]]; i[1] = Number(i[1]); return i}));
|
||
}
|
||
|
||
if (layers) {
|
||
const l = layers.split(";").filter(i => this.layersAvailable.hasOwnProperty(i));
|
||
if (l.length) {
|
||
this.layerSelection = l;
|
||
}
|
||
}
|
||
|
||
if (crosshairs) {
|
||
const [ x, y ] = crosshairs.split(",").map(i => Number(i));
|
||
|
||
if (!isNaN(x) && !isNaN(y)) {
|
||
this.crosshairsPosition = [ x, y ];
|
||
if (!this.layerSelection.includes("crosshairs")) {
|
||
this.layerSelection.push("crosshairs");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
checkWebGLSupport() {
|
||
const canvas = document.createElement('canvas');
|
||
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
|
||
if (!gl) {
|
||
//this.error = 'WebGL is not supported in this browser or device.';
|
||
//this.loading = false;
|
||
console.error('WebGL not supported');
|
||
return false;
|
||
}
|
||
const isWebGL2 = gl instanceof WebGL2RenderingContext;
|
||
console.log('WebGL Support:', isWebGL2 ? 'WebGL2' : 'WebGL1');
|
||
return true;
|
||
},
|
||
|
||
toBase64 (binary) {
|
||
return btoa(String.fromCharCode(...new Uint8Array(binary)));
|
||
},
|
||
|
||
fromBase64 (text) {
|
||
const decoded = atob(text);
|
||
const arr = new Uint8Array(decoded.length);
|
||
for (let i=0; i<decoded.length; i++) {
|
||
arr[i] = decoded.charCodeAt(i);
|
||
}
|
||
return arr.buffer;
|
||
},
|
||
|
||
...mapActions(["api"])
|
||
|
||
},
|
||
|
||
async mounted () {
|
||
|
||
if (!this.checkWebGLSupport()) {
|
||
for (const countdown = 5; countdown > 0; countdown--) {
|
||
if (countdown) {
|
||
this.error = `This browser does not support WebGL. Switching to legacy map view in ${countdown} seconds.`;
|
||
await new Promise( (resolve) => setTimeout(() => resolve, 1000) );
|
||
}
|
||
}
|
||
this.error = null;
|
||
console.log("TODO: Should switch to legacy map view");
|
||
}
|
||
|
||
const COLOR_SCALE = [
|
||
// negative
|
||
[65, 182, 196],
|
||
[127, 205, 187],
|
||
[199, 233, 180],
|
||
[237, 248, 177],
|
||
|
||
// positive
|
||
[255, 255, 204],
|
||
[255, 237, 160],
|
||
[254, 217, 118],
|
||
[254, 178, 76],
|
||
[253, 141, 60],
|
||
[252, 78, 42],
|
||
[227, 26, 28],
|
||
[189, 0, 38],
|
||
[128, 0, 38]
|
||
];
|
||
|
||
this.layersAvailable.osm = (options = {}) => {
|
||
return new TileLayer({
|
||
id: "osm",
|
||
// https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
|
||
data: 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||
|
||
minZoom: 0,
|
||
maxZoom: 19,
|
||
tileSize: 256,
|
||
|
||
renderSubLayers: props => {
|
||
const {
|
||
bbox: {west, south, east, north}
|
||
} = props.tile;
|
||
|
||
return new BitmapLayer(props, {
|
||
data: null,
|
||
image: props.data,
|
||
bounds: [west, south, east, north]
|
||
});
|
||
},
|
||
...options
|
||
})
|
||
}
|
||
|
||
/*
|
||
// OSM tiles layer. Handy to make water transparent
|
||
// but super reliable yet
|
||
|
||
this.layersAvailable.osm = (options = {}) => {
|
||
return new MVTLayer({
|
||
id: 'osm',
|
||
data: 'https://vector.openstreetmap.org/shortbread_v1/{z}/{x}/{y}.mvt',
|
||
minZoom: 0,
|
||
maxZoom: 14,
|
||
getFillColor: feature => {
|
||
const layer = feature.properties.layerName;
|
||
//console.log("layer =", layer, feature.properties.kind);
|
||
switch (layer) {
|
||
case "ocean":
|
||
return [0, 0, 0, 0];
|
||
case "land":
|
||
return [ 0x54, 0x6E, 0x7A, 255 ];
|
||
default:
|
||
return [ 240, 240, 240, 255 ];
|
||
}
|
||
},
|
||
getLineColor: feature => {
|
||
if (feature.properties.layer === 'water') {
|
||
return [0, 0, 0, 0]; // No outline for water
|
||
}
|
||
return [192, 192, 192, 255]; // Default line color for roads, etc.
|
||
},
|
||
getLineWidth: feature => {
|
||
if (feature.properties.highway) {
|
||
return feature.properties.highway === 'motorway' ? 6 : 3; // Example road widths
|
||
}
|
||
return 1;
|
||
},
|
||
stroked: true,
|
||
filled: true,
|
||
pickable: true
|
||
});
|
||
}
|
||
*/
|
||
|
||
|
||
/*
|
||
this.layersAvailable.nau = (options = {}) => {
|
||
return new TileLayer({
|
||
id: "nau",
|
||
// https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
|
||
data: 'https://opencache.statkart.no/gatekeeper/gk/gk.open_gmaps?layers=sjokartraster&zoom={z}&x={x}&y={y}',
|
||
|
||
minZoom: 0,
|
||
maxZoom: 19,
|
||
tileSize: 256,
|
||
|
||
renderSubLayers: props => {
|
||
const {
|
||
bbox: {west, south, east, north}
|
||
} = props.tile;
|
||
|
||
return new BitmapLayer(props, {
|
||
data: null,
|
||
image: props.data,
|
||
bounds: [west, south, east, north]
|
||
});
|
||
},
|
||
...options
|
||
})
|
||
}
|
||
*/
|
||
|
||
this.layersAvailable.navp = (options = {}) => {
|
||
return new ScatterplotLayer({
|
||
id: 'navp',
|
||
data: `/api/navdata?limit=10000`,
|
||
getPosition: (d) => ([d.longitude, d.latitude]),
|
||
getRadius: d => (d.speed),
|
||
radiusScale: 1,
|
||
lineWidthMinPixels: 2,
|
||
getFillColor: d => d.guns
|
||
? d.lineStatus == "online"
|
||
? [0xaa, 0x00, 0xff] // Online
|
||
: [0xd5, 0x00, 0xf9] // Soft start or guns otherwise active
|
||
: [0xea, 0x80, 0xfc], // Offline, guns inactive
|
||
getLineColor: [127, 65, 90],
|
||
getColor: [ 255, 0, 0 ],
|
||
getPointRadius: 12,
|
||
radiusUnits: "pixels",
|
||
pointRadiusMinPixels: 4,
|
||
stroked: false,
|
||
filled: true,
|
||
pickable: true,
|
||
...options
|
||
})
|
||
};
|
||
|
||
this.layersAvailable.navl = (options = {}) => {
|
||
return new LineLayer({
|
||
id: 'navl',
|
||
data: `/api/navdata`,
|
||
lineWidthMinPixels: 2,
|
||
getLineColor: (d) => d.properties.ntba ? [240, 248, 255, 200] : [85, 170, 255, 200],
|
||
getSourcePosition: (obj, i) => i.index < i.data?.length ? [i.data[i.index]?.longitude, i.data[i.index]?.latitude] : null,
|
||
getTargetPosition: (obj, i) => i.index < i.data?.length ? [i.data[i.index+1]?.longitude, i.data[i.index+1]?.latitude] : null,
|
||
getLineWidth: 3,
|
||
getPointRadius: 2,
|
||
radiusUnits: "pixels",
|
||
pointRadiusMinPixels: 2,
|
||
pickable: true,
|
||
...options
|
||
})
|
||
};
|
||
|
||
this.layersAvailable.sea = (options = {}) => {
|
||
return new TileLayer({
|
||
id: "sea",
|
||
data: 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png',
|
||
|
||
minZoom: 0,
|
||
maxZoom: 19,
|
||
tileSize: 256,
|
||
|
||
renderSubLayers: props => {
|
||
const {
|
||
bbox: {west, south, east, north}
|
||
} = props.tile;
|
||
|
||
return new BitmapLayer(props, {
|
||
data: null,
|
||
image: props.data,
|
||
bounds: [west, south, east, north]
|
||
});
|
||
},
|
||
...options
|
||
})
|
||
}
|
||
|
||
this.layersAvailable.log = (options = {}) => {
|
||
return new GeoJsonLayer({
|
||
id: 'log',
|
||
data: `/api/project/${this.$route.params.project}/event?mime=application/geo%2Bjson`,
|
||
lineWidthMinPixels: 2,
|
||
getLineColor: [127, 65, 90],
|
||
getPointRadius: 2,
|
||
radiusUnits: "pixels",
|
||
pointRadiusMinPixels: 2,
|
||
pickable: true,
|
||
...options
|
||
})
|
||
};
|
||
|
||
this.layersAvailable.psll = (options = {}) => {
|
||
return new GeoJsonLayer({
|
||
id: 'psll',
|
||
data: `/api/project/${this.$route.params.project}/gis/preplot/line?class=V`,
|
||
lineWidthMinPixels: 1,
|
||
getLineColor: (d) => d.properties.ntba ? [240, 248, 255, 200] : [85, 170, 255, 200],
|
||
getLineWidth: 1,
|
||
getPointRadius: 2,
|
||
radiusUnits: "pixels",
|
||
pointRadiusMinPixels: 2,
|
||
pickable: true,
|
||
...options
|
||
})
|
||
};
|
||
|
||
this.layersAvailable.ppll = (options = {}) => {
|
||
return new GeoJsonLayer({
|
||
id: 'ppll',
|
||
data: `/api/project/${this.$route.params.project}/gis/preplot/line`,
|
||
lineWidthMinPixels: 1,
|
||
getLineColor: (d) => d.properties.ntba ? [240, 248, 255, 200] : [85, 170, 255, 200],
|
||
getLineWidth: 1,
|
||
getPointRadius: 2,
|
||
radiusUnits: "pixels",
|
||
pointRadiusMinPixels: 2,
|
||
pickable: true,
|
||
...options
|
||
})
|
||
};
|
||
|
||
this.layersAvailable.planl = (options = {}) => {
|
||
return new LineLayer({
|
||
id: 'plan',
|
||
data: `/api/project/${this.$route.params.project}/plan`,
|
||
widthMinPixels: 2,
|
||
widthUnits: "pixels",
|
||
getSourcePosition: d => d.geometry.coordinates[0],
|
||
getTargetPosition: d => d.geometry.coordinates[1],
|
||
getLineWidth: 8,
|
||
getColor: (d) => {
|
||
const k = d.azimuth/360*255;
|
||
console.log("COLOUR", k);
|
||
return [ k, 128, k, 200 ];
|
||
},
|
||
pickable: true,
|
||
...options
|
||
})
|
||
};
|
||
|
||
this.layersAvailable.seqrl = (options = {}) => {
|
||
return new GeoJsonLayer({
|
||
id: 'seqrl',
|
||
data: `/api/project/${this.$route.params.project}/gis/raw/line`,
|
||
lineWidthMinPixels: 1,
|
||
getLineColor: (d) => d.properties.ntbp ? [0xe6, 0x51, 0x00, 200] : [0xff, 0x98, 0x00, 200],
|
||
getLineWidth: 1,
|
||
getPointRadius: 2,
|
||
radiusUnits: "pixels",
|
||
pointRadiusMinPixels: 2,
|
||
pickable: true,
|
||
...options
|
||
})
|
||
};
|
||
|
||
this.layersAvailable.seqfl = (options = {}) => {
|
||
return new GeoJsonLayer({
|
||
id: 'seqfl',
|
||
data: `/api/project/${this.$route.params.project}/gis/final/line`,
|
||
lineWidthMinPixels: 1,
|
||
getLineColor: (d) => d.properties.pending ? [0xa7, 0xff, 0xab, 200] : [0x00, 0x96, 0x88, 200],
|
||
getLineWidth: 1,
|
||
getPointRadius: 2,
|
||
radiusUnits: "pixels",
|
||
pointRadiusMinPixels: 2,
|
||
pickable: true,
|
||
...options
|
||
})
|
||
};
|
||
|
||
this.layersAvailable.pplp = (options = {}) => {
|
||
return new SequenceDataLayer({
|
||
id: 'pplp',
|
||
data: this.sequenceDataPreplots,
|
||
pickable: true,
|
||
...options
|
||
});
|
||
};
|
||
|
||
this.layersAvailable.seqrp = (options = {}) => {
|
||
return new SequenceDataLayer({
|
||
id: 'seqrp',
|
||
data: this.sequenceDataRaw,
|
||
pickable: true,
|
||
...options
|
||
});
|
||
};
|
||
|
||
this.layersAvailable.seqfp = (options = {}) => {
|
||
return new SequenceDataLayer({
|
||
id: 'seqfp',
|
||
data: this.sequenceDataFinal,
|
||
pickable: true,
|
||
...options
|
||
});
|
||
};
|
||
|
||
this.layersAvailable.seqrh = (options = {}) => {
|
||
return new HeatmapLayer({
|
||
id: 'seqrh',
|
||
data: this.fullProspectRaw,
|
||
radiusPixels: 15,
|
||
getPosition: d => [ d.longitude, d.latitude ],
|
||
getWeight: d => Math.sqrt(d.Δi**2 + d.Δj**2),
|
||
colorDomain: [2, 20],
|
||
aggregation: "MEAN",
|
||
pickable: false,
|
||
...options
|
||
});
|
||
};
|
||
|
||
this.layersAvailable.seqfh = (options = {}) => {
|
||
return new HeatmapLayer({
|
||
id: 'seqfh',
|
||
data: this.fullProspectFinal,
|
||
radiusPixels: 15,
|
||
getPosition: d => [ d.longitude, d.latitude ],
|
||
getWeight: d => Math.sqrt(d.Δi**2 + d.Δj**2),
|
||
colorDomain: [2, 20],
|
||
aggregation: "MEAN",
|
||
pickable: false,
|
||
...options
|
||
});
|
||
};
|
||
|
||
this.layersAvailable.crosshairs = (options = {}) => {
|
||
return new IconLayer({
|
||
id: 'crosshairs',
|
||
data: [{position: [...this.crosshairsPosition]}],
|
||
getSize: 24,
|
||
getColor: [ 255, 0, 0 ],
|
||
getIcon: (d) => {
|
||
return {
|
||
url: hashMarkerIconURL,
|
||
width: 24,
|
||
height: 24
|
||
};
|
||
},
|
||
...options
|
||
});
|
||
};
|
||
|
||
deck = new Deck({
|
||
parent: document.getElementById("map"),
|
||
mapStyle: 'https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json',
|
||
initialViewState: {
|
||
...this.viewStateDefaults,
|
||
latitude: 61.5,
|
||
longitude: 2.5,
|
||
zoom: 7,
|
||
pitch: 0,
|
||
},
|
||
controller: {inertia: true},
|
||
layers: [],
|
||
getTooltip,
|
||
pickingRadius: 24,
|
||
onWebGLInitialized: this.initLayers
|
||
});
|
||
|
||
|
||
function getTooltip (args) {
|
||
//console.log("tooltip", args?.layer?.constructor?.layerName, args);
|
||
if (args?.layer?.constructor?.tooltip) {
|
||
|
||
return args.layer.constructor.tooltip(args);
|
||
|
||
} else if (args?.layer?.id == "log") {
|
||
|
||
const p = args?.object?.properties;
|
||
if (p) {
|
||
let html = "";
|
||
if (p.sequence && p.point) {
|
||
html += `S${p.sequence.toString().padStart(3, "0")} ${p.point}</br>\n`;
|
||
}
|
||
html += `<small>${p.tstamp}</small><br/>\n`;
|
||
html += `<span>${p.remarks}</span>`;
|
||
if (p.labels?.length) {
|
||
html += `</br>\n<small><i>${p.labels.join(", ")}</i></small>`;
|
||
}
|
||
|
||
const style = { "max-width": "50ex"};
|
||
|
||
return {html, style};
|
||
}
|
||
|
||
} else if (args?.layer?.id == "pplp" || args?.layer?.id == "pslp") {
|
||
|
||
const p = args?.object?.properties;
|
||
const isSailline = args?.layer?.id == "psl";
|
||
if (p) {
|
||
let html = `${isSailline ? "Sail" : "Preplot"} line ${p.line} (${p.incr ? "+" : "-"})`;
|
||
if (p.ntba) {
|
||
html += ` <b title="Not to be acquired">NTBA</a>`;
|
||
}
|
||
if (p.remarks) {
|
||
html += `<br/>\n${p.remarks}`;
|
||
}
|
||
|
||
const style = { "max-width": "50ex"};
|
||
|
||
return {html, style};
|
||
}
|
||
} else if (args?.layer?.id == "plan") {
|
||
|
||
const p = args?.object;
|
||
if (p) {
|
||
const duration = `${(p.duration?.hours??0).toString().padStart(2, "0")}:${(p.duration?.minutes??0).toString().padStart(2, "0")}`;
|
||
const Δt = ((new Date(p.ts1)).valueOf() - (new Date(p.ts0)).valueOf()) / 1000; // seconds
|
||
const speed = (p.length / Δt) * 3.6 / 1.852; // knots
|
||
|
||
let html = `Planned sequence <b>${p.sequence}</b></br>\n` +
|
||
`Line <b>${p.line}</b> – ${p.name}</br>\n` +
|
||
`${p.num_points} Points</br>\n` +
|
||
`${p.length} m ${p.azimuth.toFixed(2)}°</br>\n` +
|
||
`${duration} @ ${speed.toFixed(1)} kt</br>\n` +
|
||
`<b>${p.fsp}</b> @ ${p.ts0.toISOString()}</br>\n` +
|
||
`<b>${p.lsp}</b> @ ${p.ts1.toISOString()}`;
|
||
|
||
if (p.remarks) {
|
||
html += `</br>\n<hr/>${p.remarks}`;
|
||
}
|
||
|
||
const style = { "max-width": "50ex"};
|
||
|
||
return {html, style};
|
||
}
|
||
|
||
} else if (args?.layer?.id == "seqrl" || args?.layer?.id == "seqfl") {
|
||
|
||
let type;
|
||
switch(args.layer.id) {
|
||
case "seqrl":
|
||
type = "Raw";
|
||
break;
|
||
case "seqfl":
|
||
type = "Final";
|
||
break;
|
||
}
|
||
|
||
const p = args?.object?.properties;
|
||
if (p) {
|
||
let html = `Sequence ${p.sequence} (${type})<br/>\n`;
|
||
html += `Line <b>${p.line}</b><br/>\n`;
|
||
html += `${p.num_points} points (${p.missing_shots ? (p.missing_shots + " missing") : "None missing"})<br/>\n`;
|
||
html+= `${(p.length??0).toFixed(0)} m ${(p.azimuth??0).toFixed(1)}°<br/>\n`;
|
||
html += `${p.duration}<br/>\n`;
|
||
html += `<b>${p.fsp}</b> @ ${p.ts0}<br/>\n`;
|
||
html += `<b>${p.lsp}</b> @ ${p.ts1}<br/>\n`;
|
||
if (p.ntbp) {
|
||
html += "<b>Not to be processed</b><br/>\n";
|
||
} else if (p.pending) {
|
||
html += "<b>Pending</b><br/>\n";
|
||
}
|
||
html += "<hr/><br/>\n";
|
||
html += markdown(p.remarks);
|
||
|
||
const style = { "max-width": "50ex"};
|
||
|
||
return {html, style};
|
||
}
|
||
} else if (args?.layer?.id == "navp") {
|
||
const p = args.object;
|
||
|
||
if (p) {
|
||
let html = `${p.vesselName}<br/>\n`
|
||
+ `${p.tstamp}<br/>\n`
|
||
+ `BSP ${(p.speed??0).toFixed(1)} kt CMG ${(p.cmg??0).toFixed(1).padStart(5, "0")}° HDG ${(p.bearing??0).toFixed(1).padStart(5, "0")}° DPT ${(p.waterDepth??0).toFixed(1)} m<br/>\n`
|
||
+ `${p.lineStatus}<br/>\n`;
|
||
|
||
if (p.guns) {
|
||
console.log(p);
|
||
const pressure = p.guns.map( i => i[11] ); // 11 is gun pressure
|
||
const μpress = d3a.mean(pressure);
|
||
const σpress = d3a.deviation(pressure);
|
||
|
||
if (p.lineStatus && p.lineStatus != "offline") {
|
||
html += `${p.lineName}<br/>\n`
|
||
+ `S: ${p._sequence} L: ${p._line} P: ${p.shot}<br/>`
|
||
+ `Source ${p.src_number} `
|
||
+ ((p.trg_mode && p.trg_mode != "external") ? `<b>${p.trg_mode.toUpperCase()} TRIGGER</b> ` : "")
|
||
+ `<small>FSID ${p.fsid}</small> <small>mask ${p.mask}</small><br/>\n`
|
||
+ `Δ ${(p.avg_delta??0).toFixed(3)} ms ±${(p.std_delta??0).toFixed(3)} ms<br/>\n`
|
||
+ `${(μpress??0).toFixed(0)} psi ±${(σpress??0).toFixed(0)} psi / ${(p.volume??0).toFixed(0)} in³<br/>\n`
|
||
+ `along ${(p.inline??0).toFixed(1)} m / across ${(p.crossline??0).toFixed(1)} m<br/>\n`;
|
||
} else {
|
||
// Soft start?
|
||
html +=
|
||
`Source ${p.src_number} `
|
||
+ ((p.trg_mode && p.trg_mode != "external") ? `<b>${p.trg_mode.toUpperCase()} TRIGGER</b> ` : "")
|
||
+ `<small>mask ${p.mask}</small><br/>\n`
|
||
+ `Δ ${(p.avg_delta??0).toFixed(3)} ms ±${(p.std_delta??0).toFixed(3)} ms<br/>\n`
|
||
+ `${(p.manifold??0).toFixed(0)} psi / ${(p.volume??0).toFixed(0)} in³<br/>\n`
|
||
+ `along ${(p.inline??0).toFixed(1)} m / across ${(p.crossline??0).toFixed(1)} m<br/>\n`;
|
||
}
|
||
}
|
||
|
||
const style = { "max-width": "50ex"};
|
||
|
||
return {html, style};
|
||
}
|
||
}
|
||
}
|
||
|
||
//this.layerSelection = [ "seq" ];
|
||
this.getSequenceData();
|
||
|
||
}
|
||
|
||
}
|
||
|
||
</script>
|