mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 12:17:08 +00:00
Refactor the planned lines editing logic.
We move most of the logic from the client (as it was until now) to the server. The PATCH command maintains the same format but it should provide only one of the following keys per request: * ts0 * ts1 * speed * fsp * lsp * lagAfter * sequence Earlier keys in the list above take priority over latter ones. The following keys may be provided by themselves or in combination with each other (but not with any of the above): * name * remarks * meta As a special case, an empty string as the `name` value causes the name to be auto-generated. See comments in the code `patch.js` for details on the update logic.
This commit is contained in:
@@ -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 (item[oldVal.key] != oldVal.value) {
|
||||||
if (oldVal.key == "lagAfter") {
|
if (oldVal.key == "lagAfter") {
|
||||||
// We need to shift the times for every subsequent sequence
|
// Convert from minutes to seconds
|
||||||
const delta = oldVal.value*60*1000 - this.lagAfter(item);
|
oldVal.value *= 60;
|
||||||
await this.shiftTimesAfter(item, delta);
|
|
||||||
} 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
|
||||||
@@ -583,91 +585,6 @@ export default {
|
|||||||
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,
|
||||||
|
|||||||
@@ -4,41 +4,220 @@ 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"};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate deltatime from next sequence's ts0'-ts0
|
||||||
|
// Shift all sequences > this one by deltatime
|
||||||
|
const ts0 = epoch(seq.ts1) + payload.lagAfter; // lagAfter is in seconds
|
||||||
|
deltatime = ts0 - epoch(seq1.ts0);
|
||||||
|
shiftSequences(sequence+1, deltatime, deltatime);
|
||||||
|
|
||||||
|
} else if ("sequence" in payload) {
|
||||||
|
/*
|
||||||
|
* Renumbering / reshuffling of sequences
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 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]);
|
||||||
|
|
||||||
|
|
||||||
|
} else if (["name", "remarks", "meta"].some(i => i in payload)) {
|
||||||
|
/*
|
||||||
|
* Change in various other attributes that do not affect
|
||||||
|
* other sequences
|
||||||
|
*/
|
||||||
|
|
||||||
|
// NOTE Magic! If name is empty, we generate one.
|
||||||
|
// Can be used for going back to a default name after it's been
|
||||||
|
// changed manually.
|
||||||
|
if (payload.name === "") {
|
||||||
|
payload.name = await getLineName(client, projectId, r0.rows[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change the relevant attribute
|
||||||
const text = `
|
const text = `
|
||||||
UPDATE planned_lines
|
UPDATE planned_lines
|
||||||
SET
|
SET
|
||||||
sequence = COALESCE($2, sequence),
|
name = COALESCE($2, name),
|
||||||
fsp = COALESCE($3, fsp),
|
remarks = COALESCE($3, remarks),
|
||||||
lsp = COALESCE($4, lsp),
|
meta = COALESCE($4, meta)
|
||||||
ts0 = COALESCE($5, ts0),
|
|
||||||
ts1 = COALESCE($6, ts1),
|
|
||||||
name = COALESCE($7, name),
|
|
||||||
remarks = COALESCE($8, remarks),
|
|
||||||
meta = COALESCE($9, meta)
|
|
||||||
WHERE sequence = $1;
|
WHERE sequence = $1;
|
||||||
`
|
`;
|
||||||
|
await client.query(text, [sequence, payload.name, payload.remarks, payload.meta]);
|
||||||
|
|
||||||
const p = payload; // For short
|
} else {
|
||||||
const values = [ sequence, p.sequence, p.fsp, p.lsp, p.ts0, p.ts1, p.name, p.remarks, p.meta ];
|
throw { status: 400, message: "Bad request"};
|
||||||
|
|
||||||
await client.query(text, values);
|
|
||||||
|
|
||||||
// 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;";
|
|
||||||
const res = await client.query(text, [p.sequence||sequence]);
|
|
||||||
const row = res.rows[0];
|
|
||||||
const name = await getLineName(client, projectId, row);
|
|
||||||
await client.query("UPDATE planned_lines SET name = $2 WHERE sequence = $1", [p.sequence||sequence, name]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction.commit(client);
|
transaction.commit(client);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
transaction.rollback(client);
|
transaction.rollback(client);
|
||||||
|
|
||||||
|
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;
|
throw err;
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
client.release();
|
client.release();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user