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:
D. Berge
2021-05-27 20:17:32 +02:00
parent 534a54ef75
commit ea3e31058f
2 changed files with 225 additions and 129 deletions

View File

@@ -264,7 +264,7 @@ import suncalc from 'suncalc';
import { mapActions, mapGetters } from 'vuex';
export default {
name: "LineList",
name: "Plan",
components: {
},
@@ -369,35 +369,37 @@ export default {
if (oldVal.value === null) oldVal.value = "";
if (item) {
if (oldVal.key == "lagAfter") {
// We need to shift the times for every subsequent sequence
const delta = oldVal.value*60*1000 - this.lagAfter(item);
await this.shiftTimesAfter(item, delta);
} else if (oldVal.key == "speed") {
const v = oldVal.value*(1.852/3.6)/1000; // m/s
const ts1 = new Date(item.ts0.valueOf() + item.length / v);
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);
if (item[oldVal.key] != oldVal.value) {
if (oldVal.key == "lagAfter") {
// Convert from minutes to seconds
oldVal.value *= 60;
} else if (oldVal.key == "speed") {
// Convert knots to metres per second
oldVal.value = oldVal.value*(1.852/3.6);
}
} else if (item[oldVal.key] != oldVal.value) {
if (await this.saveItem(oldVal)) {
item[oldVal.key] = oldVal.value;
} else {
this.edit = oldVal;
}
}
}
}
},
async serverEvent (event) {
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) {
// Do not force a non-cached response if refreshing as a result
// of an event notification. We will assume that the server has
@@ -583,91 +585,6 @@ export default {
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) {
this.edit = {
sequence: item.sequence,

View File

@@ -4,41 +4,220 @@ const { getLineName } = require('./lib');
async function patch (projectId, sequence, payload, opts = {}) {
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 {
transaction.begin(client);
const text = `
UPDATE planned_lines
SET
sequence = COALESCE($2, sequence),
fsp = COALESCE($3, fsp),
lsp = COALESCE($4, lsp),
ts0 = COALESCE($5, ts0),
ts1 = COALESCE($6, ts1),
name = COALESCE($7, name),
remarks = COALESCE($8, remarks),
meta = COALESCE($9, meta)
WHERE sequence = $1;
`
let deltatime;
const p = payload; // For short
const values = [ sequence, p.sequence, p.fsp, p.lsp, p.ts0, p.ts1, p.name, p.remarks, p.meta ];
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
await client.query(text, values);
if ("ts0" in payload || "ts1" in payload) {
/*
* Change in start or end times
*/
// 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]);
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 = `
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);
} catch (err) {
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 {
client.release();
}