diff --git a/lib/www/client/source/src/views/Plan.vue b/lib/www/client/source/src/views/Plan.vue index 698b456..62a4a80 100644 --- a/lib/www/client/source/src/views/Plan.vue +++ b/lib/www/client/source/src/views/Plan.vue @@ -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 @@ -582,92 +584,7 @@ export default { await this.api([url, init]); 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, diff --git a/lib/www/server/lib/db/plan/patch.js b/lib/www/server/lib/db/plan/patch.js index d324f14..0565e77 100644 --- a/lib/www/server/lib/db/plan/patch.js +++ b/lib/www/server/lib/db/plan/patch.js @@ -3,42 +3,221 @@ 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); + + 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 = ` - 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; - ` + // 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]); - const p = payload; // For short - const values = [ sequence, p.sequence, p.fsp, p.lsp, p.ts0, p.ts1, p.name, p.remarks, p.meta ]; - - 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]); + + } 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(); }