Clean up whitespace.

Commands used:

find . -type f -name '*.js'| while read FILE; do if echo $FILE |grep -qv node_modules; then sed -ri 's/^\s+$//' "$FILE"; fi; done
find . -type f -name '*.vue'| while read FILE; do if echo $FILE |grep -qv node_modules; then sed -ri 's/^\s+$//' "$FILE"; fi; done
find . -type f -name '*.py'| while read FILE; do if echo $FILE |grep -qv node_modules; then sed -ri 's/^\s+$//' "$FILE"; fi; done
This commit is contained in:
D. Berge
2022-04-29 14:48:21 +02:00
parent 0e534b583c
commit e3a3bdb153
58 changed files with 657 additions and 657 deletions

View File

@@ -59,7 +59,7 @@ def qc_data (cursor, prefix):
else:
print("No QC data found");
return
#print("QC", qc)
index = 0
for item in qc["results"]:

View File

@@ -39,7 +39,7 @@ def seis_data (survey):
if not pathlib.Path(pathPrefix).exists():
print(pathPrefix)
raise ValueError("Export path does not exist")
print(f"Requesting sequences for {survey['id']}")
url = f"http://localhost:3000/api/project/{survey['id']}/sequence"
r = requests.get(url)
@@ -47,12 +47,12 @@ def seis_data (survey):
for sequence in r.json():
if sequence['status'] not in ["final", "ntbp"]:
continue
filename = pathlib.Path(pathPrefix, "sequence{:0>3d}.json".format(sequence['sequence']))
if filename.exists():
print(f"Skipping export for sequence {sequence['sequence']} file already exists")
continue
print(f"Processing sequence {sequence['sequence']}")
url = f"http://localhost:3000/api/project/{survey['id']}/event?sequence={sequence['sequence']}&missing=t"
headers = { "Accept": "application/vnd.seis+json" }

View File

@@ -19,7 +19,7 @@ from datastore import Datastore
def add_pending_remark(db, sequence):
text = '<!-- @@DGL:PENDING@@ --><h4 style="color:red;cursor:help;" title="Edit the sequence file or directory name to import final data">Marked as <code>PENDING</code>.</h4><!-- @@/DGL:PENDING@@ -->\n'
with db.conn.cursor() as cursor:
qry = "SELECT remarks FROM raw_lines WHERE sequence = %s;"
cursor.execute(qry, (sequence,))
@@ -33,7 +33,7 @@ def add_pending_remark(db, sequence):
db.maybe_commit()
def del_pending_remark(db, sequence):
with db.conn.cursor() as cursor:
qry = "SELECT remarks FROM raw_lines WHERE sequence = %s;"
cursor.execute(qry, (sequence,))
@@ -89,12 +89,12 @@ if __name__ == '__main__':
pending = pendingRx.search(filepath) is not None
if not db.file_in_db(filepath):
age = time.time() - os.path.getmtime(filepath)
if age < file_min_age:
print("Skipping file because too new", filepath)
continue
print("Importing")
match = rx.match(os.path.basename(filepath))
@@ -106,7 +106,7 @@ if __name__ == '__main__':
file_info = dict(zip(pattern["captures"], match.groups()))
file_info["meta"] = {}
if pending:
print("Skipping / removing final file because marked as PENDING", filepath)
db.del_sequence_final(file_info["sequence"])

View File

@@ -51,12 +51,12 @@ if __name__ == '__main__':
print(f"Found {filepath}")
if not db.file_in_db(filepath):
age = time.time() - os.path.getmtime(filepath)
if age < file_min_age:
print("Skipping file because too new", filepath)
continue
print("Importing")
match = rx.match(os.path.basename(filepath))

View File

@@ -31,12 +31,12 @@ if __name__ == '__main__':
for file in survey["preplots"]:
print(f"Preplot: {file['path']}")
if not db.file_in_db(file["path"]):
age = time.time() - os.path.getmtime(file["path"])
if age < file_min_age:
print("Skipping file because too new", file["path"])
continue
print("Importing")
try:
preplot = preplots.from_file(file)

View File

@@ -59,12 +59,12 @@ if __name__ == '__main__':
ntbp = False
if not db.file_in_db(filepath):
age = time.time() - os.path.getmtime(filepath)
if age < file_min_age:
print("Skipping file because too new", filepath)
continue
print("Importing")
match = rx.match(os.path.basename(filepath))

View File

@@ -54,12 +54,12 @@ if __name__ == '__main__':
print(f"Found {filepath}")
if not db.file_in_db(filepath):
age = time.time() - os.path.getmtime(filepath)
if age < file_min_age:
print("Skipping file because too new", filepath)
continue
print("Importing")
match = rx.match(os.path.basename(filepath))

View File

@@ -55,12 +55,12 @@ if __name__ == '__main__':
print(f"Found {filepath}")
if not db.file_in_db(filepath):
age = time.time() - os.path.getmtime(filepath)
if age < file_min_age:
print("Skipping file because too new", filepath)
continue
print("Importing")
match = rx.match(os.path.basename(filepath))

View File

@@ -14,7 +14,7 @@ def detect_schema (conn):
if __name__ == '__main__':
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("-s", "--schema", required=False, default=None, help="survey where to insert the event")
ap.add_argument("-t", "--tstamp", required=False, default=None, help="event timestamp")
@@ -30,16 +30,16 @@ if __name__ == '__main__':
schema = args["schema"]
else:
schema = detect_schema(db.conn)
if args["tstamp"]:
tstamp = args["tstamp"]
else:
tstamp = datetime.utcnow().isoformat()
message = " ".join(args["remarks"])
print("new event:", schema, tstamp, message)
if schema and tstamp and message:
db.set_survey(schema)
with db.conn.cursor() as cursor:

View File

@@ -12,7 +12,7 @@ from parse_fwr import parse_fwr
def parse_p190_header (string):
"""Parse a generic P1/90 header record.
Returns a dictionary of fields.
"""
names = [ "record_type", "header_type", "header_type_modifier", "description", "data" ]
@@ -27,7 +27,7 @@ def parse_p190_type1 (string):
"doy", "time", "spare2" ]
record = parse_fwr(string, [1, 12, 3, 1, 1, 1, 6, 10, 11, 9, 9, 6, 3, 6, 1])
return dict(zip(names, record))
def parse_p190_rcv_group (string):
"""Parse a P1/90 Type 1 receiver group record."""
names = [ "record_type",
@@ -37,7 +37,7 @@ def parse_p190_rcv_group (string):
"streamer_id" ]
record = parse_fwr(string, [1, 4, 9, 9, 4, 4, 9, 9, 4, 4, 9, 9, 4, 1])
return dict(zip(names, record))
def parse_line (string):
type = string[0]
if string[:3] == "EOF":
@@ -52,7 +52,7 @@ def parse_line (string):
def p190_type(type, records):
return [ r for r in records if r["record_type"] == type ]
def p190_header(code, records):
return [ h for h in p190_type("H", records) if h["header_type"]+h["header_type_modifier"] == code ]
@@ -86,15 +86,15 @@ def normalise_record(record):
# These are probably strings
elif "strip" in dir(record[key]):
record[key] = record[key].strip()
return record
def normalise(records):
for record in records:
normalise_record(record)
return records
def from_file(path, only_records=None, shot_range=None, with_objrefs=False):
records = []
with open(path) as fd:
@@ -102,10 +102,10 @@ def from_file(path, only_records=None, shot_range=None, with_objrefs=False):
line = fd.readline()
while line:
cnt = cnt + 1
if line == "EOF":
break
record = parse_line(line)
if record is not None:
if only_records:
@@ -121,9 +121,9 @@ def from_file(path, only_records=None, shot_range=None, with_objrefs=False):
records.append(record)
line = fd.readline()
return records
def apply_tstamps(recordset, tstamp=None, fix_bad_seconds=False):
#print("tstamp", tstamp, type(tstamp))
if type(tstamp) is int:
@@ -161,16 +161,16 @@ def apply_tstamps(recordset, tstamp=None, fix_bad_seconds=False):
record["tstamp"] = ts
prev[object_id(record)] = doy
break
return recordset
def dms(value):
# 591544.61N
hemisphere = 1 if value[-1] in "NnEe" else -1
seconds = float(value[-6:-1])
minutes = int(value[-8:-6])
degrees = int(value[:-8])
return (degrees + minutes/60 + seconds/3600) * hemisphere
def tod(record):
@@ -183,7 +183,7 @@ def tod(record):
m = int(time[2:4])
s = float(time[4:])
return d*86400 + h*3600 + m*60 + s
def duration(record0, record1):
ts0 = tod(record0)
ts1 = tod(record1)
@@ -198,10 +198,10 @@ def azimuth(record0, record1):
x0, y0 = float(record0["easting"]), float(record0["northing"])
x1, y1 = float(record1["easting"]), float(record1["northing"])
return math.degrees(math.atan2(x1-x0, y1-y0)) % 360
def speed(record0, record1, knots=False):
scale = 3600/1852 if knots else 1
t0 = tod(record0)
t1 = tod(record1)
return (distance(record0, record1) / math.fabs(t1-t0)) * scale

View File

@@ -39,7 +39,7 @@ exportables = {
}
def primary_key (table, cursor):
# https://wiki.postgresql.org/wiki/Retrieve_primary_key_columns
qry = """
SELECT a.attname, format_type(a.atttypid, a.atttypmod) AS data_type
@@ -50,7 +50,7 @@ def primary_key (table, cursor):
WHERE i.indrelid = %s::regclass
AND i.indisprimary;
"""
cursor.execute(qry, (table,))
return cursor.fetchall()

View File

