mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 12:07:08 +00:00
Compare commits
24 Commits
v2025.32.1
...
v2025.33.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd23a78592 | ||
|
|
e368183bf0 | ||
|
|
02477b071b | ||
|
|
6651868ea7 | ||
|
|
c0b52a8245 | ||
|
|
90ce6f063e | ||
|
|
b2fa0c3d40 | ||
|
|
83ecaad4fa | ||
|
|
1c5fd2e34d | ||
|
|
aabcc74891 | ||
|
|
2a7b51b995 | ||
|
|
5d19ca7ca7 | ||
|
|
910195fc0f | ||
|
|
6e5570aa7c | ||
|
|
595c20f504 | ||
|
|
40d0038d80 | ||
|
|
acdf118a67 | ||
|
|
b9e0975d3d | ||
|
|
39d9c9d748 | ||
|
|
b8b25dcd62 | ||
|
|
db97382758 | ||
|
|
ae8e5d4ef6 | ||
|
|
2c1a24e4a5 | ||
|
|
0b83187372 |
@@ -9,10 +9,12 @@
|
||||
"dependencies": {
|
||||
"@deck.gl/aggregation-layers": "^9.1.13",
|
||||
"@deck.gl/geo-layers": "^9.1.13",
|
||||
"@deck.gl/mesh-layers": "^9.1.14",
|
||||
"@dougal/binary": "file:../../../modules/@dougal/binary",
|
||||
"@dougal/concurrency": "file:../../../modules/@dougal/concurrency",
|
||||
"@dougal/organisations": "file:../../../modules/@dougal/organisations",
|
||||
"@dougal/user": "file:../../../modules/@dougal/user",
|
||||
"@loaders.gl/obj": "^4.3.4",
|
||||
"@mdi/font": "^7.2.96",
|
||||
"buffer": "^6.0.3",
|
||||
"core-js": "^3.6.5",
|
||||
|
||||
406982
lib/www/client/source/public/assets/boat0.obj
Normal file
406982
lib/www/client/source/public/assets/boat0.obj
Normal file
File diff suppressed because it is too large
Load Diff
@@ -44,7 +44,16 @@
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<pre>{{ versionHistory }}</pre>
|
||||
<v-carousel v-model="releaseShown"
|
||||
:continuous="false"
|
||||
:cycle="false"
|
||||
:show-arrows="true"
|
||||
:hide-delimiters="true"
|
||||
>
|
||||
<v-carousel-item v-for="release in releaseHistory">
|
||||
<pre>{{release}}</pre>
|
||||
</v-carousel-item>
|
||||
</v-carousel>
|
||||
</v-card-text>
|
||||
|
||||
|
||||
@@ -127,6 +136,8 @@ export default {
|
||||
clientVersion: process.env.DOUGAL_FRONTEND_VERSION ?? "(unknown)",
|
||||
serverVersion: null,
|
||||
versionHistory: null,
|
||||
releaseHistory: [],
|
||||
releaseShown: null,
|
||||
page: "support"
|
||||
};
|
||||
},
|
||||
@@ -138,7 +149,8 @@ export default {
|
||||
this.serverVersion = version?.tag ?? "(unknown)";
|
||||
}
|
||||
if (!this.versionHistory) {
|
||||
const history = await this.api(['/version/history?count=1', {}, null, {silent:true}]);
|
||||
const history = await this.api(['/version/history?count=3', {}, null, {silent:true}]);
|
||||
this.releaseHistory = history;
|
||||
this.versionHistory = history?.[this.serverVersion.replace(/-.*$/, "")] ?? null;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -25,20 +25,22 @@ async function login ({ commit, dispatch }, loginRequest) {
|
||||
async function logout ({ commit, dispatch }) {
|
||||
commit('setToken', null);
|
||||
commit('setUser', null);
|
||||
commit('setCookie', {value: null});
|
||||
await dispatch('api', ["/logout"]);
|
||||
commit('setPreferences', {});
|
||||
}
|
||||
|
||||
function setCookie(context, {name, value, expiry, path}) {
|
||||
if (!name) name = "JWT";
|
||||
if (!path) path = "/";
|
||||
if (!value) value = "";
|
||||
|
||||
if (expiry) {
|
||||
document.cookie = `${name}=${value}; expiry=${(new Date(expiry)).toUTCString()}; path=${path}`;
|
||||
if (name) {
|
||||
if (expiry) {
|
||||
document.cookie = `${name}=${value}; expiry=${(new Date(expiry)).toUTCString()}; path=${path}`;
|
||||
} else {
|
||||
document.cookie = `${name}=${value}; path=${path}`;
|
||||
}
|
||||
} else {
|
||||
document.cookie = `${name}=${value}; path=${path}`;
|
||||
console.warn(`seCookie: You must supply a name`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,17 +62,6 @@ function setCredentials({ state, commit, getters, dispatch, rootState }, { force
|
||||
commit('setToken', tokenValue);
|
||||
commit('setUser', decoded ? new User(decoded, rootState.api.api) : null);
|
||||
|
||||
if (tokenValue && decoded) {
|
||||
if (decoded?.exp) {
|
||||
dispatch('setCookie', {value: tokenValue, expiry: decoded.exp*1000});
|
||||
} else {
|
||||
dispatch('setCookie', {value: tokenValue});
|
||||
}
|
||||
} else {
|
||||
// Clear the cookie
|
||||
dispatch('setCookie', {value: "", expiry: 0});
|
||||
}
|
||||
|
||||
console.log('Credentials refreshed at', new Date().toISOString());
|
||||
} else {
|
||||
console.log('JWT unchanged, skipping update');
|
||||
|
||||
@@ -7,12 +7,6 @@ function jwt (state) {
|
||||
return state.token;
|
||||
}
|
||||
|
||||
function cookie (state) {
|
||||
if (state.token) {
|
||||
return "JWT="+token;
|
||||
}
|
||||
}
|
||||
|
||||
function preferences (state) {
|
||||
return state.preferences;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,47 @@
|
||||
|
||||
<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>
|
||||
@@ -359,6 +399,7 @@
|
||||
</v-select>
|
||||
END QC data -->
|
||||
|
||||
<!--
|
||||
<hr class="my-2"/>
|
||||
|
||||
<div title="Not yet implemented">
|
||||
@@ -371,6 +412,7 @@
|
||||
Map settings
|
||||
</v-btn>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -620,6 +662,67 @@ export default {
|
||||
maxPitch: 89
|
||||
},
|
||||
|
||||
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: [],
|
||||
@@ -761,6 +864,14 @@ export default {
|
||||
deep: true
|
||||
},
|
||||
|
||||
vesselTrackPeriod () {
|
||||
this.updateVesselIntervalTimer();
|
||||
},
|
||||
|
||||
vesselTrackLastRefresh () {
|
||||
this.render();
|
||||
},
|
||||
|
||||
lines () {
|
||||
// Refresh map on change of preplot data
|
||||
this.render();
|
||||
@@ -1062,6 +1173,12 @@ export default {
|
||||
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
|
||||
@@ -1407,6 +1524,19 @@ export default {
|
||||
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}`);
|
||||
@@ -1438,18 +1568,46 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
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: (context, message) => {
|
||||
this.handleSequences(context, message);
|
||||
}
|
||||
handler: this.handleSequences
|
||||
})
|
||||
});
|
||||
|
||||
this.$store.dispatch(action, {
|
||||
table: 'realtime',
|
||||
handler: this.handleVesselPosition
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
unregisterNotificationHandlers () {
|
||||
@@ -1473,6 +1631,8 @@ export default {
|
||||
console.log("TODO: Should switch to legacy map view");
|
||||
}
|
||||
|
||||
this.updateVesselIntervalTimer();
|
||||
|
||||
this.layersAvailable.osm = this.osmLayer;
|
||||
|
||||
this.layersAvailable.sea = this.openSeaMapLayer;
|
||||
@@ -1572,6 +1732,7 @@ export default {
|
||||
|
||||
beforeDestroy () {
|
||||
this.unregisterNotificationHandlers();
|
||||
this.vesselTrackIntervalID = this.clearInterval(this.vesselTrackIntervalID);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
import { Deck, WebMercatorViewport, FlyToInterpolator, CompositeLayer } from '@deck.gl/core';
|
||||
import { GeoJsonLayer, LineLayer, PathLayer, BitmapLayer, ScatterplotLayer, ColumnLayer, IconLayer } from '@deck.gl/layers';
|
||||
import {HeatmapLayer} from '@deck.gl/aggregation-layers';
|
||||
import { TileLayer, MVTLayer } from '@deck.gl/geo-layers';
|
||||
|
||||
import { TileLayer, MVTLayer, TripsLayer } from '@deck.gl/geo-layers';
|
||||
import { SimpleMeshLayer } from '@deck.gl/mesh-layers';
|
||||
import { OBJLoader } from '@loaders.gl/obj';
|
||||
|
||||
//import { json } from 'd3-fetch';
|
||||
import * as d3a from 'd3-array';
|
||||
@@ -18,7 +19,6 @@ import DougalBinaryLoader from '@/lib/deck.gl/DougalBinaryLoader';
|
||||
|
||||
import { colors } from 'vuetify/lib'
|
||||
|
||||
|
||||
function hexToArray (hex, defaultValue = [ 0xc0, 0xc0, 0xc0, 0xff ]) {
|
||||
|
||||
if (typeof hex != "string" || hex.length < 6) {
|
||||
@@ -121,6 +121,21 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
loadOptions (options = {}) {
|
||||
return {
|
||||
loadOptions: {
|
||||
fetch: {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.$store.getters.jwt}`,
|
||||
}
|
||||
},
|
||||
...options
|
||||
},
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
|
||||
osmLayer (options = {}) {
|
||||
return new TileLayer({
|
||||
@@ -241,45 +256,99 @@ export default {
|
||||
},
|
||||
|
||||
vesselTrackPointsLayer (options = {}) {
|
||||
return new ScatterplotLayer({
|
||||
|
||||
if (!this.vesselPosition) return;
|
||||
|
||||
return new SimpleMeshLayer({
|
||||
id: 'navp',
|
||||
data: `/api/navdata?limit=10000`,
|
||||
getPosition: (d) => ([d.longitude, d.latitude]),
|
||||
getRadius: d => (d.speed),
|
||||
radiusScale: 1,
|
||||
lineWidthMinPixels: 2,
|
||||
getFillColor: d => d.guns
|
||||
? d.lineStatus == "online"
|
||||
? [0xaa, 0x00, 0xff] // Online
|
||||
: [0xd5, 0x00, 0xf9] // Soft start or guns otherwise active
|
||||
: [0xea, 0x80, 0xfc], // Offline, guns inactive
|
||||
getLineColor: [127, 65, 90],
|
||||
getColor: [ 255, 0, 0 ],
|
||||
getPointRadius: 12,
|
||||
radiusUnits: "pixels",
|
||||
pointRadiusMinPixels: 4,
|
||||
stroked: false,
|
||||
filled: true,
|
||||
data: [ this.vesselPosition ],
|
||||
//getColor: [ 255, 48, 0 ],
|
||||
getColor: [ 174, 1, 174 ],
|
||||
getOrientation: d => [0, (270 - (d.heading ?? d.cmg ?? d.bearing ?? d.lineBearing ?? 0)) % 360 , 0],
|
||||
getPosition: d => [ d.x, d.y ],
|
||||
mesh: `/assets/boat0.obj`,
|
||||
sizeScale: 0.1,
|
||||
loaders: [OBJLoader],
|
||||
pickable: true,
|
||||
...options
|
||||
})
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
vesselTrackLinesLayer (options = {}) {
|
||||
return new LineLayer({ // TODO Change to TrackLayer
|
||||
|
||||
const cfg = this.vesselTrackPeriodSettings[this.vesselTrackPeriod];
|
||||
|
||||
let ts1 = new Date(this.vesselTrackLastRefresh*1000);
|
||||
let ts0 = new Date(ts1.valueOf() - cfg.offset);
|
||||
let di = cfg.decimation;
|
||||
let l = 10000;
|
||||
|
||||
const breakLimit = (di ? di*20 : 5 * 60) * 1000;
|
||||
|
||||
let trailLength = (ts1 - ts0) / 1000;
|
||||
|
||||
return new TripsLayer({
|
||||
id: 'navl',
|
||||
data: `/api/navdata?v=${Date.now()}`, // NOTE Not too sure about this
|
||||
lineWidthMinPixels: 2,
|
||||
getLineColor: (d) => d.properties.ntba ? [240, 248, 255, 200] : [85, 170, 255, 200],
|
||||
getSourcePosition: (obj, i) => i.index < i.data?.length ? [i.data[i.index]?.longitude, i.data[i.index]?.latitude] : null,
|
||||
getTargetPosition: (obj, i) => i.index < i.data?.length ? [i.data[i.index+1]?.longitude, i.data[i.index+1]?.latitude] : null,
|
||||
getLineWidth: 3,
|
||||
getPointRadius: 2,
|
||||
radiusUnits: "pixels",
|
||||
pointRadiusMinPixels: 2,
|
||||
data: `/api/vessel/track/?di=${di}&l=${l}&project=&ts0=${ts0.toISOString()}&ts1=${ts1.toISOString()}`,
|
||||
...this.loadOptions({
|
||||
fetch: {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.$store.getters.jwt}`,
|
||||
}
|
||||
}
|
||||
}),
|
||||
dataTransform: (data) => {
|
||||
if (data.length >= l) {
|
||||
console.warn(`Vessel track data may be truncated! Limit: ${l}`);
|
||||
}
|
||||
|
||||
const paths = [];
|
||||
let prevTstamp;
|
||||
paths.push({path: [], timestamps: [], num: 0, ts0: +Infinity, ts1: -Infinity});
|
||||
for (const el of data) {
|
||||
const tstamp = new Date(el.tstamp).valueOf();
|
||||
const curPath = () => paths[paths.length-1];
|
||||
if (prevTstamp && Math.abs(tstamp - prevTstamp) > breakLimit) {
|
||||
// Start a new path
|
||||
console.log(`Breaking path on interval ${Math.abs(tstamp - prevTstamp)} > ${breakLimit}`);
|
||||
paths.push({path: [], timestamps: [], num: paths.length, ts0: +Infinity, ts1: -Infinity});
|
||||
}
|
||||
|
||||
if (tstamp < curPath().ts0) {
|
||||
curPath().ts0 = tstamp;
|
||||
}
|
||||
if (tstamp > curPath().ts1) {
|
||||
curPath().ts1 = tstamp;
|
||||
}
|
||||
|
||||
curPath().path.push([el.x, el.y]);
|
||||
curPath().timestamps.push(tstamp/1000);
|
||||
prevTstamp = tstamp;
|
||||
}
|
||||
|
||||
paths.forEach (path => {
|
||||
path.nums = paths.length;
|
||||
path.ts0 = new Date(path.ts0);
|
||||
path.ts1 = new Date(path.ts1);
|
||||
});
|
||||
|
||||
return paths;
|
||||
},
|
||||
getPath: d => d.path,
|
||||
getTimestamps: d => d.timestamps,
|
||||
currentTime: ts1.valueOf() / 1000,
|
||||
trailLength,
|
||||
widthUnits: "meters",
|
||||
widthMinPixels: 4,
|
||||
getWidth: 10,
|
||||
getColor: [ 174, 1, 126, 200 ],
|
||||
stroked: true,
|
||||
pickable: true,
|
||||
...options
|
||||
})
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
eventsLogLayer (options = {}) {
|
||||
@@ -308,6 +377,7 @@ export default {
|
||||
return new DougalEventsLayer({
|
||||
id: 'log',
|
||||
data: `/api/project/${this.$route.params.project}/event?mime=application/geo%2Bjson`,
|
||||
...this.loadOptions(),
|
||||
lineWidthMinPixels: 2,
|
||||
getPosition: d => d.geometry.coordinates,
|
||||
jitter: 0.00015,
|
||||
@@ -332,6 +402,7 @@ export default {
|
||||
return new GeoJsonLayer({
|
||||
id: 'psll',
|
||||
data: `/api/project/${this.$route.params.project}/gis/preplot/line?class=V&v=${this.lineTStamp?.valueOf()}`,
|
||||
...this.loadOptions(),
|
||||
lineWidthMinPixels: 1,
|
||||
getLineColor: (d) => d.properties.ntba ? [240, 248, 255, 200] : [85, 170, 255, 200],
|
||||
getLineWidth: 1,
|
||||
@@ -347,6 +418,7 @@ export default {
|
||||
return new GeoJsonLayer({
|
||||
id: 'ppll',
|
||||
data: `/api/project/${this.$route.params.project}/gis/preplot/line?v=${this.lineTStamp?.valueOf()}`,
|
||||
...this.loadOptions(),
|
||||
lineWidthMinPixels: 1,
|
||||
getLineColor: (d) => d.properties.ntba ? [240, 248, 255, 200] : [85, 170, 255, 200],
|
||||
getLineWidth: 1,
|
||||
@@ -393,6 +465,7 @@ export default {
|
||||
return new GeoJsonLayer({
|
||||
id: 'seqrl',
|
||||
data: `/api/project/${this.$route.params.project}/gis/raw/line?v=${this.sequenceTStamp?.valueOf()}`,
|
||||
...this.loadOptions(),
|
||||
lineWidthMinPixels: 1,
|
||||
getLineColor: (d) => d.properties.ntbp ? [0xe6, 0x51, 0x00, 200] : [0xff, 0x98, 0x00, 200],
|
||||
getLineWidth: 1,
|
||||
@@ -408,6 +481,7 @@ export default {
|
||||
return new GeoJsonLayer({
|
||||
id: 'seqfl',
|
||||
data: `/api/project/${this.$route.params.project}/gis/final/line?v=${this.sequenceTStamp?.valueOf()}`,
|
||||
...this.loadOptions(),
|
||||
lineWidthMinPixels: 1,
|
||||
getLineColor: (d) => d.properties.pending ? [0xa7, 0xff, 0xab, 200] : [0x00, 0x96, 0x88, 200],
|
||||
getLineWidth: 1,
|
||||
@@ -424,12 +498,15 @@ export default {
|
||||
id: 'pslp',
|
||||
data: `/api/project/${this.$route.params.project}/line/sail?v=${this.lineTStamp?.valueOf()}`, // API endpoint returning binary data
|
||||
loaders: [DougalBinaryLoader],
|
||||
loadOptions: {
|
||||
...this.loadOptions({
|
||||
fetch: {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/vnd.aaltronav.dougal+octet-stream' }
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.$store.getters.jwt}`,
|
||||
Accept: 'application/vnd.aaltronav.dougal+octet-stream'
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
getRadius: 2,
|
||||
getFillColor: (d, {data, index}) => data.attributes.value2.value[index] ? [240, 248, 255, 200] : [85, 170, 255, 200],
|
||||
//getFillColor: [0, 120, 220, 200],
|
||||
@@ -443,12 +520,15 @@ export default {
|
||||
id: 'pplp',
|
||||
data: `/api/project/${this.$route.params.project}/line/source?v=${this.lineTStamp?.valueOf()}`, // API endpoint returning binary data
|
||||
loaders: [DougalBinaryLoader],
|
||||
loadOptions: {
|
||||
...this.loadOptions({
|
||||
fetch: {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/vnd.aaltronav.dougal+octet-stream' }
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.$store.getters.jwt}`,
|
||||
Accept: 'application/vnd.aaltronav.dougal+octet-stream'
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
getRadius: 2,
|
||||
getFillColor: (d, {data, index}) => data.attributes.value2.value[index] ? [240, 248, 255, 200] : [85, 170, 255, 200],
|
||||
//getFillColor: [0, 120, 220, 200],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import * as d3a from 'd3-array';
|
||||
|
||||
export default {
|
||||
name: "MapTooltipsMixin",
|
||||
@@ -28,6 +29,8 @@ export default {
|
||||
return this.sequenceLinesTooltip(args);
|
||||
} else if (args?.layer?.id == "navp") {
|
||||
return this.vesselTrackPointsTooltip(args);
|
||||
} else if (args?.layer?.id == "navl") {
|
||||
return this.vesselTrackLinesTooltip(args);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -235,7 +238,20 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
vesselTrackLinesTooltip (args) {
|
||||
const p = args.object;
|
||||
|
||||
console.log("track lines tooltip", p);
|
||||
|
||||
if (p) {
|
||||
|
||||
let html = `Segment ${p.num+1} / ${p.nums}<br/>\n`
|
||||
html += `${p.ts0.toISOString()}<br/>\n`
|
||||
html += `${p.ts1.toISOString()}<br/>\n`;
|
||||
|
||||
return {html, style: this.tooltipDefaultStyle};
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ const allMeta = (key, value) => {
|
||||
return { all: [ meta(key, value) ] };
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// NOTICE These routes do not require authentication
|
||||
//
|
||||
@@ -103,6 +104,9 @@ app.use(mw.auth.access.user);
|
||||
// Don't process the request if the data hasn't changed
|
||||
app.use(mw.etag.ifNoneMatch);
|
||||
|
||||
// Use compression across the board
|
||||
app.use(mw.compress);
|
||||
|
||||
// We must be authenticated before we can access these
|
||||
app.map({
|
||||
'/project': {
|
||||
@@ -311,6 +315,30 @@ app.map({
|
||||
get: [ mw.etag.noSave, mw.gis.navdata.get ]
|
||||
}
|
||||
},
|
||||
'/vessel/track': {
|
||||
get: [ /*mw.etag.noSave,*/ mw.vessel.track.get ], // JSON array
|
||||
'/line': {
|
||||
get: [ // GeoJSON Feature: type = LineString
|
||||
//mw.etag.noSave,
|
||||
(req, res, next) => { req.query.geojson = 'LineString'; next(); },
|
||||
mw.vessel.track.get
|
||||
]
|
||||
},
|
||||
'/point': {
|
||||
get: [ // GeoJSON FeatureCollection: feature types = Point
|
||||
//mw.etag.noSave,
|
||||
(req, res, next) => { req.query.geojson = 'Point'; next(); },
|
||||
mw.vessel.track.get
|
||||
]
|
||||
},
|
||||
'/points': {
|
||||
get: [ // JSON array of (Feature: type = Point)
|
||||
mw.etag.noSave,
|
||||
(req, res, next) => { req.query.geojson = true; next(); },
|
||||
mw.vessel.track.get
|
||||
],
|
||||
},
|
||||
},
|
||||
'/info/': {
|
||||
':path(*)': {
|
||||
get: [ mw.auth.operations, mw.info.get ],
|
||||
|
||||
@@ -5,8 +5,6 @@ const cfg = require("../../../lib/config").jwt;
|
||||
const getToken = function (req) {
|
||||
if (req.headers.authorization && req.headers.authorization.split(' ')[0] == 'Bearer') {
|
||||
return req.headers.authorization.split(' ')[1];
|
||||
} else if (req.cookies.JWT) {
|
||||
return req.cookies.JWT;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
18
lib/www/server/api/middleware/compress/index.js
Normal file
18
lib/www/server/api/middleware/compress/index.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const compression = require('compression');
|
||||
|
||||
const compress = compression({
|
||||
level: 6, // Balance speed vs. ratio (1-9)
|
||||
threshold: 512, // Compress only if response >512 bytes to avoid overhead on small bundles
|
||||
filter: (req, res) => { // Ensure bundles are compressed
|
||||
const accept = req.get("Accept");
|
||||
if (accept.startsWith("application/vnd.aaltronav.dougal+octet-stream")) return true;
|
||||
if (accept.includes("json")) return true;
|
||||
if (accept.startsWith("text/")) return true;
|
||||
if (accept.startsWith("model/obj")) return true;
|
||||
|
||||
// fallback to standard filter function
|
||||
return compression.filter(req, res)
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = compress;
|
||||
@@ -11,6 +11,7 @@ module.exports = {
|
||||
gis: require('./gis'),
|
||||
label: require('./label'),
|
||||
navdata: require('./navdata'),
|
||||
vessel: require('./vessel'),
|
||||
queue: require('./queue'),
|
||||
qc: require('./qc'),
|
||||
configuration: require('./configuration'),
|
||||
@@ -20,5 +21,6 @@ module.exports = {
|
||||
rss: require('./rss'),
|
||||
etag: require('./etag'),
|
||||
version: require('./version'),
|
||||
admin: require('./admin')
|
||||
admin: require('./admin'),
|
||||
compress: require('./compress'),
|
||||
};
|
||||
|
||||
@@ -8,7 +8,6 @@ async function login (req, res, next) {
|
||||
if (payload) {
|
||||
const token = jwt.issue(payload, req, res);
|
||||
res.set("X-JWT", token);
|
||||
res.set("Set-Cookie", `JWT=${token}`); // For good measure
|
||||
res.status(200).send({token});
|
||||
next();
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
async function logout (req, res, next) {
|
||||
res.clearCookie("JWT");
|
||||
res.status(204).send();
|
||||
next();
|
||||
}
|
||||
|
||||
4
lib/www/server/api/middleware/vessel/index.js
Normal file
4
lib/www/server/api/middleware/vessel/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
module.exports = {
|
||||
track: require('./track'),
|
||||
}
|
||||
10
lib/www/server/api/middleware/vessel/track/get.js
Normal file
10
lib/www/server/api/middleware/vessel/track/get.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const { gis } = require('../../../../lib/db');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
try {
|
||||
res.status(200).send(await gis.vesseltrack.get(req.query));
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
4
lib/www/server/api/middleware/vessel/track/index.js
Normal file
4
lib/www/server/api/middleware/vessel/track/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
module.exports = {
|
||||
get: require('./get'),
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
module.exports = {
|
||||
project: require('./project'),
|
||||
navdata: require('./navdata')
|
||||
navdata: require('./navdata'),
|
||||
vesseltrack: require('./vesseltrack'),
|
||||
// line: require('./line')
|
||||
};
|
||||
|
||||
@@ -7,7 +7,10 @@ async function lines (options = {}) {
|
||||
const client = await pool.connect();
|
||||
|
||||
const text = `
|
||||
SELECT ST_AsGeoJSON(ST_MakeLine(geometry)) geojson
|
||||
SELECT json_build_object(
|
||||
'type', 'Feature',
|
||||
'geometry', ST_AsGeoJSON(ST_MakeLine(geometry))::json
|
||||
) geojson
|
||||
FROM (
|
||||
SELECT geometry
|
||||
FROM real_time_inputs
|
||||
|
||||
191
lib/www/server/lib/db/gis/vesseltrack/get.js
Normal file
191
lib/www/server/lib/db/gis/vesseltrack/get.js
Normal file
@@ -0,0 +1,191 @@
|
||||
const { pool } = require('../../connection');
|
||||
const { project } = require('../../utils');
|
||||
|
||||
async function get(options = {}) {
|
||||
/*
|
||||
* ts0: earliest timestamp (default: NOW - 7 days)
|
||||
* ts1: latest timestamp (if null, assume NOW)
|
||||
* di: decimation interval (return every di-th record, if null: no decimation)
|
||||
* l: limit (return no more than l records, default: 1000, max: 1,000,000)
|
||||
* geojson: 'LineString' (GeoJSON LineString Feature), 'Point' (GeoJSON FeatureCollection),
|
||||
* truthy (array of Point features), falsy (array of {x, y, tstamp, meta})
|
||||
*/
|
||||
let { l, di, ts1, ts0, geojson, projection } = {
|
||||
l: 1000,
|
||||
di: null,
|
||||
ts1: null,
|
||||
ts0: null,
|
||||
geojson: false,
|
||||
...options
|
||||
};
|
||||
|
||||
// Input validation and sanitization
|
||||
l = Math.max(1, Math.min(parseInt(l) || 1000, 1000000)); // Enforce 1 <= l <= 1,000,000
|
||||
di = di != null ? Math.max(1, parseInt(di)) : null; // Ensure di is positive integer or null
|
||||
ts0 = ts0 ? new Date(ts0).toISOString() : new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); // Default: 7 days ago
|
||||
ts1 = ts1 ? new Date(ts1).toISOString() : null; // Convert to ISO string or null
|
||||
geojson = geojson === 'LineString' || geojson === 'Point' ? geojson : !!geojson; // Normalize geojson
|
||||
|
||||
const client = await pool.connect();
|
||||
|
||||
// Build the WHERE clause and values array dynamically
|
||||
let whereClauses = [];
|
||||
let values = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (ts0) {
|
||||
whereClauses.push(`tstamp >= $${paramIndex++}`);
|
||||
values.push(ts0);
|
||||
}
|
||||
if (ts1) {
|
||||
whereClauses.push(`tstamp <= $${paramIndex++}`);
|
||||
values.push(ts1);
|
||||
}
|
||||
|
||||
// Add limit to values
|
||||
values.push(l);
|
||||
const limitClause = `LIMIT $${paramIndex++}`;
|
||||
|
||||
// Base query with conditional geometry selection
|
||||
let queryText = `
|
||||
SELECT
|
||||
tstamp,
|
||||
CASE
|
||||
WHEN meta->>'lineStatus' = 'offline' THEN
|
||||
ARRAY[COALESCE((meta->>'longitude')::float, ST_X(geometry)), COALESCE((meta->>'latitude')::float, ST_Y(geometry))]
|
||||
ELSE
|
||||
ARRAY[
|
||||
COALESCE(
|
||||
(meta->>'longitudeMaster')::float,
|
||||
ST_X(geometry)
|
||||
),
|
||||
COALESCE(
|
||||
(meta->>'latitudeMaster')::float,
|
||||
ST_Y(geometry)
|
||||
)
|
||||
]
|
||||
END AS coordinates,
|
||||
meta::json AS meta
|
||||
FROM public.real_time_inputs
|
||||
${whereClauses.length ? 'WHERE ' + whereClauses.join(' AND ') : ''}
|
||||
ORDER BY tstamp DESC
|
||||
${limitClause}
|
||||
`;
|
||||
|
||||
// If decimation is requested, wrap the query in a subquery with ROW_NUMBER
|
||||
if (di != null && di > 1) {
|
||||
values.push(di);
|
||||
queryText = `
|
||||
SELECT tstamp, coordinates, meta
|
||||
FROM (
|
||||
SELECT
|
||||
tstamp,
|
||||
CASE
|
||||
WHEN meta->>'lineStatus' = 'offline' THEN
|
||||
ARRAY[COALESCE((meta->>'longitude')::float, ST_X(geometry)), COALESCE((meta->>'latitude')::float, ST_Y(geometry))]
|
||||
ELSE
|
||||
ARRAY[
|
||||
COALESCE(
|
||||
(meta->>'longitudeMaster')::float,
|
||||
ST_X(geometry)
|
||||
),
|
||||
COALESCE(
|
||||
(meta->>'latitudeMaster')::float,
|
||||
ST_Y(geometry)
|
||||
)
|
||||
]
|
||||
END AS coordinates,
|
||||
meta::json AS meta,
|
||||
ROW_NUMBER() OVER (ORDER BY tstamp DESC) AS rn
|
||||
FROM public.real_time_inputs
|
||||
${whereClauses.length ? 'WHERE ' + whereClauses.join(' AND ') : ''}
|
||||
) sub
|
||||
WHERE rn % $${paramIndex} = 0
|
||||
ORDER BY tstamp DESC
|
||||
${limitClause}
|
||||
`;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await client.query(queryText, values);
|
||||
if (!res.rows?.length) {
|
||||
throw { status: 204 }; // No Content
|
||||
}
|
||||
|
||||
// Process rows: Convert tstamp to Date and extract coordinates
|
||||
let processed = res.rows.map(row => ({
|
||||
tstamp: new Date(row.tstamp),
|
||||
x: row.coordinates[0], // Longitude
|
||||
y: row.coordinates[1], // Latitude
|
||||
meta: projection != null
|
||||
? projection == ""
|
||||
? undefined
|
||||
: project([row.meta], projection)[0] : row.meta
|
||||
}));
|
||||
|
||||
// Handle geojson output formats
|
||||
if (geojson === 'LineString') {
|
||||
// Compute line length (haversine formula in JavaScript for simplicity)
|
||||
let length = 0;
|
||||
for (let i = 1; i < processed.length; i++) {
|
||||
const p1 = processed[i - 1];
|
||||
const p2 = processed[i];
|
||||
const R = 6371e3; // Earth's radius in meters
|
||||
const φ1 = p1.y * Math.PI / 180;
|
||||
const φ2 = p2.y * Math.PI / 180;
|
||||
const Δφ = (p2.y - p1.y) * Math.PI / 180;
|
||||
const Δλ = (p2.x - p1.x) * Math.PI / 180;
|
||||
const a = Math.sin(Δφ / 2) ** 2 +
|
||||
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) ** 2;
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
length += R * c;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'LineString',
|
||||
coordinates: processed.map(p => [p.x, p.y])
|
||||
},
|
||||
properties: {
|
||||
ts0: processed[processed.length - 1].tstamp.toISOString(),
|
||||
ts1: processed[0].tstamp.toISOString(),
|
||||
distance: length
|
||||
}
|
||||
};
|
||||
} else if (geojson === 'Point') {
|
||||
return {
|
||||
type: 'FeatureCollection',
|
||||
features: processed.map(p => ({
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [p.x, p.y]
|
||||
},
|
||||
properties: {
|
||||
...p.meta,
|
||||
tstamp: p.tstamp.toISOString()
|
||||
}
|
||||
}))
|
||||
};
|
||||
} else if (geojson) {
|
||||
return processed.map(p => ({
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [p.x, p.y]
|
||||
},
|
||||
properties: {
|
||||
...p.meta,
|
||||
tstamp: p.tstamp.toISOString()
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
return processed;
|
||||
}
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = get;
|
||||
5
lib/www/server/lib/db/gis/vesseltrack/index.js
Normal file
5
lib/www/server/lib/db/gis/vesseltrack/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
module.exports = {
|
||||
get: require('./get')
|
||||
};
|
||||
|
||||
94
lib/www/server/lib/db/project/clone.js
Normal file
94
lib/www/server/lib/db/project/clone.js
Normal file
@@ -0,0 +1,94 @@
|
||||
const { exec } = require('child_process');
|
||||
const util = require('util');
|
||||
const fs = require('fs');
|
||||
const execPromise = util.promisify(exec);
|
||||
const { setSurvey, pool } = require('../connection');
|
||||
|
||||
async function createProject(pid, name, src_srid, dst_srid, src_schema, dst_schema) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Determine default src_schema and src_srid
|
||||
let src_schema_val;
|
||||
let src_srid_val;
|
||||
const res = await client.query(`
|
||||
SELECT schema, meta->>'epsg' as epsg
|
||||
FROM public.projects
|
||||
ORDER BY CAST(SUBSTRING(schema FROM 8) AS INTEGER) DESC
|
||||
LIMIT 1
|
||||
`);
|
||||
if (res.rows.length === 0) {
|
||||
src_schema_val = 'survey_0';
|
||||
src_srid_val = 23031;
|
||||
} else {
|
||||
src_schema_val = res.rows[0].schema;
|
||||
src_srid_val = parseInt(res.rows[0].epsg, 10);
|
||||
}
|
||||
|
||||
// Apply parameters or defaults
|
||||
src_schema = src_schema || src_schema_val;
|
||||
src_srid = src_srid ?? src_srid_val;
|
||||
dst_srid = dst_srid ?? src_srid;
|
||||
if (dst_schema === undefined) {
|
||||
const srcNum = parseInt(src_schema.replace('survey_', ''), 10);
|
||||
dst_schema = `survey_${srcNum + 1}`;
|
||||
}
|
||||
|
||||
// Dump the source schema structure
|
||||
const pgDumpCmd = `PGPASSWORD=${pool.options.password} pg_dump --schema-only --schema=${src_schema} --host=${pool.options.host} --port=${pool.options.port} --username=${pool.options.user} ${pool.options.database}`;
|
||||
const { stdout: sqlDump } = await execPromise(pgDumpCmd);
|
||||
//fs.writeFileSync('sqlDump.sql', sqlDump);
|
||||
//console.log('Saved original SQL to sqlDump.sql');
|
||||
|
||||
// Modify the dump to use the destination schema and update SRID
|
||||
const escapedSrcSchema = src_schema.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
let modifiedSql = sqlDump
|
||||
.replace(new RegExp(`CREATE SCHEMA ${escapedSrcSchema};`, 'gi'), `CREATE SCHEMA ${dst_schema};`)
|
||||
.replace(new RegExp(`ALTER SCHEMA ${escapedSrcSchema} OWNER TO`, 'gi'), `ALTER SCHEMA ${dst_schema} OWNER TO`)
|
||||
.replace(/SELECT pg_catalog\.set_config\('search_path',\s*'', false\);/, `SELECT pg_catalog.set_config('search_path', '${dst_schema}, public', false);`)
|
||||
.replace(new RegExp(`${escapedSrcSchema}\\.`, 'g'), `${dst_schema}.`);
|
||||
|
||||
// Replace SRID in the SQL dump if src_srid !== dst_srid
|
||||
if (src_srid !== dst_srid) {
|
||||
// Replace SRID in geometry column definitions (e.g., geometry(Point, 23031))
|
||||
modifiedSql = modifiedSql.replace(
|
||||
new RegExp(`geometry\\((\\w+),\\s*${src_srid}\\s*\\)`, 'g'),
|
||||
`geometry($1, ${dst_srid})`
|
||||
);
|
||||
// Replace SRID in AddGeometryColumn calls (if used in the dump)
|
||||
modifiedSql = modifiedSql.replace(
|
||||
new RegExp(`AddGeometryColumn\\((['"]?)${escapedSrcSchema}\\1,\\s*(['"]\\w+['"]),\\s*(['"]\\w+['"]),\\s*${src_srid},`, 'g'),
|
||||
`AddGeometryColumn($1${dst_schema}$1, $2, $3, ${dst_srid},`
|
||||
);
|
||||
console.log(`Replaced SRID ${src_srid} with ${dst_srid} in SQL dump`);
|
||||
}
|
||||
|
||||
//fs.writeFileSync('modifiedSql.sql', modifiedSql);
|
||||
//console.log('Saved modified SQL to modifiedSql.sql');
|
||||
|
||||
// Execute the modified SQL to create the cloned schema
|
||||
await client.query(modifiedSql);
|
||||
console.log('Applied modified SQL successfully');
|
||||
|
||||
// Insert the new project into public.projects
|
||||
const meta = { epsg: dst_srid.toString() }; // Ensure string for JSONB
|
||||
await client.query(`
|
||||
INSERT INTO public.projects (pid, name, schema, meta)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
`, [pid, name, dst_schema, meta]);
|
||||
console.log(`Inserted project ${pid} into public.projects with schema ${dst_schema}`);
|
||||
|
||||
await client.query('COMMIT');
|
||||
console.log('Transaction committed successfully');
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error('Transaction rolled back due to error:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
console.log('Database client released');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = createProject;
|
||||
@@ -46,10 +46,6 @@ function issue (payload, req, res) {
|
||||
if (token) {
|
||||
res.set("X-JWT", token);
|
||||
const expiry = payload.exp ? (new Date(payload.exp*1000)).toUTCString() : null;
|
||||
const cookie = expiry
|
||||
? `JWT=${token}; path=/; SameSite=lax; expires=${expiry}`
|
||||
: `JWT=${token}; path=/; SameSite=lax`
|
||||
res.set("Set-Cookie", cookie); // For good measure
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"@dougal/organisations": "file:../../modules/@dougal/organisations",
|
||||
"@dougal/user": "file:../../modules/@dougal/user",
|
||||
"body-parser": "gitlab:aaltronav/contrib/expressjs/body-parser",
|
||||
"compression": "^1.8.1",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"csv": "^6.3.3",
|
||||
"d3": "^6.7.0",
|
||||
|
||||
154
package-lock.json
generated
154
package-lock.json
generated
@@ -39,10 +39,12 @@
|
||||
"dependencies": {
|
||||
"@deck.gl/aggregation-layers": "^9.1.13",
|
||||
"@deck.gl/geo-layers": "^9.1.13",
|
||||
"@deck.gl/mesh-layers": "^9.1.14",
|
||||
"@dougal/binary": "file:../../../modules/@dougal/binary",
|
||||
"@dougal/concurrency": "file:../../../modules/@dougal/concurrency",
|
||||
"@dougal/organisations": "file:../../../modules/@dougal/organisations",
|
||||
"@dougal/user": "file:../../../modules/@dougal/user",
|
||||
"@loaders.gl/obj": "^4.3.4",
|
||||
"@mdi/font": "^7.2.96",
|
||||
"buffer": "^6.0.3",
|
||||
"core-js": "^3.6.5",
|
||||
@@ -3368,14 +3370,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"lib/www/client/source/node_modules/bytes": {
|
||||
"version": "3.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"lib/www/client/source/node_modules/call-bind": {
|
||||
"version": "1.0.5",
|
||||
"dev": true,
|
||||
@@ -3709,47 +3703,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"lib/www/client/source/node_modules/compressible": {
|
||||
"version": "2.0.18",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": ">= 1.43.0 < 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"lib/www/client/source/node_modules/compression": {
|
||||
"version": "1.7.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.5",
|
||||
"bytes": "3.0.0",
|
||||
"compressible": "~2.0.16",
|
||||
"debug": "2.6.9",
|
||||
"on-headers": "~1.0.2",
|
||||
"safe-buffer": "5.1.2",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"lib/www/client/source/node_modules/compression/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"lib/www/client/source/node_modules/compression/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"lib/www/client/source/node_modules/connect-history-api-fallback": {
|
||||
"version": "2.0.0",
|
||||
"dev": true,
|
||||
@@ -6495,14 +6448,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"lib/www/client/source/node_modules/on-headers": {
|
||||
"version": "1.0.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"lib/www/client/source/node_modules/onetime": {
|
||||
"version": "5.1.2",
|
||||
"dev": true,
|
||||
@@ -8641,14 +8586,6 @@
|
||||
"spdx-expression-parse": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"lib/www/client/source/node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"lib/www/client/source/node_modules/vue": {
|
||||
"version": "2.6.14",
|
||||
"license": "MIT"
|
||||
@@ -9431,6 +9368,7 @@
|
||||
"@dougal/organisations": "file:../../modules/@dougal/organisations",
|
||||
"@dougal/user": "file:../../modules/@dougal/user",
|
||||
"body-parser": "gitlab:aaltronav/contrib/expressjs/body-parser",
|
||||
"compression": "^1.8.1",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"csv": "^6.3.3",
|
||||
"d3": "^6.7.0",
|
||||
@@ -13365,13 +13303,6 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"lib/www/server/node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"lib/www/server/node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"license": "MIT",
|
||||
@@ -13493,19 +13424,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@deck.gl/mesh-layers": {
|
||||
"version": "9.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@deck.gl/mesh-layers/-/mesh-layers-9.1.13.tgz",
|
||||
"integrity": "sha512-ujhe9FtB4qRRCXH/hY5p+IQ5VO/AC+/dtern6CTzYzjGnUnAvsbIgBZ3jxSlb1B/D3wlVE778W2cmv7MIToJJg==",
|
||||
"peer": true,
|
||||
"version": "9.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@deck.gl/mesh-layers/-/mesh-layers-9.1.14.tgz",
|
||||
"integrity": "sha512-NVUw0yG4stJfrklWCGP9j8bNlf9YQc4PccMeNNIHNrU/Je6/Va6dJZg0RGtVkeaTY1Lk3A7wRzq8/M5Urfvuiw==",
|
||||
"dependencies": {
|
||||
"@loaders.gl/gltf": "^4.2.0",
|
||||
"@luma.gl/gltf": "^9.1.5",
|
||||
"@luma.gl/shadertools": "^9.1.5"
|
||||
"@luma.gl/gltf": "~9.1.9",
|
||||
"@luma.gl/shadertools": "~9.1.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deck.gl/core": "^9.1.0",
|
||||
"@luma.gl/core": "^9.1.5",
|
||||
"@luma.gl/engine": "^9.1.5"
|
||||
"@luma.gl/core": "~9.1.9",
|
||||
"@luma.gl/engine": "~9.1.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@dougal/binary": {
|
||||
@@ -13700,6 +13630,18 @@
|
||||
"@loaders.gl/core": "^4.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@loaders.gl/obj": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@loaders.gl/obj/-/obj-4.3.4.tgz",
|
||||
"integrity": "sha512-Rdn+NHjLI0jKYrKNicJuQJohnHh7QAv4szCji8eafYYMrVtSIonNozBXUfe/c4V7HL/FVvvHCkfC66rvLvayaQ==",
|
||||
"dependencies": {
|
||||
"@loaders.gl/loader-utils": "4.3.4",
|
||||
"@loaders.gl/schema": "4.3.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@loaders.gl/core": "^4.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@loaders.gl/schema": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@loaders.gl/schema/-/schema-4.3.4.tgz",
|
||||
@@ -14444,6 +14386,34 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/compressible": {
|
||||
"version": "2.0.18",
|
||||
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
||||
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
|
||||
"dependencies": {
|
||||
"mime-db": ">= 1.43.0 < 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/compression": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
|
||||
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"compressible": "~2.0.18",
|
||||
"debug": "2.6.9",
|
||||
"negotiator": "~0.6.4",
|
||||
"on-headers": "~1.1.0",
|
||||
"safe-buffer": "5.2.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -15983,6 +15953,14 @@
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
|
||||
"integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ=="
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
|
||||
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
@@ -16090,6 +16068,14 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/on-headers": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
@@ -16713,6 +16699,14 @@
|
||||
"uuid": "bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/verror": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||
|
||||
42
sbin/get-ip.sh
Executable file
42
sbin/get-ip.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Function to get the internet IP
|
||||
get_internet_ip() {
|
||||
INTERFACE=$(ip route get 8.8.8.8 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="dev") {print $(i+1); exit}}')
|
||||
|
||||
if [ -z "$INTERFACE" ] || [ "$INTERFACE" = "lo" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
IP=$(ip -4 addr show dev "$INTERFACE" scope global | grep -oP '(?<=inet\s)\d{1,3}(\.\d{1,3}){3}(?=/)' | head -n1)
|
||||
|
||||
if [ -z "$IP" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$IP"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Get all global non-loopback IPv4 addresses
|
||||
ALL_IPS=$(ip -4 addr show scope global | grep -oP '(?<=inet\s)\d{1,3}(\.\d{1,3}){3}(?=/)')
|
||||
|
||||
ARG="${1:-local}"
|
||||
|
||||
if [ "$ARG" = "internet" ]; then
|
||||
INTERNET_IP=$(get_internet_ip)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "No valid default route or global IPv4 address found."
|
||||
exit 1
|
||||
fi
|
||||
echo "$INTERNET_IP"
|
||||
else
|
||||
# For "local" or anything else
|
||||
INTERNET_IP=$(get_internet_ip) || INTERNET_IP="" # If fails, set to empty so no exclusion
|
||||
|
||||
for IP in $ALL_IPS; do
|
||||
if [ "$IP" != "$INTERNET_IP" ]; then
|
||||
echo "$IP"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
40
sbin/update-dns.sh
Executable file
40
sbin/update-dns.sh
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Path to Nginx configuration file
|
||||
NGINX_CONFIG="/etc/nginx/vhosts.d/dougal.conf"
|
||||
|
||||
# Extract the first hostname matching 'lan.dougal' from the config
|
||||
# Assumes server_name lines like: server_name hostname1 hostname2;
|
||||
HOSTNAME=$(grep -oE '[a-zA-Z0-9.-]*lan\.dougal[a-zA-Z0-9.-]*' "$NGINX_CONFIG" | head -n 1)
|
||||
|
||||
if [ -z "$HOSTNAME" ]; then
|
||||
echo "Error: No matching hostname found in $NGINX_CONFIG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Path to IP retrieval script
|
||||
IP_SCRIPT="$HOME/software/sbin/get-ip.sh"
|
||||
|
||||
# Get the current IPv4 address
|
||||
IP_ADDRESS=$("$IP_SCRIPT" |head -n1)
|
||||
|
||||
if [ -z "$IP_ADDRESS" ]; then
|
||||
echo "Error: Failed to retrieve IP address from $IP_SCRIPT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for DYNDNS_PASSWD environment variable
|
||||
if [ -z "$DYNDNS_PASSWD" ]; then
|
||||
echo "Error: DYNDNS_PASSWD environment variable is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Hurricane Electric DynDNS update URL
|
||||
UPDATE_URL="https://dyn.dns.he.net/nic/update?hostname=$HOSTNAME&password=$DYNDNS_PASSWD&myip=$IP_ADDRESS"
|
||||
|
||||
# Send the update request and capture the response
|
||||
RESPONSE=$(curl -s "$UPDATE_URL")
|
||||
|
||||
# Output the response for logging/debugging
|
||||
echo "Update response for $HOSTNAME ($IP_ADDRESS): $RESPONSE"
|
||||
|
||||
Reference in New Issue
Block a user