mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 10:37:07 +00:00
Merge branch '60-update-planner-as-sequences-are-shot' into 'devel'
Resolve "Update planner as sequences are shot" Closes #60 See merge request wgp/dougal/software!12
This commit is contained in:
@@ -1816,7 +1816,7 @@ ALTER TABLE ONLY _SURVEY__TEMPLATE_.planned_lines
|
|||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY _SURVEY__TEMPLATE_.planned_lines
|
ALTER TABLE ONLY _SURVEY__TEMPLATE_.planned_lines
|
||||||
ADD CONSTRAINT planned_lines_pkey PRIMARY KEY (sequence);
|
ADD CONSTRAINT planned_lines_pkey PRIMARY KEY (sequence) DEFERRABLE;
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
@@ -1940,7 +1940,7 @@ CREATE TRIGGER final_shots_tg AFTER INSERT OR DELETE OR UPDATE ON _SURVEY__TEMPL
|
|||||||
-- Name: planned_lines planned_lines_tg; Type: TRIGGER; Schema: _SURVEY__TEMPLATE_; Owner: postgres
|
-- Name: planned_lines planned_lines_tg; Type: TRIGGER; Schema: _SURVEY__TEMPLATE_; Owner: postgres
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE TRIGGER planned_lines_tg AFTER INSERT OR DELETE OR UPDATE ON _SURVEY__TEMPLATE_.planned_lines FOR EACH STATEMENT EXECUTE FUNCTION public.notify('planned_lines');
|
CREATE TRIGGER planned_lines_tg AFTER INSERT OR DELETE OR UPDATE ON _SURVEY__TEMPLATE_.planned_lines FOR EACH ROW EXECUTE FUNCTION public.notify('planned_lines');
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
|
|||||||
33
etc/db/upgrades/upgrade05-3d70a460→0983abac.sql
Normal file
33
etc/db/upgrades/upgrade05-3d70a460→0983abac.sql
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
-- Upgrade the database from commit 3d70a460 to 0983abac.
|
||||||
|
--
|
||||||
|
-- NOTE: This upgrade must be applied to every schema in the database.
|
||||||
|
-- NOTE: Each application starts a transaction, which must be committed
|
||||||
|
-- or rolled back.
|
||||||
|
--
|
||||||
|
-- This:
|
||||||
|
--
|
||||||
|
-- * makes the primary key on planned_lines deferrable; and
|
||||||
|
-- * changes the planned_lines trigger from statement to row.
|
||||||
|
--
|
||||||
|
-- To apply, run as the dougal user, for every schema in the database:
|
||||||
|
--
|
||||||
|
-- psql <<EOF
|
||||||
|
-- SET search_path TO survey_*,public;
|
||||||
|
-- \i $THIS_FILE
|
||||||
|
-- COMMIT;
|
||||||
|
-- EOF
|
||||||
|
--
|
||||||
|
-- NOTE: It can be applied multiple times without ill effect.
|
||||||
|
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE planned_lines DROP CONSTRAINT planned_lines_pkey;
|
||||||
|
ALTER TABLE planned_lines ADD CONSTRAINT planned_lines_pkey PRIMARY KEY (sequence) DEFERRABLE;
|
||||||
|
|
||||||
|
DROP TRIGGER planned_lines_tg ON planned_lines;
|
||||||
|
CREATE TRIGGER planned_lines_tg AFTER INSERT OR DELETE OR UPDATE ON planned_lines FOR EACH ROW EXECUTE FUNCTION public.notify('planned_lines');
|
||||||
|
|
||||||
|
--
|
||||||
|
--NOTE Run `COMMIT;` now if all went well
|
||||||
|
--
|
||||||
@@ -264,7 +264,7 @@ import suncalc from 'suncalc';
|
|||||||
import { mapActions, mapGetters } from 'vuex';
|
import { mapActions, mapGetters } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "LineList",
|
name: "Plan",
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
},
|
},
|
||||||
@@ -369,35 +369,37 @@ export default {
|
|||||||
if (oldVal.value === null) oldVal.value = "";
|
if (oldVal.value === null) oldVal.value = "";
|
||||||
|
|
||||||
if (item) {
|
if (item) {
|
||||||
if (oldVal.key == "lagAfter") {
|
if (item[oldVal.key] != oldVal.value) {
|
||||||
// We need to shift the times for every subsequent sequence
|
if (oldVal.key == "lagAfter") {
|
||||||
const delta = oldVal.value*60*1000 - this.lagAfter(item);
|
// Convert from minutes to seconds
|
||||||
await this.shiftTimesAfter(item, delta);
|
oldVal.value *= 60;
|
||||||
} else if (oldVal.key == "speed") {
|
} else if (oldVal.key == "speed") {
|
||||||
const v = oldVal.value*(1.852/3.6)/1000; // m/s
|
// Convert knots to metres per second
|
||||||
const ts1 = new Date(item.ts0.valueOf() + item.length / v);
|
oldVal.value = oldVal.value*(1.852/3.6);
|
||||||
const delta = ts1 - item.ts1;
|
|
||||||
await this.shiftTimesAfter(item, delta);
|
|
||||||
await this.saveItem({sequence: item.sequence, key: 'ts1', value: ts1});
|
|
||||||
} else if (oldVal.key == "sequence") {
|
|
||||||
if (this.shiftAll) {
|
|
||||||
await this.shiftSequences(oldVal.value-item.sequence);
|
|
||||||
} else {
|
|
||||||
await this.shiftSequence(item, oldVal.value);
|
|
||||||
}
|
}
|
||||||
} else if (item[oldVal.key] != oldVal.value) {
|
|
||||||
if (await this.saveItem(oldVal)) {
|
if (await this.saveItem(oldVal)) {
|
||||||
item[oldVal.key] = oldVal.value;
|
item[oldVal.key] = oldVal.value;
|
||||||
} else {
|
} else {
|
||||||
this.edit = oldVal;
|
this.edit = oldVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async serverEvent (event) {
|
async serverEvent (event) {
|
||||||
if (event.channel == "planned_lines" && event.payload.pid == this.$route.params.project) {
|
if (event.channel == "planned_lines" && event.payload.pid == this.$route.params.project) {
|
||||||
|
|
||||||
|
// Ignore non-ops
|
||||||
|
/*
|
||||||
|
if (event.payload.old === null && event.payload.new === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
if (!this.loading && !this.queuedReload) {
|
if (!this.loading && !this.queuedReload) {
|
||||||
// Do not force a non-cached response if refreshing as a result
|
// Do not force a non-cached response if refreshing as a result
|
||||||
// of an event notification. We will assume that the server has
|
// of an event notification. We will assume that the server has
|
||||||
@@ -582,92 +584,7 @@ export default {
|
|||||||
await this.api([url, init]);
|
await this.api([url, init]);
|
||||||
await this.getPlannedLines();
|
await this.getPlannedLines();
|
||||||
},
|
},
|
||||||
|
|
||||||
async shiftSequences(delta) {
|
|
||||||
const lines = delta < 0
|
|
||||||
? this.items
|
|
||||||
: [...this.items].reverse(); // We go backwards so as to avoid conflicts.
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const sequence = line.sequence+delta;
|
|
||||||
const url = `/project/${this.$route.params.project}/plan/${line.sequence}`;
|
|
||||||
const init = {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: {"Content-Type": "application/json"},
|
|
||||||
body: {sequence, name: null} // Setting name to null causes it to be regenerated
|
|
||||||
}
|
|
||||||
await this.api([url, init]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async shiftSequence (item, newSequence) {
|
|
||||||
|
|
||||||
if (item.sequence == newSequence) {
|
|
||||||
// Nothing to do
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const conflict = this.items.find(i => i.sequence == newSequence)
|
|
||||||
if (conflict) {
|
|
||||||
this.showSnack([`Sequence ${newSequence} already exists`, "error"]);
|
|
||||||
} else {
|
|
||||||
// Cannot do this check at the moment as we would have to load the list of sequences.
|
|
||||||
// TODO We will do this after refactoring.
|
|
||||||
/*
|
|
||||||
if (this.sequences.find(i => i.sequence == newSequence)) {
|
|
||||||
this.showSnack([`Sequence ${newSequence} conflicts with a line that's already been acquired`, "warning"]);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
const url = `/project/${this.$route.params.project}/plan/${item.sequence}`;
|
|
||||||
const init = {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: {"Content-Type": "application/json"},
|
|
||||||
body: {
|
|
||||||
sequence: newSequence,
|
|
||||||
name: null
|
|
||||||
} // Setting name to null causes it to be regenerated
|
|
||||||
}
|
|
||||||
await this.api([url, init]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async shiftTimesAfter(item, delta) {
|
|
||||||
const pos = this.items.indexOf(item)+1;
|
|
||||||
if (pos != 0) {
|
|
||||||
const modifiedLines = this.items.slice(pos);
|
|
||||||
if (modifiedLines.length) {
|
|
||||||
modifiedLines.reverse();
|
|
||||||
for (const line of modifiedLines) {
|
|
||||||
const ts0 = new Date(line.ts0.valueOf() + delta);
|
|
||||||
const ts1 = new Date(line.ts1.valueOf() + delta);
|
|
||||||
const url = `/project/${this.$route.params.project}/plan/${line.sequence}`;
|
|
||||||
const init = {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: {"Content-Type": "application/json"},
|
|
||||||
body: {ts1, ts0}
|
|
||||||
}
|
|
||||||
await this.api([url, init]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn("Item", item, "not found");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
editLagAfter (item) {
|
|
||||||
const pos = this.items.indexOf(item)+1;
|
|
||||||
if (pos != 0) {
|
|
||||||
if (pos < this.items.length) {
|
|
||||||
// Not last item
|
|
||||||
this.editedItems = this.items.slice(pos);
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn("Item", item, "not found");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
editItem (item, key, value) {
|
editItem (item, key, value) {
|
||||||
this.edit = {
|
this.edit = {
|
||||||
sequence: item.sequence,
|
sequence: item.sequence,
|
||||||
|
|||||||
@@ -3,42 +3,221 @@ const { getLineName } = require('./lib');
|
|||||||
|
|
||||||
async function patch (projectId, sequence, payload, opts = {}) {
|
async function patch (projectId, sequence, payload, opts = {}) {
|
||||||
const client = await setSurvey(projectId);
|
const client = await setSurvey(projectId);
|
||||||
|
|
||||||
|
sequence = Number(sequence);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Takes a Date object and returns the epoch
|
||||||
|
* in seconds
|
||||||
|
*/
|
||||||
|
function epoch (ts) {
|
||||||
|
return Number(ts)/1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Shift sequence ts0, ts1 by dt0, dt1 respectively
|
||||||
|
* for only one sequence
|
||||||
|
*/
|
||||||
|
async function shiftSequence (sequence, dt0, dt1) {
|
||||||
|
const text = `
|
||||||
|
UPDATE planned_lines
|
||||||
|
SET ts0 = ts0 + make_interval(secs => $2), ts1 = ts1 + make_interval(secs => $3)
|
||||||
|
WHERE sequence = $1
|
||||||
|
`;
|
||||||
|
|
||||||
|
return await client.query(text, [sequence, dt0, dt1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Shift sequence ts0, ts1 by dt0, dt1 respectively
|
||||||
|
* for all sequences >= sequence
|
||||||
|
*/
|
||||||
|
async function shiftSequences (sequence, dt0, dt1) {
|
||||||
|
const text = `
|
||||||
|
UPDATE planned_lines
|
||||||
|
SET ts0 = ts0 + make_interval(secs => $2), ts1 = ts1 + make_interval(secs => $3)
|
||||||
|
WHERE sequence >= $1
|
||||||
|
`;
|
||||||
|
|
||||||
|
return await client.query(text, [sequence, dt0, dt1]);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
transaction.begin(client);
|
transaction.begin(client);
|
||||||
|
|
||||||
|
let deltatime;
|
||||||
|
|
||||||
|
const r0 = await client.query("SELECT * FROM planned_lines_summary WHERE sequence >= $1 ORDER BY sequence ASC LIMIT 2;", [sequence]);
|
||||||
|
const seq = (r0?.rows || [])[0];
|
||||||
|
if (!seq || seq?.sequence != sequence) {
|
||||||
|
throw {status: 400, message: `Sequence ${sequence} does not exist`};
|
||||||
|
}
|
||||||
|
const seq1 = r0.rows[1];
|
||||||
|
const speed = seq.length/(epoch(seq.ts1)-epoch(seq.ts0)); // m/s
|
||||||
|
|
||||||
|
if ("ts0" in payload || "ts1" in payload) {
|
||||||
|
/*
|
||||||
|
* Change in start or end times
|
||||||
|
*/
|
||||||
|
|
||||||
|
deltatime = "ts0" in payload
|
||||||
|
? (epoch(new Date(payload.ts0)) - epoch(seq.ts0))
|
||||||
|
: (epoch(new Date(payload.ts1)) - epoch(seq.ts1));
|
||||||
|
// Now shift all sequences >= this one by deltatime
|
||||||
|
await shiftSequences(sequence, deltatime, deltatime);
|
||||||
|
|
||||||
|
} else if ("speed" in payload) {
|
||||||
|
/*
|
||||||
|
* Change in acquisition speed (m/s)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Check that speed is sensible
|
||||||
|
if (payload.speed < 0.1) {
|
||||||
|
throw {status: 400, message: "Speed must be at least 0.1 m/s"};
|
||||||
|
}
|
||||||
|
|
||||||
|
deltatime = epoch(seq.ts0) + (seq.length/payload.speed) - epoch(seq.ts1);
|
||||||
|
// Fix seq.ts0, shift set.ts1 += deltatime, plus all sequences > this one
|
||||||
|
await shiftSequence(sequence, 0, deltatime);
|
||||||
|
await shiftSequences(sequence+1, deltatime, deltatime);
|
||||||
|
|
||||||
|
} else if ("fsp" in payload) {
|
||||||
|
/*
|
||||||
|
* Change of FSP
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Keep ts1, adjust fsp and ts0 according to speed
|
||||||
|
// ts0' = (shot_distance * delta_shots / speed) + ts0
|
||||||
|
const sign = Math.sign(seq.lsp-seq.fsp);
|
||||||
|
const ts0 = (sign * (seq.length/seq.num_points) * (payload.fsp-seq.fsp) / speed) + epoch(seq.ts0);
|
||||||
|
const text = `
|
||||||
|
UPDATE planned_lines
|
||||||
|
SET fsp = $2, ts0 = $3
|
||||||
|
WHERE sequence = $1;
|
||||||
|
`;
|
||||||
|
await client.query(text, [sequence, payload.fsp, new Date(ts0*1000)]);
|
||||||
|
|
||||||
|
} else if ("lsp" in payload) {
|
||||||
|
/*
|
||||||
|
* Change of LSP
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Keep ts0, adjust lsp and ts1 according to speed
|
||||||
|
// Calculate deltatime from ts1'-ts1
|
||||||
|
// Shift all sequences > this one by deltatime
|
||||||
|
|
||||||
|
// deltatime = (shot_distance * delta_shots / speed)
|
||||||
|
// ts1' = deltatime + ts1
|
||||||
|
const sign = Math.sign(seq.lsp-seq.fsp);
|
||||||
|
deltatime = (sign * (seq.length/seq.num_points) * (payload.lsp-seq.lsp) / speed);
|
||||||
|
const ts1 = deltatime + epoch(seq.ts1);
|
||||||
|
const text = `
|
||||||
|
UPDATE planned_lines
|
||||||
|
SET lsp = $2, ts1 = $3
|
||||||
|
WHERE sequence = $1;
|
||||||
|
`;
|
||||||
|
await client.query(text, [sequence, payload.lsp, new Date(ts1*1000)]);
|
||||||
|
shiftSequences(sequence+1, deltatime, deltatime);
|
||||||
|
|
||||||
|
} else if ("lagAfter" in payload && seq1) {
|
||||||
|
/*
|
||||||
|
* Change of line change time
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Check that the value is sensible
|
||||||
|
if (payload.lagAfter < 0) {
|
||||||
|
throw {status: 400, message: "Line change time cannot be negative"};
|
||||||
|
}
|
||||||
|
|
||||||
const text = `
|
// Calculate deltatime from next sequence's ts0'-ts0
|
||||||
UPDATE planned_lines
|
// Shift all sequences > this one by deltatime
|
||||||
SET
|
const ts0 = epoch(seq.ts1) + payload.lagAfter; // lagAfter is in seconds
|
||||||
sequence = COALESCE($2, sequence),
|
deltatime = ts0 - epoch(seq1.ts0);
|
||||||
fsp = COALESCE($3, fsp),
|
shiftSequences(sequence+1, deltatime, deltatime);
|
||||||
lsp = COALESCE($4, lsp),
|
|
||||||
ts0 = COALESCE($5, ts0),
|
} else if ("sequence" in payload) {
|
||||||
ts1 = COALESCE($6, ts1),
|
/*
|
||||||
name = COALESCE($7, name),
|
* Renumbering / reshuffling of sequences
|
||||||
remarks = COALESCE($8, remarks),
|
*/
|
||||||
meta = COALESCE($9, meta)
|
|
||||||
WHERE sequence = $1;
|
// NOTE: This does not enforce consecutive sequences, because sometimes
|
||||||
`
|
// there is a need for those (don't ask).
|
||||||
|
|
||||||
|
// Renumber or reorder sequences
|
||||||
|
const r1 = await client.query("SELECT sequence FROM planned_lines ORDER BY sequence;");
|
||||||
|
const sequences = (r1?.rows||[]).map(i => i.sequence);
|
||||||
|
const index = sequences.indexOf(payload.sequence);
|
||||||
|
if (index != -1) {
|
||||||
|
// Make space by shifting all sequence numbers >= payload.sequence by 1
|
||||||
|
const text = `
|
||||||
|
UPDATE planned_lines
|
||||||
|
SET sequence = sequence + 1
|
||||||
|
WHERE sequence >= $1;
|
||||||
|
`;
|
||||||
|
await client.query("SET CONSTRAINTS planned_lines_pkey DEFERRED;");
|
||||||
|
await client.query(text, [payload.sequence]);
|
||||||
|
|
||||||
|
// And now we need to rename all affected lines
|
||||||
|
const r2 = await client.query("SELECT * FROM planned_lines WHERE sequence > $1 ORDER BY sequence;", [payload.sequence]);
|
||||||
|
for (let row in r2.rows) {
|
||||||
|
const name = await getLineName(client, projectId, row);
|
||||||
|
await client.query("UPDATE planned_lines SET name = $2 WHERE sequence = $1", [row.sequence, name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// Now update just this sequence
|
||||||
|
const text = `
|
||||||
|
UPDATE planned_lines
|
||||||
|
SET sequence = $2
|
||||||
|
WHERE sequence = $1;
|
||||||
|
`;
|
||||||
|
await client.query(text, [sequence, payload.sequence]);
|
||||||
|
|
||||||
|
// And rename
|
||||||
|
const r3 = await client.query("SELECT * FROM planned_lines WHERE sequence = $1 ORDER BY sequence;", [payload.sequence]);
|
||||||
|
const name = await getLineName(client, projectId, r3.rows[0]);
|
||||||
|
await client.query("UPDATE planned_lines SET name = $2 WHERE sequence = $1", [payload.sequence, name]);
|
||||||
|
|
||||||
const p = payload; // For short
|
|
||||||
const values = [ sequence, p.sequence, p.fsp, p.lsp, p.ts0, p.ts1, p.name, p.remarks, p.meta ];
|
} else if (["name", "remarks", "meta"].some(i => i in payload)) {
|
||||||
|
/*
|
||||||
await client.query(text, values);
|
* Change in various other attributes that do not affect
|
||||||
|
* other sequences
|
||||||
// Magic – if the name is (strictly) null or empty, we generate a new one
|
*/
|
||||||
if (p.name === null || p.name === "") {
|
|
||||||
const text = "SELECT * FROM planned_lines WHERE sequence = $1;";
|
// NOTE Magic! If name is empty, we generate one.
|
||||||
const res = await client.query(text, [p.sequence||sequence]);
|
// Can be used for going back to a default name after it's been
|
||||||
const row = res.rows[0];
|
// changed manually.
|
||||||
const name = await getLineName(client, projectId, row);
|
if (payload.name === "") {
|
||||||
await client.query("UPDATE planned_lines SET name = $2 WHERE sequence = $1", [p.sequence||sequence, name]);
|
payload.name = await getLineName(client, projectId, r0.rows[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change the relevant attribute
|
||||||
|
const text = `
|
||||||
|
UPDATE planned_lines
|
||||||
|
SET
|
||||||
|
name = COALESCE($2, name),
|
||||||
|
remarks = COALESCE($3, remarks),
|
||||||
|
meta = COALESCE($4, meta)
|
||||||
|
WHERE sequence = $1;
|
||||||
|
`;
|
||||||
|
await client.query(text, [sequence, payload.name, payload.remarks, payload.meta]);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw { status: 400, message: "Bad request"};
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction.commit(client);
|
transaction.commit(client);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
transaction.rollback(client);
|
transaction.rollback(client);
|
||||||
throw err;
|
|
||||||
|
if (err.code == 23503) {
|
||||||
|
if (err.constraint == "planned_lines_line_fsp_class_fkey" || err.constraint == "planned_lines_line_lsp_class_fkey") {
|
||||||
|
throw {status: 400, message: "Attempt to shoot a non-existent shotpoint"};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
client.release();
|
client.release();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user