@@ -34,7 +34,7 @@ exportables = {
}
def primary_key (table, cursor):
# https://wiki.postgresql.org/wiki/Retrieve_primary_key_columns
qry = """
SELECT a.attname, format_type(a.atttypid, a.atttypmod) AS data_type
@@ -45,13 +45,13 @@ def primary_key (table, cursor):
WHERE i.indrelid = %s::regclass
AND i.indisprimary;
"""
cursor.execute(qry, (table,))
return cursor.fetchall()
def import_table(fd, table, columns, cursor):
pk = [ r[0] for r in primary_key(table, cursor) ]
# Create temporary table to import into
temptable = "import_"+table
print("Creating temporary table", temptable)
@@ -61,29 +61,29 @@ def import_table(fd, table, columns, cursor):
AS SELECT {', '.join(pk + columns)} FROM {table}
WITH NO DATA;
"""
#print(qry)
cursor.execute(qry)
# Import into the temp table
print("Import data into temporary table")
cursor.copy_from(fd, temptable)
# Update the destination table
print("Updating destination table")
setcols = ", ".join([ f"{c} = t.{c}" for c in columns ])
wherecols = " AND ".join([ f"{table}.{c} = t.{c}" for c in pk ])
qry = f"""
UPDATE {table}
SET {setcols}
FROM {temptable} t
WHERE {wherecols};
"""
#print(qry)
cursor.execute(qry)
if __name__ == '__main__':
@@ -111,7 +111,7 @@ if __name__ == '__main__':
print(f"It looks like table {table} may have already been imported. Skipping it.")
except FileNotFoundError:
print(f"File not found. Skipping {path}")
db.conn.commit()
print("Reading surveys")
@@ -130,7 +130,7 @@ if __name__ == '__main__':
columns = exportables["survey"][table]
path = os.path.join(pathPrefix, "-"+table)
print(" ←← ", path, " →→ ", table, columns)
try:
with open(path, "rb") as fd:
if columns is not None:
@@ -143,7 +143,7 @@ if __name__ == '__main__':
print(f"It looks like table {table} may have already been imported. Skipping it.")
except FileNotFoundError:
print(f"File not found. Skipping {path}")
# If we don't commit the data does not actually get copied
db.conn.commit()

View File

@@ -26,7 +26,7 @@
<style lang="stylus">
@import '../node_modules/typeface-roboto/index.css'
@import '../node_modules/@mdi/font/css/materialdesignicons.css'
.markdown.v-textarea textarea
font-family monospace
line-height 1.1 !important
@@ -66,7 +66,7 @@ export default {
snackText (newVal) {
this.snack = !!newVal;
},
snack (newVal) {
// When the snack is hidden (one way or another), clear
// the text so that if we receive the same message again

View File

@@ -11,7 +11,7 @@
<v-icon v-if="serverConnected" class="mr-6" small title="Connected to server">mdi-lan-connect</v-icon>
<v-icon v-else class="mr-6" small color="red" title="Server connection lost (we'll reconnect automatically when the server comes back)">mdi-lan-disconnect</v-icon>
<dougal-notifications-control class="mr-6"></dougal-notifications-control>
<div title="Night mode">
@@ -31,7 +31,7 @@
font-family: "Bank Gothic Medium";
src: local("Bank Gothic Medium"), url("/fonts/bank-gothic-medium.woff");
}
.brand {
font-family: "Bank Gothic Medium";
}
@@ -56,7 +56,7 @@ export default {
const date = new Date();
return date.getUTCFullYear();
},
...mapState({serverConnected: state => state.notify.serverConnected})
}
};

View File

@@ -50,7 +50,7 @@ import unpack from '@/lib/unpack.js';
export default {
name: 'DougalGraphArraysIJScatter',
props: [ "data", "settings" ],
data () {
@@ -62,15 +62,15 @@ export default {
histogram: false
};
},
computed: {
//...mapGetters(['apiUrl'])
},
watch: {
data (newVal, oldVal) {
if (newVal === null) {
this.busy = true;
@@ -79,46 +79,46 @@ export default {
this.plot();
}
},
settings () {
for (const key in this.settings) {
this[key] = this.settings[key];
}
},
histogram () {
this.plot();
this.$emit("update:settings", {[`${this.$options.name}.histogram`]: this.histogram});
},
scatterplot () {
this.plot();
this.$emit("update:settings", {[`${this.$options.name}.scatterplot`]: this.scatterplot});
}
},
methods: {
plot () {
this.plotSeries();
if (this.histogram) {
this.plotHistogram();
}
if (this.scatterplot) {
this.plotScatter();
}
},
plotSeries () {
if (!this.data) {
return;
}
function transform (d, idx=0, otherParams={}) {
const errortype = d.errorfinal ? "errorfinal" : "errorraw";
const coords = unpack(unpack(d, errortype), "coordinates");
@@ -141,7 +141,7 @@ export default {
};
return data;
}
const data = [
transform(this.data.items, 1, {
xaxis: 'x',
@@ -155,7 +155,7 @@ export default {
})
];
this.busy = false;
const layout = {
//autosize: true,
title: {text: "Inline / crossline error sequence %{meta.sequence}"},
@@ -177,25 +177,25 @@ export default {
},
meta: this.data.meta
};
const config = {
editable: false,
displaylogo: false
};
this.graph[0] = Plotly.newPlot(this.$refs.graph0, data, layout, config);
},
plotScatter () {
console.log("plot");
if (!this.data) {
console.log("missing data");
return;
}
console.log("Will plot sequence", this.data.meta.project, this.data.meta.sequence);
function transform (d) {
const errortype = d.errorfinal ? "errorfinal" : "errorraw";
const coords = unpack(unpack(d, errortype), "coordinates");
@@ -217,10 +217,10 @@ export default {
}];
return data;
}
const data = transform(this.data.items);
this.busy = false;
const layout = {
//autosize: true,
//title: {text: "Inline / crossline error sequence %{meta.sequence}"},
@@ -235,22 +235,22 @@ export default {
},
meta: this.data.meta
};
const config = {
editable: false,
displaylogo: false
};
this.graph[1] = Plotly.newPlot(this.$refs.graph1, data, layout, config);
},
plotHistogram () {
if (!this.data) {
console.log("missing data");
return;
}
function transform (d, idx=0, otherParams={}) {
const errortype = d.errorfinal ? "errorfinal" : "errorraw";
const coords = unpack(unpack(d, errortype), "coordinates");
@@ -271,7 +271,7 @@ export default {
};
return data;
}
const data = [
transform(this.data.items, 0, {
xaxis: 'x',
@@ -284,7 +284,7 @@ export default {
name: 'Inline'
})
];
const layout = {
//autosize: true,
//title: {text: "Inline / crossline error sequence %{meta.sequence}"},
@@ -308,7 +308,7 @@ export default {
},
meta: this.data.meta
};
const config = {
editable: false,
displaylogo: false
@@ -319,12 +319,12 @@ export default {
this.graph[2] = Plotly.newPlot(this.$refs.graph2, data, layout, config);
},
replot () {
if (!this.graph.length) {
return;
}
console.log("Replotting");
this.graph.forEach( (graph, idx) => {
const ref = this.$refs["graph"+idx];
@@ -334,23 +334,23 @@ export default {
});
});
},
},
async mounted () {
if (this.data) {
this.plot();
} else {
this.busy = true;
}
this.resizeObserver = new ResizeObserver(this.replot)
this.resizeObserver.observe(this.$refs.graph0);
this.resizeObserver.observe(this.$refs.graph1);
this.resizeObserver.observe(this.$refs.graph2);
},
beforeDestroy () {
if (this.resizeObserver) {
this.resizeObserver.unobserve(this.$refs.graph2);

View File

@@ -6,7 +6,7 @@
<v-switch v-model="shotpoint" label="Shotpoint"></v-switch>
<v-switch class="ml-4" v-model="violinplot" label="Violin plot"></v-switch>
</v-card-title>
<v-container fluid fill-height>
<v-row>
<v-col>
@@ -49,7 +49,7 @@ import * as aes from '@/lib/graphs/aesthetics.js';
export default {
name: 'DougalGraphGunsDepth',
props: [ "data", "settings" ],
data () {
@@ -62,16 +62,16 @@ export default {
violinplot: false
};
},
computed: {
//...mapGetters(['apiUrl'])
},
watch: {
data (newVal, oldVal) {
console.log("data changed");
if (newVal === null) {
this.busy = true;
} else {
@@ -79,42 +79,42 @@ export default {
this.plot();
}
},
settings () {
for (const key in this.settings) {
this[key] = this.settings[key];
}
},
shotpoint () {
if (this.shotpoint) {
this.replot();
}
this.$emit("update:settings", {[`${this.$options.name}.shotpoint`]: this.shotpoint});
},
violinplot () {
if (this.violinplot) {
this.plotViolin();
}
this.$emit("update:settings", {[`${this.$options.name}.violinplot`]: this.violinplot});
}
},
methods: {
plot () {
this.plotSeries();
if (this.violinplot) {
this.plotViolin();
}
},
async plotSeries () {
function transformSeries (d, src_number, otherParams={}) {
const meta = src_number
? unpack(d, "meta").filter( s => s.src_number == src_number )
: unpack(d, "meta");
@@ -122,11 +122,11 @@ export default {
const gunDepths = guns.map(s => s.map(g => g[10]));
const gunDepthsSorted = gunDepths.map(s => d3a.sort(s));
const gunsAvgDepth = gunDepths.map( (s, sidx) => d3a.mean(s) );
const x = src_number
? unpack(d.filter(s => s.meta.src_number == src_number), "point")
: unpack(d, "point");
const tracesGunDepths = [{
type: "scatter",
mode: "lines",
@@ -150,7 +150,7 @@ export default {
y: gunDepthsSorted.map(s => d3a.quantileSorted(s, 0.75)),
...aes.gunArrays[src_number || 1].max
}];
const tracesGunsDepthsIndividual = {
//name: `Array ${src_number} outliers`,
type: "scatter",
@@ -166,22 +166,22 @@ export default {
).flat(),
...aes.gunArrays[src_number || 1].out
};
const data = [ ...tracesGunDepths, tracesGunsDepthsIndividual ]
return data;
}
if (!this.data) {
console.log("missing data");
return;
}
const sources = [ ...new Set(unpack(this.data.items, "meta").map( s => s.src_number ))];
const data = sources.map( src_number => transformSeries(this.data.items, src_number) ).flat();
console.log("Sources", sources);
console.log(data);
this.busy = false;
const layout = {
//autosize: true,
title: {text: "Gun depths sequence %{meta.sequence}"},
@@ -198,12 +198,12 @@ export default {
},
meta: this.data.meta
};
const config = {
editable: false,
displaylogo: false
};
this.graph = Plotly.newPlot(this.$refs.graphSeries, data, layout, config);
this.$refs.graphSeries.on('plotly_hover', (d) => {
const point = d.points[0].x;
@@ -220,7 +220,7 @@ export default {
groups: unpack(guns, 0)
}],
}];
const layout = {
title: {text: "Gun depths shot %{meta.point}"},
height: 300,
@@ -236,19 +236,19 @@ export default {
point
}
};
const config = { displaylogo: false };
Plotly.react(this.$refs.graphBar, data, layout, config);
});
},
async plotViolin () {
function transformViolin (d, opts = {}) {
const styles = [];
unpack(unpack(d, "meta"), "guns").flat().forEach(i => {
const gunId = i[1];
const arrayId = i[2];
@@ -256,7 +256,7 @@ export default {
styles[gunId] = Object.assign({target: gunId}, aes.gunArrayViolins[arrayId]);
}
});
const data = {
type: 'violin',
x: unpack(unpack(unpack(d, "meta"), "guns").flat(), 1), // Gun number
@@ -277,21 +277,21 @@ export default {
styles: styles.filter(i => !!i)
}]
}
return data;
}
console.log("plot violin");
if (!this.data) {
console.log("missing data");
return;
}
console.log("Will plot sequence", this.data.meta.project, this.data.meta.sequence);
const data = [ transformViolin(this.data.items) ];
this.busy = false;
const layout = {
//autosize: true,
showlegend: false,
@@ -307,21 +307,21 @@ export default {
},
meta: this.data.meta
};
const config = {
editable: false,
displaylogo: false
};
this.graph = Plotly.newPlot(this.$refs.graphViolin, data, layout, config);
},
replot () {
if (!this.graph) {
return;
}
console.log("Replotting");
Object.values(this.$refs).forEach( ref => {
if (ref.data) {
@@ -333,25 +333,25 @@ export default {
}
});
},
...mapActions(["api"])
},
mounted () {
if (this.data) {
this.plot();
} else {
this.busy = true;
}
this.resizeObserver = new ResizeObserver(this.replot)
this.resizeObserver.observe(this.$refs.graphSeries);
this.resizeObserver.observe(this.$refs.graphViolin);
this.resizeObserver.observe(this.$refs.graphBar);
},
beforeDestroy () {
if (this.resizeObserver) {
this.resizeObserver.unobserve(this.$refs.graphBar);

View File

@@ -3,7 +3,7 @@
<v-card-title class="headline">
Gun details
</v-card-title>
<v-container fluid fill-height>
<v-row>
<v-col>
@@ -37,7 +37,7 @@ import * as aes from '@/lib/graphs/aesthetics.js';
export default {
name: 'DougalGraphGunsDepth',
props: [ "data" ],
data () {
@@ -54,16 +54,16 @@ export default {
]
};
},
computed: {
//...mapGetters(['apiUrl'])
},
watch: {
data (newVal, oldVal) {
console.log("data changed");
if (newVal === null) {
this.busy = true;
} else {
@@ -71,31 +71,31 @@ export default {
this.plot();
}
},
violinplot () {
if (this.violinplot) {
this.plotViolin();
}
}
},
methods: {
plot () {
this.plotHeat();
},
async plotHeat () {
if (!this.data) {
console.log("missing data");
return;
}
function transform (data, aspects=["Depth", "Pressure"]) {
const facets = [
// Mode
{
@@ -103,9 +103,9 @@ export default {
name: "Mode",
hovertemplate: "SP%{x}<br>%{y}<br>%{text}",
},
text: [ "Off", "Auto", "Manual", "Disabled" ],
conversion: (gun, shot) => {
switch (gun[3]) {
case "A":
@@ -119,16 +119,16 @@ export default {
}
}
},
// Detect
{
params: {
name: "Detect",
hovertemplate: "SP%{x}<br>%{y}<br>%{text}",
},
text: [ "Zero", "Peak", "Level" ],
conversion: (gun, shot) => {
switch (gun[4]) {
case "P":
@@ -140,41 +140,41 @@ export default {
}
}
},
// Autofire
{
params: {
name: "Autofire",
hovertemplate: "SP%{x}<br>%{y}<br>%{text}",
},
text: [ "False", "True" ],
conversion: (gun, shot) => {
return gun[5] ? 1 : 0;
}
},
// Aimpoint
{
params: {
name: "Aimpoint",
hovertemplate: "SP%{x}<br>%{y}<br>%{z} ms"
},
conversion: (gun, shot) => gun[7]
},
// Firetime
{
params: {
name: "Firetime",
hovertemplate: "SP%{x}<br>%{y}<br>%{z} ms"
},
conversion: (gun, shot) => gun[2] == shot.meta.src_number ? gun[8] : null
},
// Delta
{
params: {
@@ -187,7 +187,7 @@ export default {
zmin: -2,
zmax: 2
},
conversion: (gun, shot) => gun[2] == shot.meta.src_number ? gun[7]-gun[8] : null
},
@@ -197,7 +197,7 @@ export default {
name: "Delay",
hovertemplate: "SP%{x}<br>%{y}<br>%{z} ms"
},
conversion: (gun, shot) => gun[9]
},
@@ -207,7 +207,7 @@ export default {
name: "Depth",
hovertemplate: "SP%{x}<br>%{y}<br>%{z} m"
},
conversion: (gun, shot) => gun[10]
},
@@ -217,7 +217,7 @@ export default {
name: "Pressure",
hovertemplate: "SP%{x}<br>%{y}<br>%{z} psi"
},
conversion: (gun, shot) => gun[11]
},
@@ -227,7 +227,7 @@ export default {
name: "Volume",
hovertemplate: "SP%{x}<br>%{y}<br>%{z} in³"
},
conversion: (gun, shot) => gun[12]
},
@@ -237,14 +237,14 @@ export default {
name: "Filltime",
hovertemplate: "SP%{x}<br>%{y}<br>%{z} ms"
},
// NOTE that filltime is applicable to the *non* firing guns
conversion: (gun, shot) => gun[2] == shot.meta.src_number ? null : gun[13]
}
];
// Get gun numbers
const guns = [...new Set(data.map( s => s.meta.guns.map( g => g[1] ) ).flat())];
@@ -256,13 +256,13 @@ export default {
// ]
// }
const z = {};
// x is an array of shotpoints
const x = [];
// y is an array of gun numbers
const y = guns.map( gun => `G${gun}` );
// Build array of guns (i.e., populate z)
// We prefer to do this outside the shot-to-shot loop
// for efficiency
@@ -273,15 +273,15 @@ export default {
z[label][i] = [];
}
}
// Populate array of guns with shotpoint data
for (let shot of data) {
x.push(shot.point);
for (const facet of facets) {
const label = facet.params.name;
const facetGunsArray = z[label];
for (const gun of shot.meta.guns) {
const gunIndex = gun[1]-1;
const facetGun = facetGunsArray[gunIndex];
@@ -289,10 +289,10 @@ export default {
}
}
}
return aspects.map( (aspect, idx) => {
const facet = facets.find(el => el.params.name == aspect) || {};
const defaultParams = {
name: aspect,
type: "heatmap",
@@ -304,15 +304,15 @@ export default {
xaxis: "x",
yaxis: "y" + (idx > 0 ? idx+1 : "")
}
return Object.assign({}, defaultParams, facet.params);
});
}
const data = transform(this.data.items, this.aspects);
this.busy = false;
const layout = {
title: {text: "Gun details sequence %{meta.sequence}"},
height: 200*this.aspects.length,
@@ -327,15 +327,15 @@ export default {
*/
//autosize: true,
// colorscale: "sequential",
xaxis: {
title: "Shotpoint",
showspikes: true
},
meta: this.data.meta
};
this.aspects.forEach ( (aspect, idx) => {
const num = idx+1;
const key = "yaxis" + num;
@@ -352,21 +352,21 @@ export default {
domain
}
});
const config = {
//editable: true,
displaylogo: false
};
this.graph = Plotly.newPlot(this.$refs.graphHeat, data, layout, config);
},
replot () {
if (!this.graph) {
return;
}
console.log("Replotting");
Object.values(this.$refs).forEach( ref => {
if (ref.data) {
@@ -378,23 +378,23 @@ export default {
}
});
},
...mapActions(["api"])
},
mounted () {
if (this.data) {
this.plot();
} else {
this.busy = true;
}
this.resizeObserver = new ResizeObserver(this.replot)
this.resizeObserver.observe(this.$refs.graphHeat);
},
beforeDestroy () {
if (this.resizeObserver) {
this.resizeObserver.unobserve(this.$refs.graphHeat);

View File

@@ -6,7 +6,7 @@
<v-switch v-model="shotpoint" label="Shotpoint"></v-switch>
<v-switch class="ml-4" v-model="violinplot" label="Violin plot"></v-switch>
</v-card-title>
<v-container fluid fill-height>
<v-row>
<v-col>
@@ -49,7 +49,7 @@ import * as aes from '@/lib/graphs/aesthetics.js';
export default {
name: 'DougalGraphGunsPressure',
props: [ "data", "settings" ],
data () {
@@ -62,16 +62,16 @@ export default {
violinplot: false
};
},
computed: {
//...mapGetters(['apiUrl'])
},
watch: {
data (newVal, oldVal) {
console.log("data changed");
if (newVal === null) {
this.busy = true;
} else {
@@ -79,42 +79,42 @@ export default {
this.plot();
}
},
settings () {
for (const key in this.settings) {
this[key] = this.settings[key];
}
},
shotpoint () {
if (this.shotpoint) {
this.replot();
}
this.$emit("update:settings", {[`${this.$options.name}.shotpoint`]: this.shotpoint});
},
violinplot () {
if (this.violinplot) {
this.plotViolin();
}
this.$emit("update:settings", {[`${this.$options.name}.violinplot`]: this.violinplot});
}
},
methods: {
plot () {
this.plotSeries();
if (this.violinplot) {
this.plotViolin();
}
},
async plotSeries () {
function transformSeries (d, src_number, otherParams={}) {
const meta = src_number
? unpack(d, "meta").filter( s => s.src_number == src_number )
: unpack(d, "meta");
@@ -126,12 +126,12 @@ export default {
const gunsWeightedAvgPressure = gunPressures.map( (s, sidx) =>
d3a.sum(s.map( (pressure, gidx) => pressure * gunPressureWeights[sidx][gidx] )) / d3a.sum(gunPressureWeights[sidx])
);
const manifold = unpack(meta, "manifold");
const x = src_number
? unpack(d.filter(s => s.meta.src_number == src_number), "point")
: unpack(d, "point");
const traceManifold = {
name: "Manifold",
type: "scatter",
@@ -140,7 +140,7 @@ export default {
x,
y: manifold,
};
const tracesGunPressures = [{
type: "scatter",
mode: "lines",
@@ -164,7 +164,7 @@ export default {
y: gunPressuresSorted.map(s => d3a.quantileSorted(s, 0.75)),
...aes.gunArrays[src_number || 1].max
}];
const tracesGunsPressuresIndividual = {
//name: `Array ${src_number} outliers`,
type: "scatter",
@@ -180,22 +180,22 @@ export default {
).flat(),
...aes.gunArrays[src_number || 1].out
};
const data = [ traceManifold, ...tracesGunPressures, tracesGunsPressuresIndividual ]
return data;
}
if (!this.data) {
console.log("missing data");
return;
}
const sources = [ ...new Set(unpack(this.data.items, "meta").map( s => s.src_number ))];
const data = sources.map( src_number => transformSeries(this.data.items, src_number) ).flat();
console.log("Sources", sources);
console.log(data);
this.busy = false;
const layout = {
//autosize: true,
title: {text: "Gun pressures sequence %{meta.sequence}"},
@@ -212,12 +212,12 @@ export default {
},
meta: this.data.meta
};
const config = {
editable: false,
displaylogo: false
};
this.graph = Plotly.newPlot(this.$refs.graphSeries, data, layout, config);
this.$refs.graphSeries.on('plotly_hover', (d) => {
const point = d.points[0].x;
@@ -237,7 +237,7 @@ export default {
groups: unpack(guns, 0)
}],
}];
const layout = {
title: {text: "Gun pressures shot %{meta.point}"},
height: 300,
@@ -253,19 +253,19 @@ export default {
point
}
};
const config = { displaylogo: false };
Plotly.react(this.$refs.graphBar, data, layout, config);
});
},
async plotViolin () {
function transformViolin (d, opts = {}) {
const styles = [];
unpack(unpack(d, "meta"), "guns").flat().forEach(i => {
const gunId = i[1];
const arrayId = i[2];
@@ -273,7 +273,7 @@ export default {
styles[gunId] = Object.assign({target: gunId}, aes.gunArrayViolins[arrayId]);
}
});
const data = {
type: 'violin',
x: unpack(unpack(unpack(d, "meta"), "guns").flat(), 1), // Gun number
@@ -294,21 +294,21 @@ export default {
styles: styles.filter(i => !!i)
}]
}
return data;
}
console.log("plot violin");
if (!this.data) {
console.log("missing data");
return;
}
console.log("Will plot sequence", this.data.meta.project, this.data.meta.sequence);
const data = [ transformViolin(this.data.items) ];
this.busy = false;
const layout = {
//autosize: true,
showlegend: false,
@@ -324,21 +324,21 @@ export default {
},
meta: this.data.meta
};
const config = {
editable: false,
displaylogo: false
};
this.graph = Plotly.newPlot(this.$refs.graphViolin, data, layout, config);
},
replot () {
if (!this.graph) {
return;
}
console.log("Replotting");
Object.values(this.$refs).forEach( ref => {
if (ref.data) {
@@ -350,25 +350,25 @@ export default {
}
});
},
...mapActions(["api"])
},
mounted () {
if (this.data) {
this.plot();
} else {
this.busy = true;
}
this.resizeObserver = new ResizeObserver(this.replot)
this.resizeObserver.observe(this.$refs.graphSeries);
this.resizeObserver.observe(this.$refs.graphViolin);
this.resizeObserver.observe(this.$refs.graphBar);
},
beforeDestroy () {
if (this.resizeObserver) {
this.resizeObserver.unobserve(this.$refs.graphBar);

View File

@@ -6,7 +6,7 @@
<v-switch v-model="shotpoint" label="Shotpoint"></v-switch>
<v-switch class="ml-4" v-model="violinplot" label="Violin plot"></v-switch>
</v-card-title>
<v-container fluid fill-height>
<v-row>
<v-col>
@@ -49,7 +49,7 @@ import * as aes from '@/lib/graphs/aesthetics.js';
export default {
name: 'DougalGraphGunsTiming',
props: [ "data", "settings" ],
data () {
@@ -62,16 +62,16 @@ export default {
violinplot: false
};
},
computed: {
//...mapGetters(['apiUrl'])
},
watch: {
data (newVal, oldVal) {
console.log("data changed");
if (newVal === null) {
this.busy = true;
} else {
@@ -79,42 +79,42 @@ export default {
this.plot();
}
},
settings () {
for (const key in this.settings) {
this[key] = this.settings[key];
}
},
shotpoint () {
if (this.shotpoint) {
this.replot();
}
this.$emit("update:settings", {[`${this.$options.name}.shotpoint`]: this.shotpoint});
},
violinplot () {
if (this.violinplot) {
this.plotViolin();
}
this.$emit("update:settings", {[`${this.$options.name}.violinplot`]: this.violinplot});
}
},
methods: {
plot () {
this.plotSeries();
if (this.violinplot) {
this.plotViolin();
}
},
async plotSeries () {
function transformSeries (d, src_number, otherParams={}) {
const meta = src_number
? unpack(d, "meta").filter( s => s.src_number == src_number )
: unpack(d, "meta");
@@ -122,11 +122,11 @@ export default {
const gunTimings = guns.map(s => s.map(g => g[9]));
const gunTimingsSorted = gunTimings.map(s => d3a.sort(s));
const gunsAvgTiming = gunTimings.map( (s, sidx) => d3a.mean(s) );
const x = src_number
? unpack(d.filter(s => s.meta.src_number == src_number), "point")
: unpack(d, "point");
const tracesGunTimings = [{
type: "scatter",
mode: "lines",
@@ -150,7 +150,7 @@ export default {
y: gunTimingsSorted.map(s => d3a.quantileSorted(s, 0.75)),
...aes.gunArrays[src_number || 1].max
}];
const tracesGunsTimingsIndividual = {
//name: `Array ${src_number} outliers`,
type: "scatter",
@@ -166,22 +166,22 @@ export default {
).flat(),
...aes.gunArrays[src_number || 1].out
};
const data = [ ...tracesGunTimings, tracesGunsTimingsIndividual ]
return data;
}
if (!this.data) {
console.log("missing data");
return;
}
const sources = [ ...new Set(unpack(this.data.items, "meta").map( s => s.src_number ))];
const data = sources.map( src_number => transformSeries(this.data.items, src_number) ).flat();
console.log("Sources", sources);
console.log(data);
this.busy = false;
const layout = {
//autosize: true,
title: {text: "Gun timings sequence %{meta.sequence}"},
@@ -198,12 +198,12 @@ export default {
},
meta: this.data.meta
};
const config = {
editable: false,
displaylogo: false
};
this.graph = Plotly.newPlot(this.$refs.graphSeries, data, layout, config);
this.$refs.graphSeries.on('plotly_hover', (d) => {
const point = d.points[0].x;
@@ -220,7 +220,7 @@ export default {
groups: unpack(guns, 0)
}],
}];
const layout = {
title: {text: "Gun timings shot %{meta.point}"},
height: 300,
@@ -236,19 +236,19 @@ export default {
point
}
};
const config = { displaylogo: false };
Plotly.react(this.$refs.graphBar, data, layout, config);
});
},
async plotViolin () {
function transformViolin (d, opts = {}) {
const styles = [];
unpack(unpack(d, "meta"), "guns").flat().forEach(i => {
const gunId = i[1];
const arrayId = i[2];
@@ -256,7 +256,7 @@ export default {
styles[gunId] = Object.assign({target: gunId}, aes.gunArrayViolins[arrayId]);
}
});
const data = {
type: 'violin',
x: unpack(unpack(unpack(d, "meta"), "guns").flat(), 1), // Gun number
@@ -277,21 +277,21 @@ export default {
styles: styles.filter(i => !!i)
}]
}
return data;
}
console.log("plot violin");
if (!this.data) {
console.log("missing data");
return;
}
console.log("Will plot sequence", this.data.meta.project, this.data.meta.sequence);
const data = [ transformViolin(this.data.items) ];
this.busy = false;
const layout = {
//autosize: true,
showlegend: false,
@@ -307,21 +307,21 @@ export default {
},
meta: this.data.meta
};
const config = {
editable: false,
displaylogo: false
};
this.graph = Plotly.newPlot(this.$refs.graphViolin, data, layout, config);
},
replot () {
if (!this.graph) {
return;
}
console.log("Replotting");
Object.values(this.$refs).forEach( ref => {
if (ref.data) {
@@ -333,25 +333,25 @@ export default {
}
});
},
...mapActions(["api"])
},
mounted () {
if (this.data) {
this.plot();
} else {
this.busy = true;
}
this.resizeObserver = new ResizeObserver(this.replot)
this.resizeObserver.observe(this.$refs.graphSeries);
this.resizeObserver.observe(this.$refs.graphViolin);
this.resizeObserver.observe(this.$refs.graphBar);
},
beforeDestroy () {
if (this.resizeObserver) {
this.resizeObserver.unobserve(this.$refs.graphBar);

View File

@@ -1,21 +1,21 @@
<template>
<v-dialog v-model="open">
<template v-slot:activator="{ on, attrs }">
<v-btn icon v-bind="attrs" v-on="on" title="Configure visible aspects">
<v-icon small>mdi-wrench-outline</v-icon>
</v-btn>
</template>
<v-card>
<v-list nav subheader>
<v-subheader>Visualisations</v-subheader>
<v-list-item-group v-model="aspectsVisible" multiple>
<v-list-item value="DougalGraphGunsPressure">
<template v-slot:default="{ active }">
<v-list-item-action>
@@ -28,7 +28,7 @@
</v-list-item-content>
</template>
</v-list-item>
<v-list-item value="DougalGraphGunsTiming">
<template v-slot:default="{ active }">
<v-list-item-action>
@@ -41,7 +41,7 @@
</v-list-item-content>
</template>
</v-list-item>
<v-list-item value="DougalGraphGunsDepth">
<template v-slot:default="{ active }">
<v-list-item-action>
@@ -54,7 +54,7 @@
</v-list-item-content>
</template>
</v-list-item>
<v-list-item value="DougalGraphGunsHeatmap">
<template v-slot:default="{ active }">
<v-list-item-action>
@@ -67,7 +67,7 @@
</v-list-item-content>
</template>
</v-list-item>
<v-list-item value="DougalGraphArraysIJScatter">
<template v-slot:default="{ active }">
<v-list-item-action>
@@ -83,14 +83,14 @@
</v-list-item-group>
</v-list>
<v-divider></v-divider>
<v-card-actions>
<v-btn v-if="user" color="warning" text @click="save" :title="'Save as preference for user '+user.name+' on this computer (other users may have other defaults).'">Save as default</v-btn>
<v-spacer></v-spacer>
<v-btn color="primary" text @click="open=false">Close</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -102,20 +102,20 @@ import { mapActions, mapGetters } from 'vuex';
export default {
name: "DougalGraphSettingsSequence",
props: [
"aspects"
],
data () {
return {
open: false,
aspectsVisible: this.aspects || []
}
},
watch: {
aspects () {
// Update the aspects selection list iff the list
// is not currently open.
@@ -123,19 +123,19 @@ export default {
this.aspectsVisible = this.aspects;
}
}
},
computed: {
...mapGetters(['user', 'writeaccess', 'loading', 'serverEvent'])
},
methods: {
save () {
this.open = false;
this.$nextTick( () => this.$emit("update:aspects", {aspects: [...this.aspectsVisible]}) );
},
reset () {
this.aspectsVisible = this.aspects || [];
}

View File

@@ -32,12 +32,12 @@
min-height 16px
background-color #d3d3d314
border-radius 4px
.sequence
flex 1 1 auto
opacity 0.5
border-radius 4px
&.ntbp
background-color red
&.raw
@@ -54,13 +54,13 @@
export default {
name: 'DougalLineStatus',
props: {
preplot: Object,
sequences: Array,
"sequence-href": Function
},
methods: {
style (s) {
const values = {};
@@ -69,28 +69,28 @@ export default {
: s.status == "ntbp"
? (s.fsp_final || s.fsp)
: s.fsp; /* status == "raw" */
const lsp = s.status == "final"
? s.lsp_final
: s.status == "ntbp"
? (s.lsp_final || s.lsp)
: s.lsp; /* status == "raw" */
const pp0 = Math.min(this.preplot.fsp, this.preplot.lsp);
const pp1 = Math.max(this.preplot.fsp, this.preplot.lsp);
const len = pp1-pp0;
const sp0 = Math.max(Math.min(fsp, lsp), pp0);
const sp1 = Math.min(Math.max(fsp, lsp), pp1);
const left = (sp0-pp0)/len;
const right = 1-((sp1-pp0)/len);
values["margin-left"] = left*100 + "%";
values["margin-right"] = right*100 + "%";
return values;
},
title (s) {
const status = s.status == "final"
? "Final"
@@ -101,13 +101,13 @@ export default {
: s.status == "planned"
? "Planned"
: s.status;
const remarks = "\n"+[s.remarks, s.remarks_final].join("\n").trim()
return `Sequence ${s.sequence} ${status} (${s.fsp_final || s.fsp}${s.lsp_final || s.lsp})${remarks}`;
}
}
}
</script>

View File

@@ -12,7 +12,7 @@
<v-toolbar-title class="mx-2" @click="$router.push('/')" style="cursor: pointer;">Dougal</v-toolbar-title>
<v-spacer></v-spacer>
<v-menu bottom offset-y>
<template v-slot:activator="{on, attrs}">
<v-hover v-slot="{hover}">
@@ -29,17 +29,17 @@
</v-btn>
</v-hover>
</template>
<v-list dense>
<v-list-item :href="`/settings/equipment`">
<v-list-item-title>Equipment list</v-list-item-title>
<v-list-item-action><v-icon small>mdi-view-list</v-icon></v-list-item-action>
</v-list-item>
</v-list>
</v-menu>
<v-breadcrumbs :items="path"></v-breadcrumbs>
<template v-if="$route.name != 'Login'">

View File

@@ -1,5 +1,5 @@
export default function FormatTimestamp (str) {
const d = new Date(str);
if (isNaN(d)) {

View File

@@ -1,4 +1,4 @@
export default function unpack(rows, key) {
return rows && rows.map( row => row[key] );
};

View File

@@ -1,4 +1,4 @@
function withParentProps(item, parent, childrenKey, prop, currentValue) {
if (!Array.isArray(parent)) {
@@ -29,43 +29,43 @@ function withParentProps(item, parent, childrenKey, prop, currentValue) {
function dms (lat, lon) {
const λh = lat < 0 ? "S" : "N";
const φh = lon < 0 ? "W" : "E";
const λn = Math.abs(lat);
const φn = Math.abs(lon);
const λi = Math.trunc(λn);
const φi = Math.trunc(φn);
const λf = λn - λi;
const φf = φn - φi;
const λs = ((λf*3600)%60).toFixed(1);
const φs = ((φf*3600)%60).toFixed(1);
const λm = Math.trunc(λf*60);
const φm = Math.trunc(φf*60);
const λ =
String(λi).padStart(2, "0") + "°" +
String(λm).padStart(2, "0") + "'" +
String(λs).padStart(4, "0") + '" ' +
λh;
const φ =
String(φi).padStart(3, "0") + "°" +
String(φm).padStart(2, "0") + "'" +
String(φs).padStart(4, "0") + '" ' +
φh;
return λ+" "+φ;
}
function geometryAsString (item, opts = {}) {
const key = "key" in opts ? opts.key : "geometry";
const formatDMS = opts.dms;
let str = "";
if (key in item) {
const geometry = item[key];
if (geometry && "coordinates" in geometry) {
@@ -76,7 +76,7 @@ function geometryAsString (item, opts = {}) {
str = `${geometry.coordinates[1].toFixed(6)}, ${geometry.coordinates[0].toFixed(6)}`;
}
}
if (str) {
if (opts.url) {
if (typeof opts.url === 'string') {
@@ -88,7 +88,7 @@ function geometryAsString (item, opts = {}) {
}
}
}
return str;
}
@@ -117,10 +117,10 @@ function geometryAsString (item, opts = {}) {
* not exist or is not searched for.
*/
function preferencesλ (preferences) {
return function (key, defaults={}) {
const keys = Object.keys(preferences).filter(str => str.startsWith(key+".") || str == key);
const settings = {...defaults};
for (const str of keys) {
const k = str == key ? str : str.substring(key.length+1);
@@ -130,7 +130,7 @@ function preferencesλ (preferences) {
return settings;
}
}

View File

@@ -1,4 +1,4 @@
function setProjectId (state, pid) {
state.projectId = pid;
}

View File

@@ -1,4 +1,4 @@
function showSnack({commit}, [text, colour]) {
commit('setSnackColour', colour || 'primary');
commit('setSnackText', text);

View File

@@ -1,4 +1,4 @@
function setSnackText (state, text) {
state.snackText = text;
}

View File

@@ -21,7 +21,7 @@ async function logout ({commit, dispatch}) {
commit('setUser', null);
// Should delete JWT cookie
await dispatch('api', ["/logout"]);
// Clear preferences
commit('setPreferences', {});
}
@@ -61,16 +61,16 @@ function setCredentials ({state, commit, getters, dispatch}, force = false) {
*/
function saveUserPreference ({state, commit}, [key, value]) {
const k = `${state.user?.name}.${state.user?.role}.${key}`;
if (value !== undefined) {
localStorage.setItem(k, JSON.stringify(value));
const preferences = state.preferences;
preferences[key] = value;
commit('setPreferences', preferences);
} else {
localStorage.removeItem(k);
const preferences = state.preferences;
delete preferences[key];
commit('setPreferences', preferences);
@@ -81,7 +81,7 @@ async function loadUserPreferences ({state, commit}) {
// Get all keys which are of interest to us
const prefix = `${state.user?.name}.${state.user?.role}`;
const keys = Object.keys(localStorage).filter( k => k.startsWith(prefix) );
// Build the preferences object
const preferences = {};
keys.map(str => {
@@ -89,7 +89,7 @@ async function loadUserPreferences ({state, commit}) {
const key = str.split(".").slice(2).join(".");
preferences[key] = value;
});
// Commit it
commit('setPreferences', preferences);
}

View File

@@ -1,4 +1,4 @@
function setCookie (state, cookie) {
state.cookie = cookie;
}

View File

@@ -35,14 +35,14 @@ import { mapActions } from 'vuex';
export default {
name: "FeedViewer",
data () {
return {
timer: null,
feed: {}
}
},
methods: {
parse (text) {
const data = {items:[]};
@@ -50,13 +50,13 @@ export default {
const xml = parser.parseFromString(text, "application/xml");
const feed = xml.getElementsByTagNameNS("http://www.w3.org/2005/Atom", "feed")[0];
const entries = feed.getElementsByTagName("entry");
data.title = feed.getElementsByTagName("title")[0].childNodes[0].textContent;
data.updated = feed.getElementsByTagName("updated")[0].childNodes[0].textContent;
data.link = [...feed.getElementsByTagName("link")].filter(i =>
i.getAttribute("type") == "text/html"
).pop().getAttribute("href");
data.items = [...entries].map(entry => {
const item = {};
const link = entry.getElementsByTagName("link")[0];
@@ -70,18 +70,18 @@ export default {
}
const summaries = entry.getElementsByTagName("summary");
const summary = [...summaries].find(i => i.getAttribute("type") == "xhtml") || summaries[0];
item.summary = summary.innerHTML;
item.id = entry.getElementsByTagName("id")[0].childNodes[0].textContent;
item.title = entry.getElementsByTagName("title")[0].childNodes[0].textContent;
item.updated = entry.getElementsByTagName("updated")[0].childNodes[0].textContent;
return item;
});
return data;
},
/** Try to fix idiosyncrasies and XML bugs in the source.
*/
fixText (text) {
@@ -89,7 +89,7 @@ export default {
// element in the source.
return text.replace(/(<hr( [^>]*)?>)/g, "$1</hr>")
},
async refresh () {
const text = await this.api([`/rss/?remote=${atob(this.$route.params.source)}`, {text:true}]);
try {
@@ -100,15 +100,15 @@ export default {
this.feed = this.parse(this.fixText(text));
}
},
...mapActions(["api"])
},
async mounted () {
await this.refresh();
this.timer = setInterval(this.refresh, 300000);
},
unmounted () {
cancelInterval(this.timer);
this.timer = null;

View File

@@ -15,7 +15,7 @@
</v-toolbar>
</v-card-title>
<v-card-text>
<v-menu v-if="writeaccess"
v-model="contextMenuShow"
:position-x="contextMenuX"
@@ -63,7 +63,7 @@
</v-list-item>
</v-list>
</v-menu>
<v-dialog
v-model="colourPickerShow"
max-width="300"
@@ -118,7 +118,7 @@
@click:row="setActiveItem"
@contextmenu:row="contextMenu"
>
<template v-slot:item.status="{item}">
<dougal-line-status
:preplot="item"
@@ -143,7 +143,7 @@
<template v-slot:item.azimuth="props">
<span>{{ props.value.toFixed(2) }} °</span>
</template>
<template v-slot:item.remarks="{item}">
<v-text-field v-if="edit && edit.line == item.line && edit.key == 'remarks'"
type="text"
@@ -169,9 +169,9 @@
</div>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-container>
@@ -192,7 +192,7 @@ import DougalLineStatus from '@/components/line-status';
export default {
name: "LineList",
components: {
DougalLineStatus
},
@@ -258,13 +258,13 @@ export default {
edit: null, // {line, key, value}
queuedReload: false,
itemsPerPage: 25,
// Context menu stuff
contextMenuShow: false,
contextMenuX: 0,
contextMenuY: 0,
contextMenuItem: null,
// Colour picker stuff
colourPickerShow: false,
selectedColour: null,
@@ -275,17 +275,17 @@ export default {
computed: {
...mapGetters(['user', 'writeaccess', 'loading', 'serverEvent'])
},
watch: {
async edit (newVal, oldVal) {
if (newVal === null && oldVal !== null) {
const item = this.items.find(i => i.line == oldVal.line);
// Get around this Vuetify feature
// https://github.com/vuetifyjs/vuetify/issues/4144
if (oldVal.value === null) oldVal.value = "";
if (item && item[oldVal.key] != oldVal.value) {
if (await this.saveItem(oldVal)) {
item[oldVal.key] = oldVal.value;
@@ -317,51 +317,51 @@ export default {
}
}
},
queuedReload (newVal, oldVal) {
if (newVal && !oldVal && !this.loading) {
this.getLines();
this.getSequences();
}
},
loading (newVal, oldVal) {
if (!newVal && oldVal && this.queuedReload) {
this.getLines();
this.getSequences();
}
},
itemsPerPage (newVal, oldVal) {
localStorage.setItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/items-per-page`, newVal);
},
user (newVal, oldVal) {
this.itemsPerPage = Number(localStorage.getItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/items-per-page`)) || 25;
}
},
methods: {
itemClass (item) {
const colourClass = item.meta.colour ? "bg-clr-"+item.meta.colour.slice(1) : null;
if (colourClass && ![...this.styles.cssRules].some(i => i.selectorText == "."+colourClass)) {
const rule = `.${colourClass} { background-color: ${item.meta.colour}; }`;
this.styles.insertRule(rule);
}
return [
item.meta.colour ? colourClass : "",
(this.activeItem == item && !this.edit) ? 'blue accent-1 elevation-3' : ''
];
},
isPlanned(item) {
return this.sequences.find(i => i.line == item.line && i.status == 'planned');
},
contextMenu (e, {item}) {
e.preventDefault();
this.contextMenuShow = false;
@@ -370,7 +370,7 @@ export default {
this.contextMenuItem = item;
this.$nextTick( () => this.contextMenuShow = true );
},
setNTBA () {
this.removeFromPlan();
this.saveItem({
@@ -379,7 +379,7 @@ export default {
value: !this.contextMenuItem.ntba
})
},
setComplete () {
this.saveItem({
line: this.contextMenuItem.line,
@@ -414,21 +414,21 @@ export default {
await this.api([url, init]);
}
},
showLineColourDialog () {
this.selectedColour = this.contextMenuItem.meta.colour
? {hexa: this.contextMenuItem.meta.colour}
: null;
this.colourPickerShow = true;
},
setLineColour () {
const items = this.selectOn ? this.selectedRows : [ this.contextMenuItem ];
const colour = this.selectedColour ? this.selectedColour.hex+"80" : null;
this.selectedRows = [];
this.selectOn = false;
for (const item of items) {
if (colour) {
item.meta.colour = colour;
@@ -439,7 +439,7 @@ export default {
this.colourPickerShow = false;
}
},
editItem (item, key) {
this.edit = {
line: item.line,
@@ -447,10 +447,10 @@ export default {
value: item[key]
}
},
async saveItem (edit) {
if (!edit) return;
try {
const url = `/project/${this.$route.params.project}/line/${edit.line}`;
const init = {
@@ -459,7 +459,7 @@ export default {
[edit.key]: edit.value
}
};
let res;
await this.api([url, init, (e, r) => res = r]);
return res && res.ok;
@@ -481,11 +481,11 @@ export default {
this.items = await this.api([url]) || [];
},
async getSequences () {
const urlS = `/project/${this.$route.params.project}/sequence`;
this.sequences = await this.api([urlS]) || [];
const urlP = `/project/${this.$route.params.project}/plan`;
const planned = await this.api([urlP]) || [];
planned.forEach(i => i.status = "planned");
@@ -505,7 +505,7 @@ export default {
this.getLines();
this.getNumLines();
this.getSequences();
// Initialise stylesheet
const el = document.createElement("style");
document.head.appendChild(el);

View File

@@ -165,15 +165,15 @@ const layers = {
onEachFeature (feature, layer) {
const p = feature.properties;
if (feature.geometry) {
const ntbp = p.ntbp
? " <b>(NTBP)</b>"
: "";
const remarks = p.remarks
? "<hr/>"+markdown(p.remarks)
: "";
const popup = feature.geometry.type == "Point"
? `Raw sequence ${feature.properties.sequence}${ntbp}<br/>Point <b>${feature.properties.line} / ${feature.properties.point}</b><br/>${feature.properties.objref}<br/>${feature.properties.tstamp}`
: `Raw sequence ${p.sequence}${ntbp}<br/>
@@ -183,7 +183,7 @@ const layers = {
${p.duration}<br/>
<table><tr><td><b>${p.fsp}</b></td><td>@ ${ftstamp(p.ts0)}</td></tr><tr><td><b>${p.lsp}</b></td><td>@ ${ftstamp(p.ts1)}</td></tr></table>${remarks}`;
layer.bindTooltip(popup, {sticky: true});
}
}
}),
@@ -204,7 +204,7 @@ const layers = {
},
onEachFeature (feature, layer) {
const p = feature.properties;
const remarks = p.remarks
? "<hr/>"+markdown(p.remarks)
: "";
@@ -221,9 +221,9 @@ const layers = {
layer.bindTooltip(popup, {sticky: true});
}
}),
"Events (QC)": L.geoJSON(null),
"Events (Other)": L.geoJSON(null),
"Real-time": L.realtime({
@@ -394,7 +394,7 @@ export default {
}
}
},
user (newVal, oldVal) {
if (newVal && (!oldVal || newVal.name != oldVal.name)) {
this.initView();
@@ -416,7 +416,7 @@ export default {
//console.log("EVENT", event);
}
},
$route (to, from) {
if (to.name == "map") {
this.setHashMarker();
@@ -431,13 +431,13 @@ export default {
const bbox = new L.GeoJSON(res);
map.fitBounds(bbox.getBounds());
},
getEvents (ffn = i => true) {
return async (success, error) => {
const url = `/project/${this.$route.params.project}/event`;
const data = await this.api([url, {headers: {"Accept": "application/geo+json"}}]);
if (data) {
function colour(feature) {
if (feature && feature.properties && feature.properties.type) {
if (feature.properties.type == "qc") {
@@ -452,7 +452,7 @@ export default {
}
return "brown";
}
const features = data.filter(ffn).map(feature => {
feature.properties.colour = colour(feature);
return feature;
@@ -480,15 +480,15 @@ export default {
for (const l of this.layerRefreshConfig.filter(i => !layerset || layerset.includes(i.layer))) {
if (map.hasLayer(l.layer)) {
const url = l.url(query);
// Skip unnecessary requests
if (url == l.layer.lastRequestURL) continue;
if (l.layer.abort && l.layer.abort instanceof AbortController) {
l.layer.abort.abort();
}
l.layer.abort = new AbortController();
const signal = l.layer.abort.signal;
const init = {
@@ -497,7 +497,7 @@ export default {
Accept: "application/geo+json"
}
};
// Firing all refresh events asynchronously, which is OK provided
// we don't have hundreds of layers to be refreshed.
this.api([url, init])
@@ -505,11 +505,11 @@ export default {
if (!layer) {
return;
}
if (typeof l.transform == 'function') {
layer = l.transform(layer);
}
l.layer.clearLayers();
if (layer instanceof L.Layer || (layer.features && layer.features.length < limit) || ("length" in layer && layer.length < limit)) {
if (l.layer.addData) {
@@ -517,7 +517,7 @@ export default {
} else if (l.layer.addLayer) {
l.layer.addLayer(layer);
}
l.layer.lastRequestURL = url;
} else {
console.warn("Too much data from", url);
@@ -551,7 +551,7 @@ export default {
} else {
value = `${zoom}/${lat}/${lng}`;
}
if (value) {
localStorage.setItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/view`, value);
}
@@ -559,11 +559,11 @@ export default {
decodeURL () {
const value = localStorage.getItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/view`);
if (!value) {
return {};
}
const parts = value.split(":");
const activeOverlays = parts.length > 1 && parts[1].split(";");
const activeLayers = parts.length > 2 && parts[2].split(";");
@@ -574,19 +574,19 @@ export default {
return {position, activeOverlays, activeLayers};
},
initView () {
if (!map) {
return;
}
map.off('overlayadd', this.updateURL);
map.off('overlayremove', this.updateURL);
map.off('layeradd', this.updateURL);
map.off('layerremove', this.updateURL);
const init = this.decodeURL();
if (init.activeOverlays) {
Object.keys(tileMaps).forEach(k => {
const l = tileMaps[k];
@@ -621,16 +621,16 @@ export default {
if (init.position) {
map.setView(init.position.slice(1), init.position[0]);
}
map.on('overlayadd', this.updateURL);
map.on('overlayremove', this.updateURL);
map.on('layeradd', this.updateURL);
map.on('layerremove', this.updateURL);
},
setHashMarker () {
const crosshairsMarkerIcon = L.divIcon({
iconSize: [20, 20],
iconAnchor: [10, 10],
@@ -643,7 +643,7 @@ export default {
</svg>
`
});
const updateMarker = (latlng) => {
if (this.hashMarker) {
if (latlng) {
@@ -657,7 +657,7 @@ export default {
this.hashMarker.addTo(map).getElement().style.fill = "fuchsia";
}
}
const parts = document.location.hash.substring(1).split(":")[0].split("/").map(p => decodeURIComponent(p));
if (parts.length == 3) {
setTimeout(() => map.setView(parts.slice(1).reverse(), parts[0]), 500);
@@ -677,7 +677,7 @@ export default {
mounted () {
map = L.map('map', {maxZoom: 22});
const eventsOptions = () => {
return {
start: false,
@@ -703,7 +703,7 @@ export default {
}
}
};
layers["Events (QC)"] = L.realtime(this.getEvents(i => i.properties.type == "qc"), eventsOptions());
layers["Events (Other)"] = L.realtime(this.getEvents(i => i.properties.type != "qc"), eventsOptions());
@@ -729,7 +729,7 @@ export default {
//console.log("Events (Other) remove event", e);
});
const init = this.decodeURL();
if (init.activeOverlays) {
@@ -828,7 +828,7 @@ export default {
});
(new LoadingControl({position: "bottomright"})).addTo(map);
// Decode a position if one given in the hash
this.setHashMarker();
}

View File

@@ -4,7 +4,7 @@
<v-card-title>
<v-toolbar flat>
<v-toolbar-title>Plan</v-toolbar-title>
<v-menu v-if="items">
<template v-slot:activator="{on, attrs}">
<v-btn class="ml-5" small v-on="on" v-bind="attrs">
@@ -12,7 +12,7 @@
<v-icon right small>mdi-cloud-download</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item
:href="`/api/project/${$route.params.project}/plan/?mime=text%2Fcsv&download`"
@@ -36,7 +36,7 @@
>PDF</v-list-item>
</v-list>
</v-menu>
<v-spacer></v-spacer>
<v-text-field
v-model="filter"
@@ -48,7 +48,7 @@
</v-toolbar>
</v-card-title>
<v-card-text>
<v-menu v-if="writeaccess"
v-model="contextMenuShow"
:position-x="contextMenuX"
@@ -63,7 +63,7 @@
</v-list-item>
</v-list>
</v-menu>
<v-card class="mb-5" flat>
<v-card-title class="text-overline">
Comments
@@ -77,7 +77,7 @@
>
<v-icon small>mdi-square-edit-outline</v-icon>
</v-btn>
<v-btn v-else
class="ml-3"
small
@@ -89,7 +89,7 @@
</v-btn>
</template>
</v-card-title>
<v-card-text v-if="editRemarks">
<v-textarea
v-model="remarks"
@@ -100,9 +100,9 @@
rows="1"
></v-textarea>
</v-card-text>
<v-card-text v-else v-html="$options.filters.markdown(remarks || '*(nil)*')"></v-card-text>
</v-card>
<v-data-table
@@ -121,7 +121,7 @@
<template v-slot:item.srss="{item}">
<v-icon small :title="srssInfo(item)">{{srssIcon(item)}}</v-icon>
</template>
<template v-slot:item.sequence="{item, value}">
<v-edit-dialog v-if="writeaccess"
large
@@ -253,7 +253,7 @@
<template v-slot:item.azimuth="props">
<span style="white-space:nowrap;">{{ props.value.toFixed(2) }} °</span>
</template>
<template v-slot:item.remarks="{item}">
<v-text-field v-if="writeaccess && edit && edit.sequence == item.sequence && edit.key == 'remarks'"
type="text"
@@ -322,9 +322,9 @@
</v-edit-dialog>
<span v-else>{{ Math.round(lagAfter(item) / (60*1000)) }} min</span>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-container>
@@ -339,7 +339,7 @@ import { mapActions, mapGetters } from 'vuex';
export default {
name: "Plan",
components: {
},
@@ -421,7 +421,7 @@ export default {
plannerConfig: null,
shiftAll: false, // Shift all sequences checkbox
// Context menu stuff
contextMenuShow: false,
contextMenuX: 0,
@@ -433,17 +433,17 @@ export default {
computed: {
...mapGetters(['user', 'writeaccess', 'loading', 'serverEvent'])
},
watch: {
async edit (newVal, oldVal) {
if (newVal === null && oldVal !== null) {
const item = this.items.find(i => i.sequence == oldVal.sequence);
// Get around this Vuetify feature
// https://github.com/vuetifyjs/vuetify/issues/4144
if (oldVal.value === null) oldVal.value = "";
if (item) {
if (item[oldVal.key] != oldVal.value) {
if (oldVal.key == "lagAfter") {
@@ -453,29 +453,29 @@ export default {
// Convert knots to metres per second
oldVal.value = oldVal.value*(1.852/3.6);
}
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
@@ -491,34 +491,34 @@ export default {
}
}
},
queuedReload (newVal, oldVal) {
if (newVal && !oldVal && !this.loading) {
this.getPlannedLines();
}
},
loading (newVal, oldVal) {
if (!newVal && oldVal && this.queuedReload) {
this.getPlannedLines();
}
},
itemsPerPage (newVal, oldVal) {
localStorage.setItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/items-per-page`, newVal);
},
user (newVal, oldVal) {
this.itemsPerPage = Number(localStorage.getItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/items-per-page`)) || 25;
}
},
methods: {
suntimes (line) {
const oneday = 86400000;
function isDay (srss, ts, lat, lng) {
if (isNaN(srss.sunriseEnd) || isNaN(srss.sunsetStart)) {
// Between March and September
@@ -541,31 +541,31 @@ export default {
}
}
}
let {ts0, ts1} = line;
const [ lng0, lat0 ] = line.geometry.coordinates[0];
const [ lng1, lat1 ] = line.geometry.coordinates[1];
if (ts1-ts0 > oneday) {
console.warn("Cannot provide reliable sunrise / sunset times for lines over 24 hr in this version");
//return null;
}
const srss0 = suncalc.getTimes(ts0, lat0, lng0);
const srss1 = suncalc.getTimes(ts1, lat1, lng1);
srss0.prevDay = suncalc.getTimes(new Date(ts0.valueOf()-oneday), lat0, lng0);
srss1.nextDay = suncalc.getTimes(new Date(ts1.valueOf()+oneday), lat1, lng1);
srss0.isDay = isDay(srss0, ts0, lat0, lng0);
srss1.isDay = isDay(srss1, ts1, lat1, lng1);
return {
ts0: srss0,
ts1: srss1
};
},
srssIcon (line) {
const srss = this.suntimes(line);
const moon = suncalc.getMoonIllumination(line.ts0);
@@ -585,7 +585,7 @@ export default {
: 'mdi-moon-waning-crescent'
: 'mdi-theme-light-dark';
},
srssMoonPhase (line) {
const ts = new Date((Number(line.ts0)+Number(line.ts1))/2);
const moon = suncalc.getMoonIllumination(ts);
@@ -601,11 +601,11 @@ export default {
? 'Waning gibbous moon'
: 'Waning crescent moon';
},
srssInfo (line) {
const srss = this.suntimes(line);
const text = [];
try {
text.push(`Sunset at\t${srss.ts0.prevDay.sunset.toISOString().substr(0, 16)}Z (FSP)`);
text.push(`Sunrise at\t${srss.ts0.sunrise.toISOString().substr(0, 16)}Z (FSP)`);
@@ -622,11 +622,11 @@ export default {
console.log("ERROR", err);
}
}
if (!srss.ts0.isDay || !srss.ts1.isDay) {
text.push(this.srssMoonPhase(line));
}
return text.join("\n");
},
@@ -647,7 +647,7 @@ export default {
const v = item.length / ((item.ts1-item.ts0)/1000); // m/s
return v*3.6/1.852;
},
contextMenu (e, {item}) {
e.preventDefault();
this.contextMenuShow = false;
@@ -656,7 +656,7 @@ export default {
this.contextMenuItem = item;
this.$nextTick( () => this.contextMenuShow = true );
},
async deletePlannedSequence () {
console.log("Delete sequence", this.contextMenuItem.sequence);
const url = `/project/${this.$route.params.project}/plan/${this.contextMenuItem.sequence}`;
@@ -664,7 +664,7 @@ export default {
await this.api([url, init]);
await this.getPlannedLines();
},
editItem (item, key, value) {
this.edit = {
sequence: item.sequence,
@@ -672,10 +672,10 @@ export default {
value: value === undefined ? item[key] : value
}
},
async saveItem (edit) {
if (!edit) return;
try {
const url = `/project/${this.$route.params.project}/plan/${edit.sequence}`;
const init = {
@@ -684,7 +684,7 @@ export default {
[edit.key]: edit.value
}
};
let res;
await this.api([url, init, (e, r) => res = r]);
return res && res.ok;
@@ -692,7 +692,7 @@ export default {
return false;
}
},
async saveRemarks () {
const url = `/project/${this.$route.params.project}/info/plan/remarks`;
let res;
@@ -735,12 +735,12 @@ export default {
"defaultLineChangeDuration": 36
}
},
async getPlannerRemarks () {
const url = `/project/${this.$route.params.project}/info/plan/remarks`;
this.remarks = await this.api([url]) || "";
},
async getSequences () {
const url = `/project/${this.$route.params.project}/sequence`;
this.sequences = await this.api([url]) || [];

View File

@@ -13,9 +13,9 @@ module.exports = async function (req, res, next) {
"text/html": html,
"application/pdf": pdf
};
const mimetype = (handlers[req.query.mime] && req.query.mime) || req.accepts(Object.keys(handlers));
if (mimetype) {
res.set("Content-Type", mimetype);
await handlers[mimetype](req, res, next);

View File

@@ -4,13 +4,13 @@ const { plan } = require('../../../../lib/db');
const json = async function (req, res, next) {
try {
const response = await plan.list(req.params.project, req.query);
if ("download" in req.query || "d" in req.query) {
const extension = "html";
const filename = `${req.params.project.toUpperCase()}-Plan.${extension}`;
res.set("Content-Disposition", `attachment; filename="${filename}"`);
}
const transforms = (i) => {
i.lon0 = Number(((i?.geometry?.coordinates||[])[0]||[])[0]).toFixed(6)*1;
i.lat0 = Number(((i?.geometry?.coordinates||[])[0]||[])[1]).toFixed(6)*1;
@@ -22,14 +22,14 @@ const json = async function (req, res, next) {
delete i.meta;
return i;
};
const csv = new AsyncParser({transforms}, {objectMode: true});
csv.processor.on('error', (err) => { throw err; });
csv.processor.on('end', () => {
res.end();
next();
});
res.status(200);
csv.processor.pipe(res);
response.forEach(row => csv.input.push(row));

View File

@@ -20,10 +20,10 @@ const html = async function (req, res, next) {
delete feature.properties.geometry;
return feature;
});
// const template = (await configuration.get(req.params.project, "sse/templates/0/template")) || defaultTemplatePath;
const template = defaultTemplatePath;
const mapConfig = {
size: { width: 500, height: 500 },
layers: [
@@ -52,18 +52,18 @@ const html = async function (req, res, next) {
}
]
}
const map = leafletMap(mapConfig);
const data = {
projectId: req.params.project,
info: planInfo,
lines,
map: await map.getImageData()
}
const response = await render(data, template);
if ("download" in req.query || "d" in req.query) {
const extension = "html";
const filename = `${req.params.project.toUpperCase()}-Plan.${extension}`;

View File

@@ -13,9 +13,9 @@ module.exports = async function (req, res, next) {
"text/html": html,
"application/pdf": pdf
};
const mimetype = (handlers[req.query.mime] && req.query.mime) || req.accepts(Object.keys(handlers));
if (mimetype) {
res.set("Content-Type", mimetype);
await handlers[mimetype](req, res, next);

View File

@@ -31,8 +31,8 @@ const pdf = async function (req, res, next) {
});
// const template = (await configuration.get(req.params.project, "sse/templates/0/template")) || defaultTemplatePath;
const template = defaultTemplatePath;
const mapConfig = {
size: { width: 500, height: 500 },
layers: [
@@ -61,21 +61,21 @@ const pdf = async function (req, res, next) {
}
]
}
const map = leafletMap(mapConfig);
const data = {
projectId: req.params.project,
info: planInfo,
lines,
map: await map.getImageData()
}
const html = await render(data, template);
await fs.writeFile(fname, html);
const pdf = Buffer.from(await url2pdf("file://"+fname), "base64");
if ("download" in req.query || "d" in req.query) {
const extension = "pdf";
const filename = `${req.params.project.toUpperCase()}-Plan.${extension}`;

View File

@@ -6,7 +6,7 @@ module.exports = async function (req, res, next) {
if (req.query.remote) {
// We're being asked to fetch a remote feed
// NOTE: No, we don't limit what feeds the user can fetch
const r = await fetch(req.query.remote);
if (r && r.ok) {
res.set("Content-Type", "application/xml");

View File

@@ -6,7 +6,7 @@ module.exports = async function (req, res, next) {
try {
const json = await sequence.get(req.params.project, req.params.sequence, req.query);
const geometry = req.query.geometry || "geometrypreplot";
const geojson = {
type: "FeatureCollection",
features: json.map(feature => {
@@ -17,7 +17,7 @@ module.exports = async function (req, res, next) {
}
})
};
res.status(200).send(geojson);
next();
} catch (err) {

View File

@@ -7,9 +7,9 @@ module.exports = async function (req, res, next) {
"application/json": json,
"application/geo+json": geojson,
};
const mimetype = (handlers[req.query.mime] && req.query.mime) || req.accepts(Object.keys(handlers));
if (mimetype) {
res.set("Content-Type", mimetype);
await handlers[mimetype](req, res, next);

View File

@@ -7,9 +7,9 @@ module.exports = async function (req, res, next) {
"application/json": json,
"application/geo+json": geojson,
};
const mimetype = (handlers[req.query.mime] && req.query.mime) || req.accepts(Object.keys(handlers));
if (mimetype) {
res.set("Content-Type", mimetype);
await handlers[mimetype](req, res, next);

View File

@@ -22,37 +22,37 @@ class DetectSOLEOL {
* first element to a falsy value, thus bootstrapping the process.
*/
static MAX_QUEUE_SIZE = 125000;
queue = [];
async processQueue () {
while (this.queue.length > 1) {
if (this.queue[0].isPending) {
setImmediate(() => this.processQueue());
return;
}
const prev = this.queue.shift();
const cur = this.queue[0];
const sequence = Number(cur._sequence);
try {
if (prev.lineName == cur.lineName && prev._sequence == cur._sequence &&
prev.lineStatus != "online" && cur.lineStatus == "online" && sequence) {
// console.log("TRANSITION TO ONLINE", prev, cur);
// Check if there are already FSP, FGSP events for this sequence
const projectId = await schema2pid(cur._schema);
const sequenceEvents = await event.list(projectId, {sequence});
const labels = ["FSP", "FGSP"].filter(l => !sequenceEvents.find(i => i.labels.includes(l)));
if (labels.includes("FSP")) {
// At this point labels contains either FSP only or FSP + FGSP,
// depending on whether a FGSP event has already been entered.
const remarks = `SEQ ${cur._sequence}, SOL ${cur.lineName}, BSP: ${(cur.speed*3.6/1.852).toFixed(1)} kt, Water depth: ${Number(cur.waterDepth).toFixed(0)} m.`;
const payload = {
type: "sequence",
@@ -61,7 +61,7 @@ class DetectSOLEOL {
remarks,
labels
}
// console.log(projectId, payload);
await event.post(projectId, payload);
} else {
@@ -70,17 +70,17 @@ class DetectSOLEOL {
}
} else if (prev.lineStatus == "online" && cur.lineStatus != "online") {
// console.log("TRANSITION TO OFFLINE", prev, cur);
// Check if there are already LSP, LGSP events for this sequence
const projectId = await schema2pid(prev._schema);
const sequenceEvents = await event.list(projectId, {sequence});
const labels = ["LSP", "LGSP"].filter(l => !sequenceEvents.find(i => i.labels.includes(l)));
if (labels.includes("LSP")) {
// At this point labels contains either LSP only or LSP + LGSP,
// depending on whether a LGSP event has already been entered.
const remarks = `SEQ ${prev._sequence}, EOL ${prev.lineName}, BSP: ${(prev.speed*3.6/1.852).toFixed(1)} kt, Water depth: ${Number(prev.waterDepth).toFixed(0)} m.`;
const payload = {
type: "sequence",
@@ -89,7 +89,7 @@ class DetectSOLEOL {
remarks,
labels
}
// console.log(projectId, payload);
await event.post(projectId, payload);
} else {
@@ -107,7 +107,7 @@ class DetectSOLEOL {
}
}
}
async run (data) {
if (!data || data.channel !== "realtime") {
return;
@@ -116,11 +116,11 @@ class DetectSOLEOL {
if (!(data.payload && data.payload.new && data.payload.new.meta)) {
return;
}
const meta = data.payload.new.meta;
if (this.queue.length < DetectSOLEOL.MAX_QUEUE_SIZE) {
this.queue.push({
isPending: this.queue.length,
_schema: meta._schema,
@@ -133,12 +133,12 @@ class DetectSOLEOL {
speed: meta.speed,
waterDepth: meta.waterDepth
});
} else {
// FIXME Change to alert
console.error("DetectSOLEOL queue full at", this.queue.length);
}
this.processQueue();
}
}

View File

@@ -14,7 +14,7 @@ function start () {
await handler.run(data);
}
});
console.log("Events manager started.", handlers.length, "active handlers");
}

View File

@@ -1,4 +1,4 @@
const { pool } = require('../../connection');

View File

@@ -1,4 +1,4 @@
const { setSurvey } = require('../../connection');

View File

@@ -11,17 +11,17 @@ async function patch (projectId, line, payload, opts = {}) {
// NOTE on the "complete" query: if complete is true it sets *only* virgin points to NTBA=true,
// but if complete is false it sets all points on the line to NTBA=false.
};
try {
transaction.begin(client);
for (const key in payload) {
const text = patchables[key];
const values = [ line, payload[key] ];
if (!text) {
throw {status: 400, message: "Invalid patch" };
}
await client.query(text, values);
}
transaction.commit(client);

View File

@@ -1,5 +1,5 @@
// FIXME This code is in painful need of refactoring
const { setSurvey, transaction, pool } = require('../connection');
let last_tstamp = 0;
@@ -67,7 +67,7 @@ async function getNearestPreplot (candidates) {
}
async function getNearestOfflinePreplot (candidates) {
const queries = candidates.map( c=> {
let text, values;
if ("latitude" in candidates[0] && "longitude" in candidates[0]) {
@@ -288,7 +288,7 @@ async function save (navData, opts = {}) {
const now = Date.now();
const do_save = !opts.offline_survey_detect_interval ||
(now - last_tstamp) >= opts.offline_survey_detect_interval;
if (do_save) {
const configs = await getAllProjectConfigs();
const candidates = configs.map(c => Object.assign({}, navData, {_schema: c.schema}));

View File

@@ -3,9 +3,9 @@ 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
@@ -13,7 +13,7 @@ async function patch (projectId, sequence, payload, opts = {}) {
function epoch (ts) {
return Number(ts)/1000;
}
/*
* Shift sequence ts0, ts1 by dt0, dt1 respectively
* for only one sequence
@@ -24,10 +24,10 @@ async function patch (projectId, sequence, payload, opts = {}) {
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
@@ -38,15 +38,15 @@ async function patch (projectId, sequence, payload, opts = {}) {
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) {
@@ -54,38 +54,38 @@ async function patch (projectId, sequence, payload, opts = {}) {
}
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);
@@ -96,16 +96,16 @@ async function patch (projectId, sequence, payload, opts = {}) {
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);
@@ -118,12 +118,12 @@ async function patch (projectId, sequence, payload, opts = {}) {
`;
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"};
@@ -134,15 +134,15 @@ async function patch (projectId, sequence, payload, opts = {}) {
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);
@@ -156,14 +156,14 @@ async function patch (projectId, sequence, payload, opts = {}) {
`;
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 = `
@@ -172,26 +172,26 @@ async function patch (projectId, sequence, payload, opts = {}) {
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
@@ -202,7 +202,7 @@ async function patch (projectId, sequence, payload, opts = {}) {
WHERE sequence = $1;
`;
await client.query(text, [sequence, payload.name, payload.remarks, payload.meta]);
} else {
throw { status: 400, message: "Bad request"};
}
@@ -210,7 +210,7 @@ async function patch (projectId, sequence, payload, opts = {}) {
transaction.commit(client);
} catch (err) {
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"};

View File

@@ -9,17 +9,17 @@ async function patch (projectId, sequence, payload, opts = {}) {
"meta": "UPDATE raw_lines SET meta = $2 WHERE sequence = $1;",
"meta_final": "UPDATE final_lines SET meta = $2 WHERE sequence = $1;"
};
try {
transaction.begin(client);
for (const key in payload) {
const text = patchables[key];
const values = [ sequence, payload[key] ];
if (!text) {
throw {status: 400, message: "Invalid patch" };
}
await client.query(text, values);
}
transaction.commit(client);

View File

@@ -1,4 +1,4 @@
class NavHeaderError extends Error {
constructor (message, payload) {
super (message);

View File

@@ -9,11 +9,11 @@ function leafletMap (cfg) {
const bbox = cfg.bbox || L.geoJSON(cfg.layers.map(i => i.features)).getBounds();
map.fitBounds(bbox);
map.setSize(cfg.size?.width || 500, cfg.size?.height || 500);
for (let layer of cfg.layers) {
L.geoJSON(layer.features, layer.options).addTo(map);
}
map.fitBounds(bbox); // again
return map;

View File

@@ -53,7 +53,7 @@ function njkTimestamp (arg, precision = "seconds") {
if (!isNaN(ts)) {
str = ts.toISOString();
}
if (str) {
str = str.replace("T", " ");
if (precision.toLowerCase().startsWith("s")) {
@@ -80,7 +80,7 @@ function njkMarkdownInline (str) {
}
async function render (data, template) {
const nenv = nunjucks.configure(Path.dirname(template), {autoescape: false, lstripBlocks: false, trimBlocks: false});
nenv.addFilter('find', njkFind);
nenv.addFilter('unique', njkUnique);
@@ -90,7 +90,7 @@ async function render (data, template) {
nenv.addFilter('timestamp', njkTimestamp);
nenv.addFilter('markdown', njkMarkdown);
nenv.addFilter('markdownInline', njkMarkdownInline);
const view = nenv.render(Path.basename(template), data);
return view;

View File

@@ -1,19 +1,19 @@
function transform (events, sequences, opts = {}) {
const dgl = !!opts.projectId;
const exportQC = opts.exportQC !== false;
const exportGeneral = opts.exportGeneral !== false;
const exportMissing = opts.exportMissing !== false;
const missingAsEvent = opts.missingAsEvent;
const output = {
DglProjectId: opts.projectId,
Sequences: [],
DglCreatedOn: dgl && new Date()
};
// NOTE: Events come in descending chronological
// order from the server.
for (const event of events.reverse()) {
@@ -23,7 +23,7 @@ function transform (events, sequences, opts = {}) {
if (!sequence) {
throw Error(`Sequence ${SequenceNumber} not found in sequence list`);
}
let SequenceObject = output.Sequences.find(s => s.SequenceNumber == SequenceNumber);
if (!SequenceObject) {
SequenceObject = {
@@ -47,10 +47,10 @@ function transform (events, sequences, opts = {}) {
}
SequenceObject.DglSequenceComments.push(i);
});
output.Sequences.push(SequenceObject);
}
if (event.labels.includes("FSP")) {
const entry = {
"EntryType": "Start of line recording",
@@ -65,10 +65,10 @@ function transform (events, sequences, opts = {}) {
"ShotPointId": event.point,
"Time": event.tstamp
}
SequenceObject.Entries.push(entry);
}
if (event.labels.includes("FGSP")) {
const entry = {
"EntryType": "Start good",
@@ -77,10 +77,10 @@ function transform (events, sequences, opts = {}) {
"ShotPointId": event.point,
"Time": event.tstamp
}
SequenceObject.Entries.push(entry);
}
if (event.labels.includes("FCSP")) {
const entry = {
"EntryType": "Start charged",
@@ -89,10 +89,10 @@ function transform (events, sequences, opts = {}) {
"ShotPointId": event.point,
"Time": event.tstamp
}
SequenceObject.Entries.push(entry);
}
if (event.labels.includes("LGFFSP")) {
const entry = {
"EntryType": "Last good Full Fold",
@@ -101,10 +101,10 @@ function transform (events, sequences, opts = {}) {
"ShotPointId": event.point,
"Time": event.tstamp
}
SequenceObject.Entries.push(entry);
}
if (event.labels.includes("LCFFSP")) {
const entry = {
"EntryType": "Last charged Full Fold",
@@ -113,10 +113,10 @@ function transform (events, sequences, opts = {}) {
"ShotPointId": event.point,
"Time": event.tstamp
}
SequenceObject.Entries.push(entry);
}
if (event.labels.includes("FDSP")) {
const entry = {
"EntryType": "Midnight",
@@ -125,10 +125,10 @@ function transform (events, sequences, opts = {}) {
"ShotPointId": event.point,
"Time": event.tstamp
}
SequenceObject.Entries.push(entry);
}
if (event.labels.includes("LCSP")) {
const entry = {
"EntryType": "End charged",
@@ -137,10 +137,10 @@ function transform (events, sequences, opts = {}) {
"ShotPointId": event.point,
"Time": event.tstamp
}
SequenceObject.Entries.push(entry);
}
if (event.labels.includes("LGSP")) {
const entry = {
"EntryType": "End good",
@@ -149,10 +149,10 @@ function transform (events, sequences, opts = {}) {
"ShotPointId": event.point,
"Time": event.tstamp
}
SequenceObject.Entries.push(entry);
}
if (event.labels.includes("LSP")) {
const entry = {
"EntryType": "End of line recording",
@@ -165,12 +165,12 @@ function transform (events, sequences, opts = {}) {
"ShotPointId": event.point,
"Time": event.tstamp
}
SequenceObject.Entries.push(entry);
}
// Dougal QC data
if (exportQC) {
if (event.labels.includes("QC")) {
if (!event.labels.includes("QCAccepted")) {
@@ -184,7 +184,7 @@ function transform (events, sequences, opts = {}) {
}
}
}
if (exportGeneral) {
// These are labels that we have already (potentially) exported
const excluded = [
@@ -203,13 +203,13 @@ function transform (events, sequences, opts = {}) {
SequenceObject.Entries.push(entry);
}
}
}
}
for (const SequenceObject of output.Sequences) {
const sequence = sequences.find(s => s.sequence == SequenceObject.SequenceNumber);
// If no explicit FSP but there is a FGSP, clone it as FSP.
if (!SequenceObject.Entries.find(i => i.EntryTypeId == 3000)) {
const fgspIndex = SequenceObject.Entries.findIndex(i => i.EntryTypeId == 3001);
@@ -247,7 +247,7 @@ function transform (events, sequences, opts = {}) {
SequenceObject.Entries.unshift(fsp);
}
}
// If no explicit LSP but there is a LGSP, clone it as LSP.
if (!SequenceObject.Entries.find(i => i.EntryTypeId == 3007)) {
const lgspIndex = SequenceObject.Entries.findIndex(i => i.EntryTypeId == 3006);
@@ -277,17 +277,17 @@ function transform (events, sequences, opts = {}) {
SequenceObject.Entries.push(lsp);
}
}
// Set the missing shots object if not inhibited by the user.
if (exportMissing) {
// The user also needs to request missing shots from the sequences
// endpoint; these are not returned by default.
if ("missing_final" in sequence) {
SequenceObject.MissingShots = sequence.missing_final.map(s => s.point).sort();
// Add (pseudo-)events for missing shots
if (missingAsEvent) {
// Create the dummy missing shot events
const pseudoevents = SequenceObject.MissingShots.map( point => {
return {
@@ -295,28 +295,28 @@ function transform (events, sequences, opts = {}) {
EntryType: "Missing shot"
}
});
const isAscending = SequenceObject.Entries[0].ShotPointId <= SequenceObject.Entries[SequenceObject.Entries.length-1].ShotPointId;
pseudoevents.forEach( pseudoevent => {
for (const index in SequenceObject.Entries) {
const ShotPointId = SequenceObject.Entries[index].ShotPointId;
const slotFound = isAscending
? ShotPointId > pseudoevent.ShotPointId
: ShotPointId < pseudoevent.ShotPointId;
if (slotFound) {
SequenceObject.Entries.splice(index, 0, pseudoevent);
break;
}
}
});
}
}
}
}
return output;
}

View File

@@ -1,35 +1,35 @@
function dms (lat, lon) {
const λh = lat < 0 ? "S" : "N";
const φh = lon < 0 ? "W" : "E";
const λn = Math.abs(lat);
const φn = Math.abs(lon);
const λi = Math.trunc(λn);
const φi = Math.trunc(φn);
const λf = λn - λi;
const φf = φn - φi;
const λs = ((λf*3600)%60).toFixed(1);
const φs = ((φf*3600)%60).toFixed(1);
const λm = Math.trunc(λf*60);
const φm = Math.trunc(φf*60);
const λ =
String(λi).padStart(2, "0") + "°" +
String(λm).padStart(2, "0") + "'" +
String(λs).padStart(4, "0") + '" ' +
λh;
const φ =
String(φi).padStart(3, "0") + "°" +
String(φm).padStart(2, "0") + "'" +
String(φs).padStart(4, "0") + '" ' +
φh;
return λ+" "+φ;
}

View File

@@ -4,9 +4,9 @@ const dms = require('./dms');
function geometryAsString (item, opts = {}) {
const key = "key" in opts ? opts.key : "geometry";
const formatDMS = opts.dms;
let str = "";
if (key in item) {
const geometry = item[key];
if (geometry && "coordinates" in geometry) {
@@ -17,7 +17,7 @@ function geometryAsString (item, opts = {}) {
str = `${geometry.coordinates[1].toFixed(6)}, ${geometry.coordinates[0].toFixed(6)}`;
}
}
if (str) {
if (opts.url) {
if (typeof opts.url === 'string') {
@@ -29,7 +29,7 @@ function geometryAsString (item, opts = {}) {
}
}
}
return str;
}

View File

@@ -4,7 +4,7 @@ const db = require('./db');
const channels = require('../lib/db/channels');
function start (server, pingInterval=30000) {
const wsServer = new ws.Server({ noServer: true });
wsServer.on('connection', socket => {
socket.alive = true;