Compare commits

..

42 Commits

Author SHA1 Message Date
D. Berge
bc59885a02 Merge branch '334-add-4d-comparisons' into 'devel'
Draft: Resolve "Add 4D comparisons"

Closes #334

See merge request wgp/dougal/software!60
2025-08-21 15:26:50 +00:00
D. Berge
b1344bebd8 Update the required schema version.
This is necessary for the comparisons code to work.
2025-08-21 17:08:23 +02:00
D. Berge
3e91ccba8d Don't show monitor lines by default 2025-08-21 15:21:01 +02:00
D. Berge
fa0be9c0b7 Make loading indicator spin when 0% 2025-08-21 15:20:31 +02:00
D. Berge
dcbf5496f6 Remove unneded dependency 2025-08-21 15:10:45 +02:00
D. Berge
8007f46e37 Fix typo 2025-08-21 15:04:48 +02:00
D. Berge
4a7683cfd0 Add group map view 2025-08-21 14:58:53 +02:00
D. Berge
565a9d7e01 Add support for type 4 decoding 2025-08-21 14:58:53 +02:00
D. Berge
b07244c823 Fix component paths 2025-08-21 14:58:53 +02:00
D. Berge
c909edc41f Move components to subdirectory 2025-08-21 14:55:27 +02:00
D. Berge
41ef511123 Return type 4 sequence data 2025-08-21 14:52:50 +02:00
D. Berge
4196e9760b Add encoding type 4 to bundle 2025-08-21 14:51:49 +02:00
D. Berge
6b6f5ab511 Link from group summary to individual projects 2025-08-20 12:06:20 +02:00
D. Berge
7d8c78648d Don't request summaries in ProjectList.
Those will be populated directly by Vuex.
2025-08-20 12:05:44 +02:00
D. Berge
faf7e9c98f Try to improve responsiveness when refreshing project list 2025-08-20 12:05:05 +02:00
D. Berge
abf2709705 Expand groups router definition 2025-08-20 12:04:26 +02:00
D. Berge
f5dfafd85a Make event handler more specific 2025-08-20 12:03:53 +02:00
D. Berge
cf8b0937d9 Rework comparison components.
More focused on error ellipses.
2025-08-19 19:28:19 +02:00
D. Berge
d737f5d676 Refresh comparisons when notified of changes 2025-08-19 19:27:38 +02:00
D. Berge
5fe19da586 Add control to reset comparisons view 2025-08-19 19:27:03 +02:00
D. Berge
0af0cf4b42 Add overlays when loading / data error 2025-08-19 18:58:04 +02:00
D. Berge
ccb8205d26 Don't cache comparisons in the API 2025-08-19 18:55:31 +02:00
D. Berge
9b3fffdcfc Don't save comparison samples 2025-08-19 18:54:28 +02:00
D. Berge
dea1e9ee0d Add comparisons channel to notifications 2025-08-19 18:53:40 +02:00
D. Berge
d45ec767ec Add database upgrade file 43 2025-08-19 17:56:30 +02:00
D. Berge
67520ffc48 Add database upgrade file 42 2025-08-19 17:56:14 +02:00
D. Berge
22a296ba26 Add database upgrade file 41 2025-08-19 17:55:58 +02:00
D. Berge
f89435d80f Don't overwrite existing comparisons unless forced.
opts.overwrite = true will cause existing comparisons to be
recomputed.
2025-08-19 17:20:57 +02:00
D. Berge
a3f1dd490c Fix non-existent method 2025-08-19 17:20:03 +02:00
D. Berge
2fcfcb4f84 Add link to group comparison from project list 2025-08-18 16:39:20 +02:00
D. Berge
b60db7e7ef Add frontend route for 4D comparisons 2025-08-18 14:17:17 +02:00
D. Berge
4bb087fff7 Add 4D comparisons list Vue component 2025-08-18 14:16:23 +02:00
D. Berge
15af5effc3 Add 4D comparisons Vue component 2025-08-18 14:15:52 +02:00
D. Berge
b5c6d04e62 Add utilities for transforming duration objects 2025-08-18 14:15:14 +02:00
D. Berge
571c5a8bca Add Vue components for 4D comparisons 2025-08-18 14:14:34 +02:00
D. Berge
c45982829c Add set operations utilities 2025-08-18 14:11:56 +02:00
D. Berge
f3958b37b7 Add comparison API endpoints 2025-08-18 14:11:20 +02:00
D. Berge
58374adc68 Add two new bundle types.
Of which 0xa is not actually used and 0xc is used for geometric
comparison data ([ line, point, δi, δj ]).
2025-08-18 14:05:26 +02:00
D. Berge
32aea8a5ed Add comparison functions to server/lib 2025-08-18 13:53:43 +02:00
D. Berge
023b65285f Fix bug trying to get project info for undefined 2025-08-18 13:51:37 +02:00
D. Berge
a320962669 Add project group info to Vuex 2025-08-18 13:50:49 +02:00
D. Berge
0c0067b8d9 Add iterators 2025-08-18 13:48:49 +02:00
13 changed files with 271 additions and 662 deletions

