mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 06:27:07 +00:00
1810 lines
64 KiB
Vue
1810 lines
64 KiB
Vue
<template>
|
||
<div id="map-container">
|
||
<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>Background</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" disabled/>
|
||
<label for="lyr-nau" title="Scan of Norway's nautical charts – CURRENTLY UNAVAILABLE" disabled>Nautical charts (NO)</label>
|
||
</form>
|
||
|
||
<hr class="my-2"/>
|
||
|
||
<h3>Layers</h3>
|
||
|
||
<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>
|
||
-->
|
||
|
||
<div>
|
||
<v-menu bottom offset-y class="pb-1">
|
||
<template v-slot:activator="{ on, attrs }">
|
||
<v-icon style="margin-right: 3px;" small v-bind="attrs" v-on="on" :title="`Show lines.\nCurrently selected period: ${vesselTrackPeriodSettings[vesselTrackPeriod].title}. Click to change`">mdi-vector-line</v-icon>
|
||
</template>
|
||
<v-list nav dense>
|
||
<v-list-item @click="vesselTrackPeriod = 'hour'">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Last hour</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item @click="vesselTrackPeriod = 'hour6'">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Last 6 hours</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item @click="vesselTrackPeriod = 'hour12'">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Last 12 hours</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item @click="vesselTrackPeriod = 'day'">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Last 24 hours</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item @click="vesselTrackPeriod = 'week'">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Last week</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
</v-list>
|
||
</v-menu>
|
||
<input type="checkbox" value="navl" v-model="layerSelection"/>
|
||
</div>
|
||
|
||
<label><!-- No heatmap available --></label>
|
||
|
||
<span>Sail lines</span>
|
||
<label title="Show points" disabled><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>
|
||
<!-- We do not show individual points for planned lines. There are the preplots for that.
|
||
<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>
|
||
-->
|
||
<span><!-- Necessary for grid alignment purposes --></span>
|
||
<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>
|
||
|
||
<div>
|
||
<v-menu bottom offset-y class="mr-1 pb-1">
|
||
<template v-slot:activator="{ on, attrs }">
|
||
<v-icon small left class="mx-0" v-bind="attrs" v-on="on" :title="`Show deviations.\nCurrently selected mode: ${heatmapTitle}. Click to change`">mdi-dots-grid</v-icon>
|
||
</template>
|
||
<v-list nav dense>
|
||
<v-list-item @click="setHeatmapValue('total_error')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Δ<span style="text-decoration:overline;">ij</span> Total error</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item @click="setHeatmapValue('delta_j')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Δj Inline error</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item @click="setHeatmapValue('delta_i')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Δi Crossline error</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item>
|
||
<v-list-item-content>
|
||
<v-menu bottom offset-y>
|
||
<template v-slot:activator="{ on, attrs }">
|
||
<v-list-item-title v-bind="attrs" v-on="on">
|
||
Raw vs final <v-icon small right>mdi-chevron-right</v-icon>
|
||
</v-list-item-title>
|
||
</template>
|
||
<v-list nav dense>
|
||
<v-list-item @click="setHeatmapValue('co_total_error')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Δ<span style="text-decoration:overline;">ij</span> Total error</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item @click="setHeatmapValue('co_delta_j')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Δj Inline error</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item @click="setHeatmapValue('co_delta_i')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Δi Crossline error</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
</v-list>
|
||
</v-menu>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item>
|
||
<v-list-item-content>
|
||
<v-menu bottom offset-y>
|
||
<template v-slot:activator="{ on, attrs }">
|
||
<v-list-item-title v-bind="attrs" v-on="on">
|
||
Gun data <v-icon small right>mdi-chevron-right</v-icon>
|
||
</v-list-item-title>
|
||
</template>
|
||
<v-list nav dense>
|
||
<v-list-item>
|
||
<v-list-item-content>
|
||
<v-menu bottom offset-y>
|
||
<template v-slot:activator="{ on, attrs }">
|
||
<v-list-item-title v-bind="attrs" v-on="on">
|
||
Pressure <v-icon small right>mdi-chevron-right</v-icon>
|
||
</v-list-item-title>
|
||
</template>
|
||
<v-list nav dense>
|
||
<v-list-item @click="setHeatmapValue('press_μ')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Mean (μ)</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item @click="setHeatmapValue('press_σ')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Standard deviation (σ)</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item @click="setHeatmapValue('press_R')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Range (R)</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
</v-list>
|
||
</v-menu>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item>
|
||
<v-list-item-content>
|
||
<v-menu bottom offset-y>
|
||
<template v-slot:activator="{ on, attrs }">
|
||
<v-list-item-title v-bind="attrs" v-on="on">
|
||
Depths <v-icon small right>mdi-chevron-right</v-icon>
|
||
</v-list-item-title>
|
||
</template>
|
||
<v-list nav dense>
|
||
<v-list-item @click="setHeatmapValue('depth_μ')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Mean (μ)</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item @click="setHeatmapValue('depth_σ')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Standard deviation (σ)</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item @click="setHeatmapValue('depth_R')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Range (R)</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
</v-list>
|
||
</v-menu>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item>
|
||
<v-list-item-content>
|
||
<v-menu bottom offset-y>
|
||
<template v-slot:activator="{ on, attrs }">
|
||
<v-list-item-title v-bind="attrs" v-on="on">
|
||
Deltas <v-icon small right>mdi-chevron-right</v-icon>
|
||
</v-list-item-title>
|
||
</template>
|
||
<v-list nav dense>
|
||
<v-list-item @click="setHeatmapValue('delta_μ')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Mean (μ)</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item @click="setHeatmapValue('delta_σ')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Standard deviation (σ)</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item @click="setHeatmapValue('delta_R')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Range (R)</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
</v-list>
|
||
</v-menu>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item>
|
||
<v-list-item-content>
|
||
<v-menu bottom offset-y>
|
||
<template v-slot:activator="{ on, attrs }">
|
||
<v-list-item-title v-bind="attrs" v-on="on">
|
||
Delay <v-icon small right>mdi-chevron-right</v-icon>
|
||
</v-list-item-title>
|
||
</template>
|
||
<v-list nav dense>
|
||
<v-list-item @click="setHeatmapValue('delay_μ')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Mean (μ)</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item @click="setHeatmapValue('delay_σ')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Standard deviation (σ)</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item @click="setHeatmapValue('delay_R')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Range (R)</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
</v-list>
|
||
</v-menu>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item>
|
||
<v-list-item-content>
|
||
<v-menu bottom offset-y>
|
||
<template v-slot:activator="{ on, attrs }">
|
||
<v-list-item-title v-bind="attrs" v-on="on">
|
||
Fill time <v-icon small right>mdi-chevron-right</v-icon>
|
||
</v-list-item-title>
|
||
</template>
|
||
<v-list nav dense>
|
||
<v-list-item @click="setHeatmapValue('fill_μ')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Mean (μ)</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item @click="setHeatmapValue('fill_σ')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Standard deviation (σ)</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item @click="setHeatmapValue('fill_R')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Range (R)</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
</v-list>
|
||
</v-menu>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item>
|
||
<v-list-item-content>
|
||
<v-menu bottom offset-y>
|
||
<template v-slot:activator="{ on, attrs }">
|
||
<v-list-item-title v-bind="attrs" v-on="on">
|
||
Misfires <v-icon small right>mdi-chevron-right</v-icon>
|
||
</v-list-item-title>
|
||
</template>
|
||
<v-list nav dense>
|
||
<v-list-item @click="setHeatmapValue('no_fire')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>No fire</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item @click="setHeatmapValue('autofire')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>Autofire</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
<v-list-item @click="setHeatmapValue('misfire')">
|
||
<v-list-item-content>
|
||
<v-list-item-title>No fire / autofire</v-list-item-title>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
</v-list>
|
||
</v-menu>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
</v-list>
|
||
</v-menu>
|
||
</v-list-item-content>
|
||
</v-list-item>
|
||
</v-list>
|
||
</v-menu>
|
||
<input type="checkbox" value="seqrh" v-model="layerSelection"/>
|
||
</div>
|
||
|
||
<!--
|
||
<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><!-- No heatmap available --></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>
|
||
|
||
<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"/>
|
||
<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>
|
||
-->
|
||
|
||
<!-- 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 -->
|
||
|
||
<!--
|
||
<hr class="my-2"/>
|
||
|
||
<div title="Not yet implemented">
|
||
<v-btn
|
||
small
|
||
text
|
||
disabled
|
||
>
|
||
<v-icon small left>mdi-cog-outline</v-icon>
|
||
Map settings
|
||
</v-btn>
|
||
</div>
|
||
-->
|
||
</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>
|
||
<v-icon
|
||
class="my-1"
|
||
title="Tilt out"
|
||
@click="tiltOut"
|
||
>mdi-axis-x-rotate-counterclockwise</v-icon>
|
||
</div>
|
||
<div>
|
||
<v-icon
|
||
class="my-1"
|
||
title="Tilt in"
|
||
@click="tiltIn"
|
||
>mdi-axis-x-rotate-clockwise</v-icon>
|
||
</div>
|
||
<div>
|
||
<v-icon v-if="bearing==0"
|
||
class="my-1"
|
||
title="Bin up"
|
||
@click="setBearing('ζ')"
|
||
>mdi-view-grid-outline</v-icon>
|
||
<v-icon v-else
|
||
class="my-1"
|
||
title="North up"
|
||
:style="`transform: rotate(${-bearing}deg);`"
|
||
@click="setBearing(0)"
|
||
>mdi-navigation</v-icon>
|
||
</div>
|
||
<div>
|
||
<v-icon
|
||
class="my-1"
|
||
:title="isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'"
|
||
@click="toggleFullscreen"
|
||
>{{ isFullscreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen' }}</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 MapLayersMixin from './MapLayersMixin';
|
||
import MapTooltipsMixin from './MapTooltipsMixin';
|
||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||
import zoomFitIcon from '@/assets/zoom-fit-best.svg'
|
||
|
||
import { Deck, FlyToInterpolator } from '@deck.gl/core';
|
||
import { IconLayer, TextLayer } from '@deck.gl/layers';
|
||
import { DougalBinaryBundle, DougalBinaryChunkSequential, DougalBinaryChunkInterleaved } from '@dougal/binary';
|
||
|
||
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",
|
||
|
||
mixins: [
|
||
MapLayersMixin,
|
||
MapTooltipsMixin
|
||
],
|
||
|
||
components: {
|
||
|
||
},
|
||
|
||
data () {
|
||
return {
|
||
layerSelection: [],
|
||
layersAvailable: {},
|
||
sequenceDataElements: [],
|
||
sequenceBinaryData: { positions: new Float32Array(0), values: [], udv: 2 },
|
||
sequenceBinaryDataFinal: { positions: new Float32Array(0), values: [], udv: 3 },
|
||
sequenceDataTStamp: null,
|
||
loadingProgress: null,
|
||
//loadingRefreshInterval: 6000, // Refresh sequenceBinaryData* every six seconds while loading
|
||
loadingRefreshInterval: 0,
|
||
viewState: {},
|
||
viewStateDefaults: {
|
||
//maxZoom: 18,
|
||
maxPitch: 89
|
||
},
|
||
bearing: 0,
|
||
|
||
vesselPosition: null,
|
||
vesselTrackLastRefresh: 0,
|
||
vesselTrackRefreshInterval: 12, // seconds
|
||
vesselTrackIntervalID: null,
|
||
vesselTrackPeriod: "hour",
|
||
vesselTrackPeriodSettings: {
|
||
hour: {
|
||
title: "1 hour",
|
||
offset: 3600 * 1000,
|
||
decimation: 1,
|
||
refreshInterval: 18,
|
||
},
|
||
hour6: {
|
||
title: "6 hours",
|
||
offset: 6 * 3600 * 1000,
|
||
decimation: 1,
|
||
refreshInterval: 18,
|
||
},
|
||
hour12: {
|
||
title: "12 hours",
|
||
offset: 12 * 3600 * 1000,
|
||
decimation: 1,
|
||
refreshInterval: 18,
|
||
},
|
||
day: {
|
||
title: "24 hours",
|
||
offset: 24 * 3600 * 1000,
|
||
decimation: 12,
|
||
refreshInterval: 18,
|
||
},
|
||
week: {
|
||
title: "7 days",
|
||
offset: 7 * 24 * 3600 * 1000,
|
||
decimation: 60,
|
||
refreshInterval: 60,
|
||
},
|
||
week2: {
|
||
title: "14 days",
|
||
offset: 14 * 24 * 3600 * 1000,
|
||
decimation: 60,
|
||
refreshInterval: 90,
|
||
},
|
||
month: {
|
||
title: "30 days",
|
||
offset: 30 * 24 * 3600 * 1000,
|
||
decimation: 90,
|
||
refreshInterval: 120,
|
||
},
|
||
quarter: {
|
||
title: "90 days",
|
||
offset: 90 * 24 * 3600 * 1000,
|
||
decimation: 180,
|
||
refreshInterval: 300,
|
||
},
|
||
year: {
|
||
title: "1 year",
|
||
offset: 365 * 24 * 3600 * 1000,
|
||
decimation: 1200,
|
||
refreshInterval: 1800,
|
||
},
|
||
},
|
||
heatmapValue: "total_error",
|
||
isFullscreen: false,
|
||
crosshairsPositions: [],
|
||
crosshairsLabels: [],
|
||
|
||
searchText: "",
|
||
searchVisible: false,
|
||
filterText: "",
|
||
filterVisible: false,
|
||
|
||
error: null,
|
||
};
|
||
},
|
||
|
||
computed: {
|
||
|
||
lineTStamp () {
|
||
return this.$store.state.line.timestamp;
|
||
},
|
||
|
||
sequenceTStamp () {
|
||
return this.$store.state.sequence.timestamp;
|
||
},
|
||
|
||
sequenceData () {
|
||
return this.sequenceDataElements?.map( el => el.data );
|
||
},
|
||
|
||
/*
|
||
sequenceBinaryData() {
|
||
return this.getSequenceBinaryData(2); // Raw + gun data
|
||
},
|
||
|
||
sequenceBinaryDataFinal() {
|
||
return this.getSequenceBinaryData(3); // Final + c-o data
|
||
},
|
||
*/
|
||
|
||
heatmapTitle () {
|
||
|
||
let title = this.heatmapValue;
|
||
|
||
switch (this.heatmapValue) {
|
||
// Raw vs preplot
|
||
case "total_error":
|
||
title = "Total position error (raw data)";
|
||
break;
|
||
case "delta_i":
|
||
title = "Total crossline position error (raw data)";
|
||
break;
|
||
case "delta_j":
|
||
title = "Total inline position error (raw data)";
|
||
break;
|
||
|
||
// Final vs raw (c-o)
|
||
case "co_total_error":
|
||
title = "Total position error (final vs raw)";
|
||
break;
|
||
case "co_delta_i":
|
||
title = "Total crossline position error (final vs raw)";
|
||
break;
|
||
case "co_delta_j":
|
||
title = "Total inline position error (final vs raw)";
|
||
break;
|
||
|
||
// Gun deltas
|
||
case "delta_μ":
|
||
title = "Guns mean delta error";
|
||
break;
|
||
case "delta_σ":
|
||
title = "Guns delta standard deviation (1⋅σ)";
|
||
break;
|
||
case "delta_R":
|
||
title = "Guns delta range";
|
||
break;
|
||
|
||
// Gun pressure
|
||
case "press_μ":
|
||
title = "Fired array guns mean pressure";
|
||
break;
|
||
case "press_σ":
|
||
title = "Fired array pressure standard deviation (1⋅σ)";
|
||
break;
|
||
case "press_R":
|
||
title = "Fired array pressure range";
|
||
break;
|
||
|
||
// Gun depth
|
||
case "depth_μ":
|
||
title = "Guns mean depth";
|
||
break;
|
||
case "depth_σ":
|
||
title = "Gun depth standard deviation (1⋅σ)";
|
||
break;
|
||
case "depth_R":
|
||
title = "Gun depth range";
|
||
break;
|
||
|
||
// Gun fill time
|
||
case "fill_μ":
|
||
title = "Guns mean fill time";
|
||
break;
|
||
case "fill_σ":
|
||
title = "Guns fill time standard deviation (1⋅σ)";
|
||
break;
|
||
case "fill_R":
|
||
title = "Guns fill time range";
|
||
break;
|
||
|
||
// Gun delay
|
||
case "delay_μ":
|
||
title = "Guns mean firing delay";
|
||
break;
|
||
case "delay_σ":
|
||
title = "Guns firing delay standard deviation (1⋅σ)";
|
||
break;
|
||
case "delay_R":
|
||
title = "Guns firing delay range";
|
||
break;
|
||
default:
|
||
}
|
||
|
||
return title;
|
||
},
|
||
|
||
...mapGetters(['user', 'loading', 'serverEvent', 'lineName', 'serverEvent', 'lines', 'plannedSequences', 'sequences', 'labels']),
|
||
...mapState({projectSchema: state => state.project.projectSchema})
|
||
},
|
||
|
||
watch: {
|
||
layerSelection (newVal, oldVal) {
|
||
this.render();
|
||
},
|
||
|
||
viewState: {
|
||
handler () {
|
||
this.setViewState();
|
||
},
|
||
deep: true
|
||
},
|
||
|
||
vesselTrackPeriod () {
|
||
this.updateVesselIntervalTimer();
|
||
},
|
||
|
||
vesselTrackLastRefresh () {
|
||
this.render();
|
||
},
|
||
|
||
lines () {
|
||
// Refresh map on change of preplot data
|
||
this.render();
|
||
},
|
||
|
||
plannedSequences () {
|
||
// Refresh map on change of planned lines
|
||
this.render();
|
||
},
|
||
|
||
sequences () {
|
||
// Refresh map on change of sequence data (raw / final)
|
||
this.sequenceDataTStamp = Date.now();
|
||
this.render();
|
||
},
|
||
|
||
labels () {
|
||
// Refresh the events layer on labels change
|
||
if (this.layerSelection.includes("log")) {
|
||
this.render();
|
||
}
|
||
},
|
||
|
||
sequenceDataElements () {
|
||
//console.log("seq data changed");
|
||
this.sequenceDataTStamp = Date.now();
|
||
//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];
|
||
|
||
if (typeof fn != 'function') {
|
||
throw new Error(`Layer ${name}: expected a function, got ${typeof fn}`);
|
||
}
|
||
|
||
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.getBBox(deck);
|
||
if (bounds) {
|
||
const viewport = deck.getViewports()[0];
|
||
const newViewState = {
|
||
...this.viewStateDefaults,
|
||
...viewport.fitBounds(bounds)
|
||
};
|
||
newViewState.transitionDuration = 500;
|
||
newViewState.transitionInterpolator = new FlyToInterpolator();
|
||
deck.setProps({ initialViewState: newViewState });
|
||
} else {
|
||
console.warn('Unable to calculate bounding box');
|
||
}
|
||
} else {
|
||
console.warn('Deck instance not available');
|
||
}
|
||
},
|
||
|
||
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});
|
||
}
|
||
},
|
||
|
||
tiltIn () {
|
||
if (deck) {
|
||
const viewState = deck.getViewports()[0];
|
||
const initialViewState = {...this.viewStateDefaults, ...viewState};
|
||
initialViewState.pitch -= 10;
|
||
initialViewState.transitionDuration = 300;
|
||
deck.setProps({initialViewState});
|
||
}
|
||
},
|
||
|
||
tiltOut () {
|
||
if (deck) {
|
||
const viewState = deck.getViewports()[0];
|
||
const initialViewState = {...this.viewStateDefaults, ...viewState};
|
||
initialViewState.pitch += 10;
|
||
initialViewState.transitionDuration = 300;
|
||
deck.setProps({initialViewState});
|
||
}
|
||
},
|
||
|
||
setBearing (bearing) {
|
||
if (deck) {
|
||
|
||
if (bearing === 'ζ') {
|
||
bearing = this.$store.getters.projectConfiguration?.binning?.theta ?? 0;
|
||
}
|
||
|
||
const viewState = deck.getViewports()[0];
|
||
const initialViewState = {...this.viewStateDefaults, ...viewState};
|
||
initialViewState.bearing = (bearing + 360) % 360;
|
||
initialViewState.transitionDuration = 300;
|
||
deck.setProps({initialViewState});
|
||
}
|
||
},
|
||
|
||
toggleFullscreen() {
|
||
const mapElement = document.getElementById('map-container');
|
||
if (!this.isFullscreen) {
|
||
if (mapElement.requestFullscreen) {
|
||
mapElement.requestFullscreen();
|
||
} else if (mapElement.webkitRequestFullscreen) { // Safari
|
||
mapElement.webkitRequestFullscreen();
|
||
} else if (mapElement.msRequestFullscreen) { // IE11
|
||
mapElement.msRequestFullscreen();
|
||
}
|
||
this.isFullscreen = true;
|
||
} else {
|
||
if (document.exitFullscreen) {
|
||
document.exitFullscreen();
|
||
} else if (document.webkitExitFullscreen) {
|
||
document.webkitExitFullscreen();
|
||
} else if (document.msExitFullscreen) {
|
||
document.msExitFullscreen();
|
||
}
|
||
this.isFullscreen = false;
|
||
}
|
||
},
|
||
|
||
async refresh () {
|
||
if (window.caches) {
|
||
const cache = await caches.open("dougal");
|
||
for (const key of await cache.keys()) {
|
||
// Match only resolved requests
|
||
if (key instanceof Request) {
|
||
if (key.url?.match(`/project/${this.$route.params.project}`)) {
|
||
console.log("removing", key.method, key.url);
|
||
cache.delete(key);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
await this.$root.sleep(300);
|
||
this.sequenceDataElements = [];
|
||
this.getSequenceData();
|
||
},
|
||
|
||
getBBox(deck, margin = 0.1) {
|
||
if (!deck || !deck.layerManager) {
|
||
console.warn('Invalid deck instance or layer manager');
|
||
return null;
|
||
}
|
||
|
||
let λ0 = +Infinity, φ0 = +Infinity, λ1 = -Infinity, φ1 = -Infinity;
|
||
|
||
const nonTileLayerTypes = ['ScatterplotLayer', 'GeoJsonLayer', 'LineLayer', 'IconLayer', 'DougalSequenceLayer'];
|
||
|
||
deck.layerManager.layers.forEach(layer => {
|
||
// Skip tile-based layers
|
||
if (['TileLayer', 'MVTLayer'].includes(layer.constructor.layerName)) {
|
||
return;
|
||
}
|
||
|
||
// Only process visible layers
|
||
if (!layer.props.visible || !nonTileLayerTypes.includes(layer.constructor.layerName)) {
|
||
return;
|
||
}
|
||
|
||
let data = layer.props.data;
|
||
|
||
// Handle DougalSequenceLayer
|
||
if (layer.constructor.layerName === 'DougalSequenceLayer') {
|
||
const { positions } = data;
|
||
if (positions && positions.length) {
|
||
for (let i = 0; i < positions.length; i += 2) {
|
||
const lon = positions[i];
|
||
const lat = positions[i + 1];
|
||
if (lon != null && lat != null && isFinite(lon) && isFinite(lat)) {
|
||
λ0 = Math.min(λ0, lon);
|
||
φ0 = Math.min(φ0, lat);
|
||
λ1 = Math.max(λ1, lon);
|
||
φ1 = Math.max(φ1, lat);
|
||
}
|
||
}
|
||
}
|
||
} else if (layer.constructor.layerName === 'ScatterplotLayer') {
|
||
// Direct point data (e.g., navp)
|
||
if (Array.isArray(data)) {
|
||
data.forEach(d => {
|
||
const [lon, lat] = layer.props.getPosition(d);
|
||
if (lon != null && lat != null && isFinite(lon) && isFinite(lat)) {
|
||
λ0 = Math.min(λ0, lon);
|
||
φ0 = Math.min(φ0, lat);
|
||
λ1 = Math.max(λ1, lon);
|
||
φ1 = Math.max(φ1, lat);
|
||
}
|
||
});
|
||
} else if (data && data.attributes && data.attributes.getPosition) {
|
||
// Binary data
|
||
const positions = data.attributes.getPosition.value;
|
||
for (let i = 0; i < data.length * 2; i += 2) {
|
||
const lon = positions[i];
|
||
const lat = positions[i + 1];
|
||
if (lon != null && lat != null && isFinite(lon) && isFinite(lat)) {
|
||
λ0 = Math.min(λ0, lon);
|
||
φ0 = Math.min(φ0, lat);
|
||
λ1 = Math.max(λ1, lon);
|
||
φ1 = Math.max(φ1, lat);
|
||
}
|
||
}
|
||
}
|
||
} else if (layer.constructor.layerName === 'GeoJsonLayer') {
|
||
// Extract points from GeoJSON features
|
||
let featureCollections = Array.isArray(data) ? data : [data];
|
||
featureCollections.forEach(item => {
|
||
let features = item;
|
||
if (item?.type === 'FeatureCollection') {
|
||
features = item.features || [];
|
||
}
|
||
if (Array.isArray(features)) {
|
||
features.forEach(feature => {
|
||
if (feature.geometry?.type === 'Point') {
|
||
const [lon, lat] = feature.geometry.coordinates;
|
||
if (lon != null && lat != null && isFinite(lon) && isFinite(lat)) {
|
||
λ0 = Math.min(λ0, lon);
|
||
φ0 = Math.min(φ0, lat);
|
||
λ1 = Math.max(λ1, lon);
|
||
φ1 = Math.max(φ1, lat);
|
||
}
|
||
} else if (feature.geometry?.type === 'LineString') {
|
||
feature.geometry.coordinates.forEach(([lon, lat]) => {
|
||
if (lon != null && lat != null && isFinite(lon) && isFinite(lat)) {
|
||
λ0 = Math.min(λ0, lon);
|
||
φ0 = Math.min(φ0, lat);
|
||
λ1 = Math.max(λ1, lon);
|
||
φ1 = Math.max(φ1, lat);
|
||
}
|
||
});
|
||
} else if (feature.geometry?.type === 'Polygon') {
|
||
feature.geometry.coordinates[0].forEach(([lon, lat]) => {
|
||
if (lon != null && lat != null && isFinite(lon) && isFinite(lat)) {
|
||
λ0 = Math.min(λ0, lon);
|
||
φ0 = Math.min(φ0, lat);
|
||
λ1 = Math.max(λ1, lon);
|
||
φ1 = Math.max(φ1, lat);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
});
|
||
} else if (layer.constructor.layerName === 'LineLayer') {
|
||
// Extract source and target positions
|
||
if (Array.isArray(data)) {
|
||
data.forEach(d => {
|
||
const source = layer.props.getSourcePosition(d);
|
||
const target = layer.props.getTargetPosition(d);
|
||
if (source && source[0] != null && source[1] != null && isFinite(source[0]) && isFinite(source[1])) {
|
||
λ0 = Math.min(λ0, source[0]);
|
||
φ0 = Math.min(φ0, source[1]);
|
||
λ1 = Math.max(λ1, source[0]);
|
||
φ1 = Math.max(φ1, source[1]);
|
||
}
|
||
if (target && target[0] != null && target[1] != null && isFinite(target[0]) && isFinite(target[1])) {
|
||
λ0 = Math.min(λ0, target[0]);
|
||
φ0 = Math.min(φ0, target[1]);
|
||
λ1 = Math.max(λ1, target[0]);
|
||
φ1 = Math.max(φ1, target[1]);
|
||
}
|
||
});
|
||
}
|
||
} else if (layer.constructor.layerName === 'IconLayer') {
|
||
// Single point (e.g., crosshairs)
|
||
if (Array.isArray(data)) {
|
||
data.forEach(d => {
|
||
const [lon, lat] = layer.props.getPosition(d);
|
||
if (lon != null && lat != null && isFinite(lon) && isFinite(lat)) {
|
||
λ0 = Math.min(λ0, lon);
|
||
φ0 = Math.min(φ0, lat);
|
||
λ1 = Math.max(λ1, lon);
|
||
φ1 = Math.max(φ1, lat);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
// Check if any valid points were found
|
||
if (!isFinite(λ0) || !isFinite(φ0) || !isFinite(λ1) || !isFinite(φ1)) {
|
||
console.warn('No valid coordinates found for non-tile layers');
|
||
return null;
|
||
}
|
||
|
||
// Apply margin
|
||
const δλ = λ1 - λ0;
|
||
const δφ = φ1 - φ0;
|
||
const mλ = δλ * margin;
|
||
const mφ = δφ * margin;
|
||
|
||
return [[λ0 - mλ, φ0 - mφ], [λ1 + mλ, φ1 + mφ]];
|
||
},
|
||
|
||
// Returns the current second, as an integer.
|
||
// Used for triggering Deck.gl URL refreshes
|
||
currentSecond () {
|
||
return Math.floor(Date.now()/1000);
|
||
},
|
||
|
||
async getSequenceData (sequenceNumbers, types = [2, 3]) {
|
||
|
||
//const types = [2, 3]; // Bundle types: 2 → raw/gun data; 3 ‒ final data. See bundles.js
|
||
|
||
if (!sequenceNumbers) {
|
||
const sequenceList = await this.api([`/project/${this.$route.params.project}/sequence`]);
|
||
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 = {
|
||
headers: {
|
||
//Accept: "application/vnd.aaltronav.dougal+octet-stream; format=1c"
|
||
Accept: "application/vnd.aaltronav.dougal+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.
|
||
self.api([url, init, cb, {cache: true, format: "arrayBuffer"}]);
|
||
});
|
||
|
||
}
|
||
|
||
for (const sequence of sequenceNumbers) {
|
||
for (const type of types) {
|
||
try {
|
||
const url = `/project/${self.$route.params.project}/sequence/${sequence}?type=${type}`;
|
||
|
||
const sequenceData = await download(url);
|
||
if (sequenceData) {
|
||
yield {
|
||
id: `seq-${sequence}-${type}`,
|
||
sequence: sequence,
|
||
type: type,
|
||
data: sequenceData.data
|
||
};
|
||
}
|
||
} catch (err) {
|
||
console.error(`Error downloading sequence ${sequence}`);
|
||
console.error(err);
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
const transferData = () => {
|
||
this.sequenceBinaryData = this.getSequenceBinaryData(2);
|
||
this.sequenceBinaryDataFinal = this.getSequenceBinaryData(3);
|
||
};
|
||
|
||
for await (const value of iterator()) {
|
||
this.loadingProgress = ++count/sequenceCount*100;
|
||
//console.log("PRG", count, sequenceCount, this.loadingProgress, );
|
||
this.sequenceDataElements.push(Object.freeze(value));
|
||
if (this.loadingRefreshInterval > 0) {
|
||
setTimeout( () => {
|
||
transferData();
|
||
this.render();
|
||
}, this.loadingRefreshInterval );
|
||
}
|
||
}
|
||
|
||
transferData();
|
||
|
||
this.loadingProgress = null;
|
||
console.log("passed for await", deck);
|
||
},
|
||
|
||
getSequenceBinaryData (type) {
|
||
const sequences = this.sequenceDataElements?.filter( el => el.type == type)?.sort((a, b) => a.sequence - b.sequence) || [];
|
||
if (!sequences.length) {
|
||
console.warn('No sequence data available');
|
||
return { positions: new Float32Array(0), values: [], udv: type };
|
||
}
|
||
|
||
// Validate first sequence to get array sizes
|
||
let firstBundle;
|
||
try {
|
||
firstBundle = DougalBinaryBundle.clone(sequences[0].data);
|
||
if (!firstBundle.chunks || typeof firstBundle.chunks !== 'function') {
|
||
throw new Error('Invalid DougalBinaryBundle: chunks method missing');
|
||
}
|
||
if (!firstBundle.chunks().length) {
|
||
throw new Error('Invalid DougalBinaryBundle: bundle has no chunks');
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to process first sequence:', e);
|
||
return { positions: new Float32Array(0), values: [], udv: type };
|
||
}
|
||
|
||
const totalCount = sequences.reduce((acc, { data }) => {
|
||
try {
|
||
const bundle = DougalBinaryBundle.clone(data);
|
||
return acc + bundle.chunks().reduce((sum, chunk) => sum + chunk.jCount, 0);
|
||
} catch (e) {
|
||
console.warn('Skipping invalid sequence data:', e);
|
||
return acc;
|
||
}
|
||
}, 0);
|
||
|
||
if (totalCount === 0) {
|
||
console.warn('No valid points found in sequences');
|
||
return { positions: new Float32Array(0), values: [], udv: type };
|
||
}
|
||
|
||
const ΔelemCount = firstBundle.chunks()[0].ΔelemCount;
|
||
const elemCount = firstBundle.chunks()[0].elemCount;
|
||
const positions = new Float32Array(totalCount * 2);
|
||
const values = new Array(ΔelemCount + elemCount + 2);
|
||
for (let k = 0; k < values.length; k++) {
|
||
values[k] = new (k === 0 ? Uint16Array : k === 1 ? Uint32Array : k === 2 ? BigUint64Array : Float32Array)(totalCount);
|
||
}
|
||
|
||
let offset = 0;
|
||
let udv = 2;
|
||
sequences.forEach(({ data, sequence }) => {
|
||
try {
|
||
const bundle = DougalBinaryBundle.clone(data);
|
||
const chunks = bundle.chunks();
|
||
if (!chunks.length) {
|
||
console.warn(`No chunks in sequence ${sequence}`);
|
||
return;
|
||
}
|
||
udv = chunks[0].udv;
|
||
|
||
if (udv != type) {
|
||
console.warn(`Found bundle with udv = ${udv} but I'm only expecting udv = ${type}`);
|
||
}
|
||
|
||
let chunkOffset = offset;
|
||
for (const chunk of chunks) {
|
||
const λarray = chunk.elem(0);
|
||
const φarray = chunk.elem(1);
|
||
for (let i = 0; i < λarray.length; i++) {
|
||
positions[chunkOffset * 2 + i * 2] = λarray[i];
|
||
positions[chunkOffset * 2 + i * 2 + 1] = φarray[i];
|
||
}
|
||
|
||
values[0].set(new Uint16Array(chunk.jCount).fill(chunk.i), chunkOffset);
|
||
values[1].set(Uint32Array.from({ length: chunk.jCount }, (_, i) => chunk.j0 + i * chunk.Δj), chunkOffset);
|
||
|
||
for (let j = 0; j < chunk.ΔelemCount; j++) {
|
||
values[2 + j].set(chunk.Δelem(j), chunkOffset);
|
||
}
|
||
for (let j = 2; j < chunk.elemCount; j++) {
|
||
values[2 + chunk.ΔelemCount + j - 2].set(chunk.elem(j), chunkOffset);
|
||
}
|
||
|
||
chunkOffset += chunk.jCount;
|
||
}
|
||
|
||
offset += chunkOffset - offset;
|
||
} catch (e) {
|
||
console.warn(`Error processing sequence ${sequence}:`, e);
|
||
}
|
||
});
|
||
|
||
if (offset !== totalCount) {
|
||
console.warn(`Offset mismatch: ${offset} ≠ ${totalCount}`);
|
||
}
|
||
|
||
console.log(`Concatenated ${totalCount} points, ${values.length} value arrays`);
|
||
return { positions, values, udv };
|
||
},
|
||
|
||
async initLayers (gl) {
|
||
//console.log("SHOULD BE INITIALISING LAYERS HERE", gl);
|
||
this.decodeURL();
|
||
this.decodeURLHash();
|
||
//deck.onViewStateChange = this.viewStateUpdated;
|
||
},
|
||
|
||
setViewState () {
|
||
const viewport = deck.getViewports()[0];
|
||
if (deck && viewport && this.viewState) {
|
||
const initialViewState = {
|
||
...this.initialViewState,
|
||
...viewport,
|
||
...this.viewState
|
||
};
|
||
deck.setProps({initialViewState});
|
||
}
|
||
},
|
||
|
||
viewStateUpdated ({viewState}) {
|
||
this.bearing = viewState.bearing;
|
||
this.updateURL({viewState});
|
||
},
|
||
|
||
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, labels ] = hash.split(":");
|
||
|
||
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);
|
||
|
||
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");
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
if (layers) {
|
||
const addLayers = [];
|
||
const remLayers = [];
|
||
const invLayers = [];
|
||
const setLayers = [];
|
||
|
||
layers
|
||
.split(";")
|
||
.map( l => l.match(/([!+-])?(.+)/).slice(1) )
|
||
.forEach( ([ action, name ]) => {
|
||
if (this.layersAvailable.hasOwnProperty(name)) {
|
||
const groupKeys = {"+": addLayers, "-": remLayers, "!": invLayers};
|
||
const group = groupKeys[action] ?? setLayers;
|
||
group.push(name);
|
||
}
|
||
});
|
||
|
||
if (!addLayers.length && !remLayers.length && !invLayers.length && setLayers.length) {
|
||
// Set layers as per setLayers
|
||
this.layerSelection = [...setLayers];
|
||
} else {
|
||
// Otherwise, ignore setLayers and use the current layerset
|
||
const layerset = new Set(this.layerSelection);
|
||
addLayers.forEach( layer => layerset.add(layer) );
|
||
remLayers.forEach( layer => layerset.delete(layer) );
|
||
invLayers.forEach( layer =>
|
||
layerset.has(layer) ? layerset.delete(layer) : layerset.add(layer) );
|
||
this.layerSelection = [...layerset];
|
||
}
|
||
}
|
||
|
||
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);
|
||
|
||
}
|
||
}
|
||
},
|
||
|
||
setHeatmapValue (v) {
|
||
this.heatmapValue = v;
|
||
console.log("Switched heatmap to", v);
|
||
this.render();
|
||
},
|
||
|
||
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;
|
||
},
|
||
|
||
updateVesselIntervalTimer (refreshInterval) {
|
||
this.vesselTrackRefreshInterval = refreshInterval ??
|
||
this.vesselTrackPeriodSettings[this.vesselTrackPeriod]?.refreshInterval ?? 0;
|
||
|
||
this.vesselTrackIntervalID = clearInterval(this.vesselTrackIntervalID);
|
||
if (this.vesselTrackRefreshInterval) {
|
||
this.vesselTrackLastRefresh = this.currentSecond();
|
||
this.vesselTrackIntervalID = setInterval( () => {
|
||
this.vesselTrackLastRefresh = this.currentSecond();
|
||
}, this.vesselTrackRefreshInterval * 1000);
|
||
}
|
||
},
|
||
|
||
async handleSequences (context, {payload}) {
|
||
if (payload.pid != this.$route.params.project) {
|
||
console.warn(`${this.$route.params.project} ignoring notification for ${payload.pid}`);
|
||
return;
|
||
}
|
||
|
||
console.log("handleSequences (Map)", payload);
|
||
|
||
if (payload.old?.sequence) {
|
||
console.log(`Remove sequence ${payload.old.sequence} from data`);
|
||
this.sequenceDataElements = this.sequenceDataElements.filter( el => el.sequence != payload.old.sequence );
|
||
if (window.caches) {
|
||
const cache = await caches.open("dougal");
|
||
const rx = new RegExp(`/project/${this.$route.params.project}/sequence/${payload.old.sequence}(\\?.*)?$`);
|
||
const keys = await cache.keys();
|
||
for (const req of keys) {
|
||
if (rx.test(req.url)) {
|
||
console.log(`Removing ${req.url} from cache`);
|
||
cache.delete(req);
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
if (payload.new?.sequence) {
|
||
console.log(`Add sequence ${payload.new.sequence} to data`);
|
||
this.getSequenceData([payload.new.sequence]);
|
||
}
|
||
},
|
||
|
||
handleVesselPosition (context, {payload}) {
|
||
if (payload.new?.geometry?.coordinates) {
|
||
const now = Date.now();
|
||
const lastRefresh = this.vesselPosition?._lastRefresh;
|
||
|
||
// Limits refreshes to once every five seconds max
|
||
if (lastRefresh && (now-lastRefresh) < 5000) return;
|
||
|
||
this.vesselPosition = {
|
||
...payload.new.meta,
|
||
tstamp: payload.new.tstamp,
|
||
_lastRefresh: now
|
||
};
|
||
if (this.vesselPosition.lineStatus == "offline") {
|
||
this.vesselPosition.x = this.vesselPosition.longitude ?? payload.new.geometry.coordinates[0];
|
||
this.vesselPosition.y = this.vesselPosition.latitude ?? payload.new.geometry.coordinates[1];
|
||
} else {
|
||
this.vesselPosition.x = this.vesselPosition.longitudeMaster
|
||
?? payload.new.geometry.coordinates[0];
|
||
this.vesselPosition.y = this.vesselPosition.latitudeMaster
|
||
?? payload.new.geometry.coordinates[1];
|
||
}
|
||
this.render();
|
||
}
|
||
},
|
||
|
||
registerNotificationHandlers (action = "registerHandler") {
|
||
|
||
["raw_lines", "raw_shots", "final_lines", "final_shots"].forEach( table => {
|
||
this.$store.dispatch(action, {
|
||
table,
|
||
handler: this.handleSequences
|
||
})
|
||
});
|
||
|
||
this.$store.dispatch(action, {
|
||
table: 'realtime',
|
||
handler: this.handleVesselPosition
|
||
});
|
||
|
||
},
|
||
|
||
unregisterNotificationHandlers () {
|
||
this.registerNotificationHandlers("unregisterHandler");
|
||
},
|
||
|
||
...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");
|
||
}
|
||
|
||
this.updateVesselIntervalTimer();
|
||
|
||
this.layersAvailable.osm = this.osmLayer;
|
||
|
||
this.layersAvailable.sea = this.openSeaMapLayer;
|
||
|
||
this.layersAvailable.navp = this.vesselTrackPointsLayer;
|
||
|
||
this.layersAvailable.navl = this.vesselTrackLinesLayer;
|
||
|
||
this.layersAvailable.log = this.eventsLogLayer;
|
||
|
||
this.layersAvailable.psll = this.preplotSaillinesLinesLayer;
|
||
|
||
this.layersAvailable.ppll = this.preplotLinesLayer;
|
||
|
||
this.layersAvailable.planl = this.plannedLinesLinesLayer;
|
||
|
||
this.layersAvailable.seqrl = this.rawSequencesLinesLayer;
|
||
|
||
this.layersAvailable.seqfl = this.finalSequencesLinesLayer;
|
||
|
||
this.layersAvailable.pslp = this.preplotSaillinesPointLayer;
|
||
|
||
this.layersAvailable.pplp = this.preplotPointsLayer;
|
||
|
||
this.layersAvailable.seqrp = this.rawSequencesPointsLayer;
|
||
|
||
this.layersAvailable.seqfp = this.finalSequencesPointsLayer;
|
||
|
||
this.layersAvailable.seqrh = this.heatmapLayer;
|
||
this.layersAvailable.crosshairs = (options = {}) => {
|
||
return new IconLayer({
|
||
id: 'crosshairs',
|
||
data: this.crosshairsPositions,
|
||
getPosition: d => d,
|
||
getSize: 32,
|
||
getColor: [ 255, 0, 0 ],
|
||
getIcon: (d) => {
|
||
return {
|
||
url: hashMarkerIconURL,
|
||
width: 24,
|
||
height: 24
|
||
};
|
||
},
|
||
...options
|
||
});
|
||
};
|
||
|
||
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',
|
||
initialViewState: {
|
||
...this.viewStateDefaults,
|
||
latitude: 61.5,
|
||
longitude: 2.5,
|
||
zoom: 7,
|
||
pitch: 0,
|
||
},
|
||
controller: {inertia: true},
|
||
layers: [],
|
||
getTooltip: this.getTooltip,
|
||
pickingRadius: 24,
|
||
onWebGLInitialized: this.initLayers,
|
||
onViewStateChange: this.viewStateUpdated,
|
||
});
|
||
|
||
// Get fullscreen state
|
||
|
||
document.addEventListener('fullscreenchange', () => {
|
||
this.isFullscreen = !!document.fullscreenElement;
|
||
});
|
||
document.addEventListener('webkitfullscreenchange', () => {
|
||
this.isFullscreen = !!document.fullscreenElement;
|
||
});
|
||
document.addEventListener('msfullscreenchange', () => {
|
||
this.isFullscreen = !!document.fullscreenElement;
|
||
});
|
||
|
||
this.registerNotificationHandlers();
|
||
|
||
//this.layerSelection = [ "seq" ];
|
||
this.getSequenceData();
|
||
},
|
||
|
||
beforeDestroy () {
|
||
this.unregisterNotificationHandlers();
|
||
this.vesselTrackIntervalID = this.clearInterval(this.vesselTrackIntervalID);
|
||
}
|
||
|
||
}
|
||
|
||
</script>
|