View File

@@ -1,89 +0,0 @@
#!/usr/bin/node
const cmp = require('../lib/www/server/lib/comparisons');
async function purgeComparisons () {
const groups = await cmp.groups();
const comparisons = await cmp.getGroup();
const pids = new Set(Object.values(groups).flat().map( p => p.pid ));
const comparison_pids = new Set(comparisons.map( c => [ c.baseline_pid, c.monitor_pid ] ).flat());
for (const pid of comparison_pids) {
if (!pids.has(pid)) {
console.log(`${pid} no longer par of a group. Deleting comparisons`);
staleComps = comparisons.filter( c => c.baseline_pid == pid || c.monitor_pid == pid );
for (c of staleComps) {
console.log(`Deleting comparison ${c.baseline_pid}${c.monitor_pid}`);
await cmp.remove(c.baseline_pid, c.monitor_pid);
}
}
}
}
async function main () {
console.log("Looking for unreferenced comparisons to purge");
await purgeComparisons();
console.log("Retrieving project groups");
const groups = await cmp.groups();
if (!Object.keys(groups??{})?.length) {
console.log("No groups found");
return 0;
}
console.log(`Found ${Object.keys(groups)?.length} groups: ${Object.keys(groups).join(", ")}`);
for (const groupName of Object.keys(groups)) {
const projects = groups[groupName];
console.log(`Fetching saved comparisons for ${groupName}`);
const comparisons = await cmp.getGroup(groupName);
if (!comparisons || !comparisons.length) {
console.log(`No comparisons found for ${groupName}`);
continue;
}
// Check if there are any projects that have been modified since last comparison
// or if there are any pairs that are no longer part of the group
const outdated = comparisons.filter( c => {
const baseline_tstamp = projects.find( p => p.pid === c.baseline_pid )?.tstamp;
const monitor_tstamp = projects.find( p => p.pid === c.monitor_pid )?.tstamp;
return (c.tstamp < baseline_tstamp) || (c.tstamp < monitor_tstamp) ||
baseline_tstamp == null || monitor_tstamp == null;
});
for (const comparison of outdated) {
console.log(`Removing stale comparison: ${comparison.baseline_pid}${comparison.monitor_pid}`);
await cmp.remove(comparison.baseline_pid, comparison.monitor_pid);
}
if (projects?.length < 2) {
console.log(`Group ${groupName} has less than two projects. No comparisons are possible`);
continue;
}
// Re-run the comparisons that are not in the database. They may
// be missing either beacause they were not there to start with
// or because we just removed them due to being stale
console.log(`Recalculating group ${groupName}`);
await cmp.saveGroup(groupName);
}
console.log("Comparisons update done");
return 0;
}
if (require.main === module) {
main();
} else {
module.exports = main;
}

View File

@@ -1,157 +0,0 @@
-- Add procedure to decimate old nav data
--
-- New schema version: 0.6.6
--
-- ATTENTION:
--
-- ENSURE YOU HAVE BACKED UP THE DATABASE BEFORE RUNNING THIS SCRIPT.
--
--
-- NOTE: This upgrade affects the public schema only.
-- NOTE: Each application starts a transaction, which must be committed
-- or rolled back.
--
-- This adds a last_project_update(pid) function. It takes a project ID
-- and returns the last known timestamp from that project. Timestamps
-- are derived from multiple sources:
--
-- - raw_shots table
-- - final_shots table
-- - events_log_full table
-- - info table where key = 'qc'
-- - files table, from the hashes (which contain the file's mtime)
-- - project configuration, looking for an _updatedOn property
--
-- To apply, run as the dougal user:
--
-- psql <<EOF
-- \i $THIS_FILE
-- COMMIT;
-- EOF
--
-- NOTE: It can be applied multiple times without ill effect.
--
BEGIN;
CREATE OR REPLACE PROCEDURE pg_temp.show_notice (notice text) AS $$
BEGIN
RAISE NOTICE '%', notice;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE PROCEDURE pg_temp.upgrade_database () AS $outer$
BEGIN
RAISE NOTICE 'Updating schema %', 'public';
SET search_path TO public;
-- BEGIN
CREATE OR REPLACE FUNCTION public.last_project_update(p_pid text)
RETURNS timestamp with time zone
LANGUAGE plpgsql
AS $function$
DECLARE
v_last_ts timestamptz := NULL;
v_current_ts timestamptz;
v_current_str text;
v_current_unix numeric;
v_sid_rec record;
BEGIN
-- From raw_shots, final_shots, info, and files
FOR v_sid_rec IN SELECT schema FROM public.projects WHERE pid = p_pid
LOOP
-- From raw_shots
EXECUTE 'SELECT max(tstamp) FROM ' || v_sid_rec.schema || '.raw_shots' INTO v_current_ts;
IF v_current_ts > v_last_ts OR v_last_ts IS NULL THEN
v_last_ts := v_current_ts;
END IF;
-- From final_shots
EXECUTE 'SELECT max(tstamp) FROM ' || v_sid_rec.schema || '.final_shots' INTO v_current_ts;
IF v_current_ts > v_last_ts OR v_last_ts IS NULL THEN
v_last_ts := v_current_ts;
END IF;
-- From info where key = 'qc'
EXECUTE 'SELECT value->>''updatedOn'' FROM ' || v_sid_rec.schema || '.info WHERE key = ''qc''' INTO v_current_str;
IF v_current_str IS NOT NULL THEN
v_current_ts := v_current_str::timestamptz;
IF v_current_ts > v_last_ts OR v_last_ts IS NULL THEN
v_last_ts := v_current_ts;
END IF;
END IF;
-- From files hash second part, only for valid colon-separated hashes
EXECUTE 'SELECT max( split_part(hash, '':'', 2)::numeric ) FROM ' || v_sid_rec.schema || '.files WHERE hash ~ ''^[0-9]+:[0-9]+\\.[0-9]+:[0-9]+\\.[0-9]+:[0-9a-f]+$''' INTO v_current_unix;
IF v_current_unix IS NOT NULL THEN
v_current_ts := to_timestamp(v_current_unix);
IF v_current_ts > v_last_ts OR v_last_ts IS NULL THEN
v_last_ts := v_current_ts;
END IF;
END IF;
-- From event_log_full
EXECUTE 'SELECT max(tstamp) FROM ' || v_sid_rec.schema || '.event_log_full' INTO v_current_ts;
IF v_current_ts > v_last_ts OR v_last_ts IS NULL THEN
v_last_ts := v_current_ts;
END IF;
END LOOP;
-- From projects.meta->_updatedOn
SELECT (meta->>'_updatedOn')::timestamptz FROM public.projects WHERE pid = p_pid INTO v_current_ts;
IF v_current_ts > v_last_ts OR v_last_ts IS NULL THEN
v_last_ts := v_current_ts;
END IF;
RETURN v_last_ts;
END;
$function$;
-- END
END;
$outer$ LANGUAGE plpgsql;
CREATE OR REPLACE PROCEDURE pg_temp.upgrade () AS $outer$
DECLARE
row RECORD;
current_db_version TEXT;
BEGIN
SELECT value->>'db_schema' INTO current_db_version FROM public.info WHERE key = 'version';
IF current_db_version >= '0.6.6' THEN
RAISE EXCEPTION
USING MESSAGE='Patch already applied';
END IF;
IF current_db_version != '0.6.5' THEN
RAISE EXCEPTION
USING MESSAGE='Invalid database version: ' || current_db_version,
HINT='Ensure all previous patches have been applied.';
END IF;
CALL pg_temp.upgrade_database();
END;
$outer$ LANGUAGE plpgsql;
CALL pg_temp.upgrade();
CALL pg_temp.show_notice('Cleaning up');
DROP PROCEDURE pg_temp.upgrade_database ();
DROP PROCEDURE pg_temp.upgrade ();
CALL pg_temp.show_notice('Updating db_schema version');
INSERT INTO public.info VALUES ('version', '{"db_schema": "0.6.6"}')
ON CONFLICT (key) DO UPDATE
SET value = public.info.value || '{"db_schema": "0.6.6"}' WHERE public.info.key = 'version';
CALL pg_temp.show_notice('All done. You may now run "COMMIT;" to persist the changes');
DROP PROCEDURE pg_temp.show_notice (notice text);
--
--NOTE Run `COMMIT;` now if all went well
--

View File

@@ -39,8 +39,7 @@ export default {
default:
return {
editable: false,
displaylogo: false,
responsive: true
displaylogo: false
};
}
},
@@ -49,8 +48,7 @@ export default {
const base = {
font: {
color: this.$vuetify.theme.isDark ? "#fff" : undefined
},
autosize: true
}
};
switch (this.facet) {
@@ -276,25 +274,18 @@ export default {
replot () {
if (this.plotted) {
const ref = this.$refs.graph;
if (ref && ref.clientWidth > 0 && ref.clientHeight > 0) {
Plotly.relayout(ref, {
width: ref.clientWidth,
height: ref.clientHeight
});
}
Plotly.relayout(ref, {
width: ref.clientWidth,
height: ref.clientHeight
});
}
}
},
mounted () {
this.$nextTick( () => {
if (this.items?.length) {
this.plot();
}
this.resizeObserver = new ResizeObserver(this.replot)
this.resizeObserver.observe(this.$refs.graph);
});
this.resizeObserver = new ResizeObserver(this.replot)
this.resizeObserver.observe(this.$refs.graph);
},
beforeDestroy () {

View File

@@ -36,8 +36,7 @@ export default {
config () {
return {
editable: false,
displaylogo: false,
responsive: true
displaylogo: false
};
},
@@ -54,8 +53,7 @@ export default {
title: "Time (s)"
},
plot_bgcolor:"rgba(0,0,0,0)",
paper_bgcolor:"rgba(0,0,0,0)",
autosize: true
paper_bgcolor:"rgba(0,0,0,0)"
};
},
@@ -156,12 +154,10 @@ export default {
replot () {
if (this.plotted) {
const ref = this.$refs.graph;
if (ref && ref.clientWidth > 0 && ref.clientHeight > 0) {
Plotly.relayout(ref, {
width: ref.clientWidth,
height: ref.clientHeight
});
}
Plotly.relayout(ref, {
width: ref.clientWidth,
height: ref.clientHeight
});
}
},
@@ -194,13 +190,8 @@ export default {
},
mounted () {
this.$nextTick( () => {
if (this.items?.length) {
this.plot();
}
this.resizeObserver = new ResizeObserver(this.replot)
this.resizeObserver.observe(this.$refs.graph);
});
this.resizeObserver = new ResizeObserver(this.replot)
this.resizeObserver.observe(this.$refs.graph);
},
beforeDestroy () {

View File

@@ -102,48 +102,21 @@
class="my-1 mt-4"
title="Fit view"
@click="zoomReset"
>mdi-magnify-scan</v-icon>
>mdi-magnify-scan</v-icon>
</div>
<div>
<v-icon
class="my-1"
title="Zoom in"
@click="zoomIn"
>mdi-magnify-plus-outline</v-icon>
>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>
>mdi-magnify-minus-outline</v-icon>
</div>
<div>
<v-icon
@@ -390,7 +363,6 @@ export default {
//maxZoom: 18,
maxPitch: 89
},
bearing: 0,
heatmapValue: "total_error",
isFullscreen: false,
@@ -435,11 +407,6 @@ export default {
watch: {
baseline () {
// We need the configuration of the baseline project so that
// the "bin up" orientation control will work.
if (this.baseline?.pid) {
this.$store.dispatch('getProject', this.baseline.pid);
}
this.populateDataLayersAvailable();
},
@@ -551,41 +518,6 @@ export default {
}
},
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) {
@@ -813,7 +745,7 @@ export default {
async initLayers (gl) {
// Does nothing
deck.onViewStateChange = this.updateURL;
},
setViewState () {
@@ -828,10 +760,6 @@ export default {
}
},
viewStateUpdated ({viewState}) {
this.bearing = viewState.bearing;
},
checkWebGLSupport() {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
@@ -1254,8 +1182,7 @@ export default {
layers: [],
getTooltip: this.getTooltip,
pickingRadius: 24,
onWebGLInitialized: this.initLayers,
onViewStateChange: this.viewStateUpdated,
onWebGLInitialized: this.initLayers
});
//console.log("deck = ", deck);
@@ -1267,10 +1194,6 @@ export default {
this.layersPendingLoad.push(`${this.baseline.pid}-seqfl`);
this.layersPendingLoad.push(`${this.baseline.pid}-seqfp`);
// We need the configuration of the baseline project so that
// the "bin up" orientation control will work.
this.$store.dispatch('getProject', this.baseline.pid);
}
if (this.monitors) {

View File

@@ -158,7 +158,6 @@ Vue.use(VueRouter)
component: SequenceList
},
{
name: "shotlog",
path: "sequences/:sequence",
component: SequenceSummary
},

View File

@@ -36,7 +36,7 @@ async function refreshEvents ({commit, dispatch, state, rootState}, [modifiedAft
/** Return a subset of events from state.events
*/
async function getEvents ({commit, dispatch, state}, [projectId, {sequence, date0, date1, sortBy, sortDesc, itemsPerPage, page, text, label, excludeLabels}]) {
async function getEvents ({commit, dispatch, state}, [projectId, {sequence, date0, date1, sortBy, sortDesc, itemsPerPage, page, text, label}]) {
let filteredEvents = [...state.events];
if (sortBy) {
@@ -114,10 +114,6 @@ async function getEvents ({commit, dispatch, state}, [projectId, {sequence, date
filteredEvents = filteredEvents.filter( event => event.labels?.includes(label) );
}
if (excludeLabels) {
filteredEvents = filteredEvents.filter( event => !excludeLabels?.some( label => event.labels?.includes(label) ) );
}
const count = filteredEvents.length;
if (itemsPerPage && itemsPerPage > 0) {

View File

@@ -5,22 +5,6 @@
<v-card-title>
<v-toolbar flat>
<v-toolbar-title>
<template v-if="$route.params.sequence">
<v-btn icon small
:disabled="sequenceIndex >= (sequences.length - 1)"
:to="{name: 'logBySequence', params: { sequence: (sequences[sequences.length-1]||{}).sequence }}"
title="Go to the first sequence"
>
<v-icon dense>mdi-chevron-double-left</v-icon>
</v-btn>
<v-btn icon small
:disabled="sequenceIndex >= (sequences.length - 1)"
:to="{name: 'logBySequence', params: { sequence: (sequences[sequenceIndex+1]||{}).sequence }}"
title="Go to the previous sequence"
>
<v-icon dense>mdi-chevron-left</v-icon>
</v-btn>
</template>
<span class="d-none d-lg-inline">
{{
$route.params.sequence
@@ -47,38 +31,18 @@
: ""
}}
</span>
<template v-if="$route.params.sequence">
<v-btn icon small
:disabled="sequenceIndex==0"
:to="{name: 'logBySequence', params: { sequence: (sequences[sequenceIndex-1]||{}).sequence }}"
title="Go to the next sequence"
>
<v-icon dense>mdi-chevron-right</v-icon>
</v-btn>
<v-btn icon small class="mr-1"
:disabled="sequenceIndex==0"
:to="{name: 'logBySequence', params: { sequence: (sequences[0]||{}).sequence }}"
title="Go to the last sequence"
>
<v-icon dense>mdi-chevron-double-right</v-icon>
</v-btn>
</template>
<a v-if="$route.params.sequence"
class="mr-3"
:href="`/projects/${$route.params.project}/sequences/${$route.params.sequence}`"
title="View the shotlog for this sequence"
>
<v-icon
right
color="teal"
>mdi-format-list-numbered</v-icon>
</a>
</v-toolbar-title>
<a v-if="$route.params.sequence"
class="mr-3"
:href="`/projects/${$route.params.project}/sequences/${$route.params.sequence}`"
title="View the shotlog for this sequence"
>
<v-icon
right
color="teal"
>mdi-format-list-numbered</v-icon>
</a>
<dougal-event-edit v-if="$parent.writeaccess()"
v-model="eventDialog"
@@ -530,6 +494,17 @@ export default {
rows () {
const rows = {};
this.items
.filter(i => {
return !this.$route.params.sequence || (this.$route.params.sequence == i.sequence)
})
.filter(i => {
for (const label of this.filterableLabels) {
if (!this.shownLabels.includes(label) && i.labels.includes(label)) {
return false;
}
}
return true;
})
.forEach(i => {
const key = (i.sequence && i.point) ? (i.sequence+"@"+i.point) : i.tstamp;
if (!rows[key]) {
@@ -560,10 +535,6 @@ export default {
.sort( (a, b) => b[1]-a[1] );
},
filteredLabels () {
return this.filterableLabels.filter( label => !this.shownLabels.includes(label) );
},
presetRemarks () {
return this.projectConfiguration?.events?.presetRemarks ?? [];
},
@@ -576,17 +547,7 @@ export default {
}
},
sequenceIndex () {
if ("sequence" in this.$route.params) {
const index = this.sequences.findIndex( i => i.sequence == this.$route.params.sequence );
if (index != -1) {
return index;
}
}
// return undefined
},
...mapGetters(['user', 'eventsLoading', 'online', 'sequence', 'sequences', 'line', 'point', 'position', 'timestamp', 'lineName', 'events', 'labels', 'userLabels', 'projectConfiguration']),
...mapGetters(['user', 'eventsLoading', 'online', 'sequence', 'line', 'point', 'position', 'timestamp', 'lineName', 'events', 'labels', 'userLabels', 'projectConfiguration']),
...mapState({projectSchema: state => state.project.projectSchema})
},
@@ -594,7 +555,6 @@ export default {
watch: {
options: {
async handler () {
this.savePrefs(),
await this.fetchEvents();
},
deep: true
@@ -613,19 +573,12 @@ export default {
},
filter (newVal, oldVal) {
this.savePrefs();
if (newVal?.toLowerCase() != oldVal?.toLowerCase()) {
this.fetchEvents();
}
},
labelSearch () {
this.savePrefs();
this.fetchEvents();
},
filteredLabels () {
this.savePrefs()
this.fetchEvents();
},
@@ -634,7 +587,7 @@ export default {
},
user (newVal, oldVal) {
this.loadPrefs();
this.itemsPerPage = Number(localStorage.getItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/items-per-page`)) || 25;
}
},
@@ -685,10 +638,8 @@ export default {
async fetchEvents (opts = {}) {
const options = {
sequence: this.$route.params.sequence,
text: this.filter,
label: this.labelSearch,
excludeLabels: this.filteredLabels,
...this.options
};
const res = await this.getEvents([this.$route.params.project, options]);
@@ -926,36 +877,10 @@ export default {
*/
},
getPrefsKey () {
return `dougal/prefs/${this.user?.name}/${this.$route.params.project}/Log/v1`;
},
savePrefs () {
const prefs = {
shownLabels: this.shownLabels,
labelSearch: this.labelSearch,
filter: this.filter,
options: this.options
};
localStorage.setItem(this.getPrefsKey(), JSON.stringify(prefs));
},
loadPrefs () {
const stored = localStorage.getItem(this.getPrefsKey());
if (stored) {
const prefs = JSON.parse(stored);
if (prefs.shownLabels !== undefined) this.shownLabels = prefs.shownLabels;
if (prefs.labelSearch !== undefined) this.labelSearch = prefs.labelSearch;
if (prefs.filter !== undefined) this.filter = prefs.filter;
if (prefs.options !== undefined) this.options = prefs.options;
}
},
...mapActions(["api", "showSnack", "refreshEvents", "getEvents"])
},
async mounted () {
this.loadPrefs();
this.fetchEvents();
window.addEventListener('keyup', this.handleKeyboardEvent);

View File

@@ -470,33 +470,6 @@
@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"
@@ -688,7 +661,6 @@ export default {
//maxZoom: 18,
maxPitch: 89
},
bearing: 0,
vesselPosition: null,
vesselTrackLastRefresh: 0,
@@ -1005,41 +977,6 @@ export default {
}
},
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) {
@@ -1431,7 +1368,7 @@ export default {
//console.log("SHOULD BE INITIALISING LAYERS HERE", gl);
this.decodeURL();
this.decodeURLHash();
//deck.onViewStateChange = this.viewStateUpdated;
deck.onViewStateChange = this.updateURL;
},
setViewState () {
@@ -1446,11 +1383,6 @@ export default {
}
},
viewStateUpdated ({viewState}) {
this.bearing = viewState.bearing;
this.updateURL({viewState});
},
updateURL ({viewState} = {}) {
if (!viewState && deck?.viewManager) {
viewState = deck.getViewports()[0];
@@ -1777,8 +1709,7 @@ export default {
layers: [],
getTooltip: this.getTooltip,
pickingRadius: 24,
onWebGLInitialized: this.initLayers,
onViewStateChange: this.viewStateUpdated,
onWebGLInitialized: this.initLayers
});
// Get fullscreen state

View File

@@ -6,42 +6,8 @@
<v-progress-linear indeterminate v-if="loading"></v-progress-linear>
<v-toolbar flat>
<v-toolbar-title>
<template v-if="$route.params.sequence">
<v-btn icon small
:disabled="sequenceIndex >= (sequences.length - 1)"
:to="{name: 'shotlog', params: { sequence: (sequences[sequences.length-1]||{}).sequence }}"
title="Go to the first sequence"
>
<v-icon dense>mdi-chevron-double-left</v-icon>
</v-btn>
<v-btn icon small
:disabled="sequenceIndex >= (sequences.length - 1)"
:to="{name: 'shotlog', params: { sequence: (sequences[sequenceIndex+1]||{}).sequence }}"
title="Go to the previous sequence"
>
<v-icon dense>mdi-chevron-left</v-icon>
</v-btn>
</template>
Sequence {{sequenceNumber}}
<small :class="statusColour" v-if="sequence">({{sequence.status}})</small>
<template v-if="$route.params.sequence">
<v-btn icon small
:disabled="sequenceIndex==0"
:to="{name: 'shotlog', params: { sequence: (sequences[sequenceIndex-1]||{}).sequence }}"
title="Go to the next sequence"
>
<v-icon dense>mdi-chevron-right</v-icon>
</v-btn>
<v-btn icon small class="mr-1"
:disabled="sequenceIndex==0"
:to="{name: 'shotlog', params: { sequence: (sequences[0]||{}).sequence }}"
title="Go to the last sequence"
>
<v-icon dense>mdi-chevron-double-right</v-icon>
</v-btn>
</template>
</v-toolbar-title>
<a v-if="$route.params.sequence"
@@ -386,16 +352,6 @@ export default {
return this.sequences.find(i => i.sequence == this.sequenceNumber);
},
sequenceIndex () {
if ("sequence" in this.$route.params) {
const index = this.sequences.findIndex( i => i.sequence == this.$route.params.sequence );
if (index != -1) {
return index;
}
}
// return undefined
},
remarks () {
return this.sequence?.remarks || "Nil.";
},

View File

@@ -36,16 +36,8 @@ async function fetchErrors (pid) {
}
async function groupTimestamps (groupName) {
const projects = await groups()?.[groupName];
if (projects?.length) {
}
}
async function groups () {
const projects = await db.project.get({timestamps: true});
const projects = await db.project.get();
const groupNames = [
...projects
.reduce( (acc, cur) => acc.add(...cur.groups), new Set() )
@@ -54,6 +46,19 @@ async function groups () {
return Object.fromEntries(groupNames.map( g => [g, projects.filter( p => p.groups.includes(g) )] ));
}
/*
async function compare (baselineProjectID, monitorProjectID) {
console.log("Getting baseline", baselineProjectID);
const baselineData = await db.sequence.get(baselineProjectID);
console.log("Getting monitor", monitorProjectID);
const monitorData = await db.sequence.get(monitorProjectID);
console.log("Comparing");
const comparison = comparisonGeometricDifferences(baselineData, monitorData);
return comparison;
}
*/
function geometric_differences (baseline, monitor) {
if (!baseline || !baseline.length) {
@@ -155,6 +160,67 @@ async function save (baselineProjectID, monitorProjectID, bundle, meta) {
}
async function saveSample (baselineProjectID, monitorProjectID, opts = {}) {
DEBUG("Not bothering to save samples. This feature will be removed.");
}
/*
async function saveSample (baselineProjectID, monitorProjectID, opts = {}) {
let sample = opts.sample;
let populationStats = opts.populationStats;
let sampleStats = opts.sampleStats;
if (!sample?.length) {
const sampleSize = opts.sampleSize ?? 2000;
const record = await get(baselineProjectID, monitorProjectID);
let data;
if (record) {
data = record.data;
} else {
console.log("Full data not found in database");
data = asBundle(await compare(baselineProjectID, monitorProjectID));
}
sample = computeSample(data, opts);
if (!populationStats) {
populationStats = stats(data);
}
}
const bundle = asBundle(sample, {type: 0x0c});
if (!sampleStats) {
sampleStats = stats(bundle);
}
meta = {tstamp: (new Date()), populationStats, sampleStats, ...(opts.meta??{})};
const client = await pool.connect();
try {
const text = `
INSERT INTO comparisons.comparisons
(type, baseline_pid, monitor_pid, data, meta)
VALUES ('geometric_difference_sample', $1, $2, $3, $4)
ON CONFLICT (type, baseline_pid, monitor_pid)
DO UPDATE SET
data = EXCLUDED.data,
meta = EXCLUDED.meta;
`;
const values = [ baselineProjectID, monitorProjectID, Buffer.from(bundle), meta ];
const res = await client.query(text, values);
return res.rowCount;
} catch (err) {
console.error(err);
} finally {
client.release();
}
}
*/
async function get (baselineProjectID, monitorProjectID, type = 'geometric_difference') {
const client = await pool.connect();
@@ -262,6 +328,77 @@ function stats (comparison) {
}
/** Compare two projects' errorfinal quantities.
*
* Assumes that the preplots are the same.
* It is not a terribly efficient way of doing it, but considering
* that this is, by and large only going to be done once every few
* hours for an active prospect, and never for inactive ones, I
* think and hope we can live with that.
*
* `baseline` and `monitor` are the result of calling
* db.sequence.get(projectId) on each of the respective
* projects.
*/
/*
function comparisonGeometricDifferences (baseline, monitor) {
if (!baseline || !baseline.length) {
throw new Error("No baseline data");
}
if (!monitor || !monitor.length) {
throw new Error("No monitor data");
}
const comparison = []; // An array of { line, point, εi, εj, δts }; line + point may be repeated
for (const bp of baseline) {
if (!bp.errorfinal) {
console.log(`No final data for baseline point L${bp.line} S${bp.sequence} P${bp.point}`);
continue;
}
const monitor_points = monitor.filter( mp => mp.line === bp.line && mp.point === bp.point );
for (const mp of monitor_points) {
if (!mp.errorfinal) {
console.log(`No final data for monitor point L${mp.line} S${mp.sequence} P${mp.point}`);
continue;
}
const line = bp.line;
const point = bp.point;
const baseSeq = bp.sequence;
const monSeq = mp.sequence;
const baseTStamp = bp.tstamp;
const monTStamp = mp.tstamp;
const δi = bp.errorfinal.coordinates[0] - mp.errorfinal.coordinates[0];
const δj = bp.errorfinal.coordinates[1] - mp.errorfinal.coordinates[1];
const obj = {line, point, baseSeq, monSeq, baseTStamp, monTStamp, δi, δj};
comparison.push(obj);
// console.log(obj);
}
}
return comparison.sort(sortFn);
}
function sortComparison (comparison) {
return comparison.sort( (a, b) => {
if (a.line == b.line) {
if (a.point == b.point) {
return a.baseTStamp - b.baseTStamp;
} else {
return a.point - b.point;
}
} else {
return a.line - b.line;
}
})
}
*/
function sortFn (a, b) {
if (a.line == b.line) {
if (a.point == b.point) {
@@ -317,7 +454,12 @@ async function saveGroup (group, opts = {}) {
}
}
await save(baselineProjectID, monitorProjectID);
const isSaved = await save(baselineProjectID, monitorProjectID);
if (isSaved) {
await saveSample(baselineProjectID, monitorProjectID, opts.sampleOpts);
} else {
await remove(baselineProjectID, monitorProjectID);
}
DEBUG("Saved comparison between %s and %s", baselineProjectID, monitorProjectID);
} catch (err) {
console.error(err);
@@ -327,83 +469,88 @@ async function saveGroup (group, opts = {}) {
}
}
/*
async function getGroup (groupName, opts = {}) {
const group = (await groups())?.[groupName]?.map( i => i.pid)?.sort();
if (!group?.length) return;
const client = await pool.connect();
try {
if (groupName) {
const text = `
-- SQL query goes here
`;
const group = (await groups())?.[groupName]?.map( i => i.pid)?.sort();
if (!group?.length || group?.length < 2) return;
const pairs = combinations(group, 2);
const flatValues = pairs.flat();
const placeholders = [];
for (let i = 0; i < pairs.length; i++) {
placeholders.push(`($${i * 2 + 1}, $${i * 2 + 2})`);
}
const inClause = placeholders.join(',');
const selectFields = opts.returnData ? 'data, meta' : 'meta';
const text = `
SELECT baseline_pid, monitor_pid, ${selectFields}
FROM comparisons.comparisons
WHERE type = 'geometric_difference'
AND (baseline_pid, monitor_pid) IN (VALUES ${inClause})
ORDER BY baseline_pid, monitor_pid
`;
if (!placeholders) {
console.log("No pairs found in group");
return [];
}
const res = await client.query(text, flatValues);
if (!res.rows.length) {
console.log("Comparison not found in database");
return;
}
if (opts.returnData) {
return res.rows.map( row => ({
...row,
data: DougalBinaryBundle.clone(row.data),
}));
} else {
return res.rows;
}
const values = combinations(group, 2);
const res = await client.query(text, values);
if (!res.rows.length) {
console.log("Comparison not found in database");
return;
}
if (opts.returnData) {
return res.rows.map( row => ({
data: DougalBinaryBundle.clone(row.data),
meta: row.meta
});
} else {
return res.rows.map( row => row.meta );
}
} catch (err) {
console.error(err);
} finally {
client.release();
}
}
*/
const selectFields = opts.returnData ? 'data, meta' : 'meta';
const text = `
SELECT baseline_pid, monitor_pid, ${selectFields}
FROM comparisons.comparisons
WHERE type = 'geometric_difference'
ORDER BY baseline_pid, monitor_pid
`;
async function getGroup (groupName, opts = {}) {
const res = await client.query(text);
if (!res.rows.length) {
console.log("Comparison not found in database");
return;
}
const group = (await groups())?.[groupName]?.map( i => i.pid)?.sort();
if (opts.returnData) {
return res.rows.map( row => ({
...row,
data: DougalBinaryBundle.clone(row.data),
}));
} else {
return res.rows;
}
if (!group?.length) return;
const client = await pool.connect();
try {
const pairs = combinations(group, 2);
const flatValues = pairs.flat();
const placeholders = [];
for (let i = 0; i < pairs.length; i++) {
placeholders.push(`($${i * 2 + 1}, $${i * 2 + 2})`);
}
const inClause = placeholders.join(',');
const selectFields = opts.returnData ? 'data, meta' : 'meta';
const text = `
SELECT baseline_pid, monitor_pid, ${selectFields}
FROM comparisons.comparisons
WHERE type = 'geometric_difference'
AND (baseline_pid, monitor_pid) IN (VALUES ${inClause})
ORDER BY baseline_pid, monitor_pid
`;
console.log(text);
console.log(flatValues);
const res = await client.query(text, flatValues);
if (!res.rows.length) {
console.log("Comparison not found in database");
return;
}
if (opts.returnData) {
return res.rows.map( row => ({
...row,
data: DougalBinaryBundle.clone(row.data),
}));
} else {
return res.rows;
}
} catch (err) {
console.error(err);
@@ -420,10 +567,12 @@ module.exports = {
get,
save,
getSample,
saveSample,
saveGroup,
getGroup,
remove,
stats,
// comparisonGeometricDifferences,
asBundle,
fromBundle
};

View File

@@ -1,14 +1,8 @@
const { setSurvey, pool } = require('../connection');
async function get (opts = {}) {
const select = opts.timestamps
? "last_project_update(pid) tstamp,"
: "";
async function get () {
const text = `
SELECT
${select}
pid,
name,
schema,

View File

@@ -16,7 +16,7 @@
"api": "0.4.0"
},
"wanted": {
"db_schema": "^0.6.6"
"db_schema": "^0.6.5"
}
},
"engines": {