mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 11:07:08 +00:00
Compare commits
53 Commits
63-serve-a
...
84-produce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42697fe91d | ||
|
|
900d7f7a3e | ||
|
|
f1953807db | ||
|
|
814e071698 | ||
|
|
2aba132220 | ||
|
|
15a802227d | ||
|
|
6745757712 | ||
|
|
9ff76867c9 | ||
|
|
e8811560de | ||
|
|
65b33a6b0f | ||
|
|
b8b5765b46 | ||
|
|
53f4e167f8 | ||
|
|
3d8f524d4a | ||
|
|
1e68676ac6 | ||
|
|
2c2d594877 | ||
|
|
fae849aeab | ||
|
|
1d47495799 | ||
|
|
592632d669 | ||
|
|
26c05b9e3c | ||
|
|
3f9a40724d | ||
|
|
a652a08815 | ||
|
|
61ffd1b766 | ||
|
|
d9f4583224 | ||
|
|
95647337aa | ||
|
|
b1e152179e | ||
|
|
142a820ed7 | ||
|
|
838b45ef26 | ||
|
|
30914b267a | ||
|
|
f1cbbdb56b | ||
|
|
9973e8f132 | ||
|
|
f53c479262 | ||
|
|
73a415a038 | ||
|
|
0b24e3224f | ||
|
|
c271256015 | ||
|
|
4887ddaa26 | ||
|
|
788c582f98 | ||
|
|
df9f7f33cf | ||
|
|
fd2e0399f8 | ||
|
|
db733ceef8 | ||
|
|
f905eb3fdf | ||
|
|
e707887702 | ||
|
|
c0ace1fe07 | ||
|
|
7bb3a3910b | ||
|
|
983113b6cc | ||
|
|
ff66c9a88d | ||
|
|
56d30d48c5 | ||
|
|
df3a0b4c50 | ||
|
|
f87aa08246 | ||
|
|
ea499a645b | ||
|
|
0fdb42c593 | ||
|
|
6e5584a433 | ||
|
|
0a4df0793d | ||
|
|
1e6cc67b05 |
@@ -392,6 +392,14 @@ class Datastore:
|
||||
cursor.execute("BEGIN;")
|
||||
|
||||
hash = self.add_file(filepath, cursor)
|
||||
|
||||
if not records or len(records) == 0:
|
||||
print("File has no records (or none have been detected)")
|
||||
# We add the file to the database anyway to signal that we have
|
||||
# actually seen it.
|
||||
self.maybe_commit()
|
||||
return
|
||||
|
||||
incr = p111.point_number(records[0]) <= p111.point_number(records[-1])
|
||||
|
||||
# Start by deleting any online data we may have for this sequence
|
||||
|
||||
@@ -12,6 +12,7 @@ import os
|
||||
import sys
|
||||
import pathlib
|
||||
import re
|
||||
import time
|
||||
import configuration
|
||||
import p111
|
||||
from datastore import Datastore
|
||||
@@ -20,6 +21,7 @@ if __name__ == '__main__':
|
||||
|
||||
print("Reading configuration")
|
||||
surveys = configuration.surveys()
|
||||
file_min_age = configuration.read().get('imports', {}).get('file_min_age', 10)
|
||||
|
||||
print("Connecting to database")
|
||||
db = Datastore()
|
||||
@@ -49,6 +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))
|
||||
|
||||
@@ -12,6 +12,7 @@ import os
|
||||
import sys
|
||||
import pathlib
|
||||
import re
|
||||
import time
|
||||
import configuration
|
||||
import p190
|
||||
from datastore import Datastore
|
||||
@@ -20,6 +21,7 @@ if __name__ == '__main__':
|
||||
|
||||
print("Reading configuration")
|
||||
surveys = configuration.surveys()
|
||||
file_min_age = configuration.read().get('imports', {}).get('file_min_age', 10)
|
||||
|
||||
print("Connecting to database")
|
||||
db = Datastore()
|
||||
@@ -49,6 +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))
|
||||
|
||||
@@ -8,7 +8,9 @@ or modified preplots and (re-)import them into the database.
|
||||
"""
|
||||
|
||||
from glob import glob
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import configuration
|
||||
import preplots
|
||||
from datastore import Datastore
|
||||
@@ -17,6 +19,7 @@ if __name__ == '__main__':
|
||||
|
||||
print("Reading configuration")
|
||||
surveys = configuration.surveys()
|
||||
file_min_age = configuration.read().get('imports', {}).get('file_min_age', 10)
|
||||
|
||||
print("Connecting to database")
|
||||
db = Datastore()
|
||||
@@ -28,6 +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)
|
||||
|
||||
@@ -12,6 +12,7 @@ import os
|
||||
import sys
|
||||
import pathlib
|
||||
import re
|
||||
import time
|
||||
import configuration
|
||||
import p111
|
||||
from datastore import Datastore
|
||||
@@ -20,6 +21,7 @@ if __name__ == '__main__':
|
||||
|
||||
print("Reading configuration")
|
||||
surveys = configuration.surveys()
|
||||
file_min_age = configuration.read().get('imports', {}).get('file_min_age', 10)
|
||||
|
||||
print("Connecting to database")
|
||||
db = Datastore()
|
||||
@@ -57,6 +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))
|
||||
|
||||
@@ -12,6 +12,7 @@ import os
|
||||
import sys
|
||||
import pathlib
|
||||
import re
|
||||
import time
|
||||
import configuration
|
||||
import p190
|
||||
from datastore import Datastore
|
||||
@@ -20,6 +21,7 @@ if __name__ == '__main__':
|
||||
|
||||
print("Reading configuration")
|
||||
surveys = configuration.surveys()
|
||||
file_min_age = configuration.read().get('imports', {}).get('file_min_age', 10)
|
||||
|
||||
print("Connecting to database")
|
||||
db = Datastore()
|
||||
@@ -52,6 +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))
|
||||
|
||||
@@ -12,6 +12,7 @@ import os
|
||||
import sys
|
||||
import pathlib
|
||||
import re
|
||||
import time
|
||||
import configuration
|
||||
import smsrc
|
||||
from datastore import Datastore
|
||||
@@ -20,6 +21,7 @@ if __name__ == '__main__':
|
||||
|
||||
print("Reading configuration")
|
||||
surveys = configuration.surveys()
|
||||
file_min_age = configuration.read().get('imports', {}).get('file_min_age', 10)
|
||||
|
||||
print("Connecting to database")
|
||||
db = Datastore()
|
||||
@@ -53,6 +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))
|
||||
|
||||
48
bin/insert_event.py
Executable file
48
bin/insert_event.py
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
from datetime import datetime
|
||||
from datastore import Datastore
|
||||
|
||||
def detect_schema (conn):
|
||||
with conn.cursor() as cursor:
|
||||
qry = "SELECT meta->>'_schema' AS schema, tstamp, age(current_timestamp, tstamp) age FROM real_time_inputs WHERE meta ? '_schema' AND age(current_timestamp, tstamp) < '02:00:00' ORDER BY tstamp DESC LIMIT 1"
|
||||
cursor.execute(qry)
|
||||
res = cursor.fetchone()
|
||||
if res and len(res):
|
||||
return res[0]
|
||||
return None
|
||||
|
||||
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")
|
||||
ap.add_argument("-l", "--label", required=False, default=None, action="append", help="event label")
|
||||
ap.add_argument('remarks', type=str, nargs="+", help="event message")
|
||||
args = vars(ap.parse_args())
|
||||
|
||||
|
||||
db = Datastore()
|
||||
db.connect()
|
||||
|
||||
if args["schema"]:
|
||||
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:
|
||||
qry = "INSERT INTO events_timed (tstamp, remarks) VALUES (%s, %s);"
|
||||
cursor.execute(qry, (tstamp, message))
|
||||
db.maybe_commit()
|
||||
@@ -96,16 +96,21 @@ if __name__ == '__main__':
|
||||
with db.conn.cursor() as cursor:
|
||||
columns = exportables["public"][table]
|
||||
path = os.path.join(VARDIR, "-"+table)
|
||||
with open(path, "rb") as fd:
|
||||
print(" →→ ", path, " ←← ", table, columns)
|
||||
if columns is not None:
|
||||
import_table(fd, table, columns, cursor)
|
||||
else:
|
||||
try:
|
||||
print(f"Copying from {path} into {table}")
|
||||
cursor.copy_from(fd, table)
|
||||
except psycopg2.errors.UniqueViolation:
|
||||
print(f"It looks like table {table} may have already been imported. Skipping it.")
|
||||
try:
|
||||
with open(path, "rb") as fd:
|
||||
print(" →→ ", path, " ←← ", table, columns)
|
||||
if columns is not None:
|
||||
import_table(fd, table, columns, cursor)
|
||||
else:
|
||||
try:
|
||||
print(f"Copying from {path} into {table}")
|
||||
cursor.copy_from(fd, table)
|
||||
except psycopg2.errors.UniqueViolation:
|
||||
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")
|
||||
for survey in surveys:
|
||||
@@ -124,15 +129,18 @@ if __name__ == '__main__':
|
||||
path = os.path.join(pathPrefix, "-"+table)
|
||||
print(" ←← ", path, " →→ ", table, columns)
|
||||
|
||||
with open(path, "rb") as fd:
|
||||
if columns is not None:
|
||||
import_table(fd, table, columns, cursor)
|
||||
else:
|
||||
try:
|
||||
print(f"Copying from {path} into {table}")
|
||||
cursor.copy_from(fd, table)
|
||||
except psycopg2.errors.UniqueViolation:
|
||||
print(f"It looks like table {table} may have already been imported. Skipping it.")
|
||||
try:
|
||||
with open(path, "rb") as fd:
|
||||
if columns is not None:
|
||||
import_table(fd, table, columns, cursor)
|
||||
else:
|
||||
try:
|
||||
print(f"Copying from {path} into {table}")
|
||||
cursor.copy_from(fd, table)
|
||||
except psycopg2.errors.UniqueViolation:
|
||||
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()
|
||||
|
||||
@@ -22,3 +22,9 @@ navigation:
|
||||
# saving routine.
|
||||
epsg: 23031 # Assume this CRS for unqualified E/N data
|
||||
|
||||
|
||||
imports:
|
||||
# For a file to be imported, it must have been last modified at
|
||||
# least this many seconds ago.
|
||||
file_min_age: 60
|
||||
|
||||
|
||||
@@ -20,11 +20,20 @@
|
||||
disabled: false
|
||||
labels: [ "QC", "QCGuns" ]
|
||||
children:
|
||||
-
|
||||
name: "Sequences without gun data"
|
||||
iterate: "sequences"
|
||||
id: seq_no_gun_data
|
||||
check: |
|
||||
const sequence = currentItem;
|
||||
currentItem.has_smsrc_data || "Sequence has no gun data"
|
||||
-
|
||||
name: "Missing gun data"
|
||||
id: missing_gun_data
|
||||
check: |
|
||||
!!currentItem._("raw_meta.smsrc.guns") || "Missing gun data"
|
||||
sequences.some(s => s.sequence == currentItem.sequence && s.has_smsrc_data)
|
||||
? (!!currentItem._("raw_meta.smsrc.guns") || "Missing gun data")
|
||||
: true
|
||||
|
||||
-
|
||||
name: "No fire"
|
||||
@@ -192,7 +201,7 @@
|
||||
check: |
|
||||
const currentShot = currentItem;
|
||||
Math.abs(currentShot.error_i) <= parameters.crosslineError
|
||||
|| `Crossline error: ${currentShot.error_i.toFixed(1)} > ${parameters.crosslineError}`
|
||||
|| `Crossline error (${currentShot.type}): ${currentShot.error_i.toFixed(1)} > ${parameters.crosslineError}`
|
||||
|
||||
-
|
||||
name: "Inline"
|
||||
@@ -200,7 +209,7 @@
|
||||
check: |
|
||||
const currentShot = currentItem;
|
||||
Math.abs(currentShot.error_j) <= parameters.inlineError
|
||||
|| `Inline error: ${currentShot.error_j.toFixed(1)} > ${parameters.inlineError}`
|
||||
|| `Inline error (${currentShot.type}): ${currentShot.error_j.toFixed(1)} > ${parameters.inlineError}`
|
||||
|
||||
-
|
||||
name: "Centre of source preplot deviation (moving average)"
|
||||
|
||||
13597
lib/www/client/source/package-lock.json
generated
13597
lib/www/client/source/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,18 +14,19 @@
|
||||
"leaflet-arrowheads": "^1.2.2",
|
||||
"leaflet-realtime": "^2.2.0",
|
||||
"leaflet.markercluster": "^1.4.1",
|
||||
"marked": "^2.0.3",
|
||||
"typeface-roboto": "0.0.75",
|
||||
"vue": "^2.6.12",
|
||||
"vue-debounce": "^2.5.7",
|
||||
"vue-router": "^3.4.5",
|
||||
"vuetify": "^2.3.12",
|
||||
"vuetify": "^2.4.11",
|
||||
"vuex": "^3.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.4.0",
|
||||
"@vue/cli-plugin-router": "~4.4.0",
|
||||
"@vue/cli-plugin-vuex": "~4.4.0",
|
||||
"@vue/cli-service": "~4.4.0",
|
||||
"@vue/cli-service": "^4.5.13",
|
||||
"sass": "^1.26.11",
|
||||
"sass-loader": "^8.0.0",
|
||||
"stylus": "^0.54.8",
|
||||
|
||||
@@ -26,6 +26,12 @@
|
||||
<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
|
||||
</style>
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
text
|
||||
:href="`mailto:${email}?Subject=Question`"
|
||||
>
|
||||
Ask a question
|
||||
<v-icon class="d-lg-none">mdi-help-circle</v-icon>
|
||||
<span class="d-none d-lg-inline">Ask a question</span>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
@@ -41,7 +42,17 @@
|
||||
text
|
||||
href="mailto:dougal-support@aaltronav.eu?Subject=Bug report"
|
||||
>
|
||||
Report a bug
|
||||
<v-icon class="d-lg-none">mdi-bug</v-icon>
|
||||
<span class="d-none d-lg-inline">Report a bug</span>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
color="info"
|
||||
text
|
||||
:href='"/feed/"+feed'
|
||||
title="View development log"
|
||||
>
|
||||
<v-icon>mdi-rss</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
@@ -52,7 +63,8 @@
|
||||
text
|
||||
@click="dialog=false"
|
||||
>
|
||||
Close
|
||||
<v-icon class="d-lg-none">mdi-close-circle</v-icon>
|
||||
<span class="d-none d-lg-inline">Close</span>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
@@ -69,7 +81,8 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
dialog: false,
|
||||
email: "dougal-support@aaltronav.eu"
|
||||
email: "dougal-support@aaltronav.eu",
|
||||
feed: btoa(encodeURIComponent("https://gitlab.com/wgp/dougal/software.atom?feed_token=XSPpvsYEny8YmH75Nz5W"))
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
background-color green
|
||||
&.online
|
||||
background-color blue
|
||||
&.planned
|
||||
background-color magenta
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -96,7 +98,9 @@ export default {
|
||||
? "Acquired"
|
||||
: s.status == "ntbp"
|
||||
? "NTBP"
|
||||
: s.status;
|
||||
: s.status == "planned"
|
||||
? "Planned"
|
||||
: s.status;
|
||||
|
||||
const remarks = "\n"+[s.remarks, s.remarks_final].join("\n").trim()
|
||||
|
||||
|
||||
11
lib/www/client/source/src/lib/markdown.js
Normal file
11
lib/www/client/source/src/lib/markdown.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const marked = require('marked');
|
||||
|
||||
function markdown (str) {
|
||||
return marked(String(str));
|
||||
}
|
||||
|
||||
function markdownInline (str) {
|
||||
return marked.parseInline(String(str));
|
||||
}
|
||||
|
||||
module.exports = { markdown, markdownInline };
|
||||
@@ -26,4 +26,70 @@ function withParentProps(item, parent, childrenKey, prop, currentValue) {
|
||||
return [];
|
||||
}
|
||||
|
||||
export { withParentProps }
|
||||
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) {
|
||||
if (geometry.type == "Point") {
|
||||
if (formatDMS) {
|
||||
str = dms(geometry.coordinates[1], geometry.coordinates[0]);
|
||||
} else {
|
||||
str = `${geometry.coordinates[1].toFixed(6)}, ${geometry.coordinates[0].toFixed(6)}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (str) {
|
||||
if (opts.url) {
|
||||
if (typeof opts.url === 'string') {
|
||||
str = `[${str}](${opts.url.replace("$x", geometry.coordinates[0]).replace("$y", geometry.coordinates[1])})`;
|
||||
} else {
|
||||
str = `[${str}](geo:${geometry.coordinates[0]},${geometry.coordinates[1]})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
export { withParentProps, geometryAsString }
|
||||
|
||||
@@ -5,12 +5,22 @@ import store from './store'
|
||||
import vuetify from './plugins/vuetify'
|
||||
import vueDebounce from 'vue-debounce'
|
||||
import { mapMutations } from 'vuex';
|
||||
|
||||
import { markdown, markdownInline } from './lib/markdown';
|
||||
import { geometryAsString } from './lib/utils';
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
Vue.use(vueDebounce);
|
||||
|
||||
Vue.filter('markdown', markdown);
|
||||
Vue.filter('markdownInline', markdownInline);
|
||||
Vue.filter('position', (str, item, opts) =>
|
||||
str
|
||||
.replace(/@POS(ITION)?@/g, geometryAsString(item, opts) || "(position unknown)")
|
||||
.replace(/@DMS@/g, geometryAsString(item, {...opts, dms:true}) || "(position unknown)")
|
||||
);
|
||||
// Vue.filter('position', (str, item, opts) => str.replace(/@POS(ITION)?@/, "☺"));
|
||||
|
||||
new Vue({
|
||||
data () {
|
||||
return {
|
||||
|
||||
@@ -33,6 +33,14 @@ Vue.use(VueRouter)
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
|
||||
},
|
||||
{
|
||||
path: '/feed/:source',
|
||||
name: 'Feed',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import(/* webpackChunkName: "about" */ '../views/Feed.vue')
|
||||
},
|
||||
{
|
||||
pathToRegexpOptions: { strict: true },
|
||||
path: "/login",
|
||||
|
||||
@@ -22,7 +22,7 @@ async function api ({state, commit, dispatch}, [resource, init = {}, cb]) {
|
||||
await dispatch('setCredentials');
|
||||
|
||||
try {
|
||||
return await res.json();
|
||||
return init.text ? (await res.text()) : (await res.json());
|
||||
} catch (err) {
|
||||
if (err instanceof SyntaxError) {
|
||||
if (Number(res.headers.get("Content-Length")) === 0) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-sheet height="64">
|
||||
<v-toolbar flat color="white">
|
||||
<v-toolbar flat>
|
||||
|
||||
<v-menu bottom right>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
|
||||
104
lib/www/client/source/src/views/Feed.vue
Normal file
104
lib/www/client/source/src/views/Feed.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-overlay absolute opacity="1" :value="!feed.updated">
|
||||
<v-progress-circular indeterminate size="64"></v-progress-circular>
|
||||
</v-overlay>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{feed.title}}
|
||||
<a :href="feed.link"><v-icon class="ml-2">mdi-link</v-icon></a>
|
||||
</v-card-title>
|
||||
<v-card-subtitle>Last updated: {{feed.updated}}</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-timeline align-top dense>
|
||||
<v-timeline-item v-for="item in feed.items" small :key="item.id">
|
||||
<v-card :color="item.colour">
|
||||
<v-card-title primary-title>
|
||||
<div class="headline">{{item.title}}</div>
|
||||
</v-card-title>
|
||||
<v-card-subtitle>{{item.updated}} ({{item.author}})</v-card-subtitle>
|
||||
<v-card-text v-html="item.summary">
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn target="_new" :href="item.link">Complete story</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-timeline-item>
|
||||
</v-timeline>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: "FeedViewer",
|
||||
|
||||
data () {
|
||||
return {
|
||||
timer: null,
|
||||
feed: {}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
parse (text) {
|
||||
const data = {items:[]};
|
||||
const parser = new DOMParser();
|
||||
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];
|
||||
if (link) {
|
||||
item.link = link.getAttribute("href");
|
||||
}
|
||||
const author = entry.getElementsByTagName("author")[0];
|
||||
if (author) {
|
||||
const name = author.getElementsByTagName("name")[0];
|
||||
item.author = name ? name.childNodes[0].textContent : author.innerHTML;
|
||||
}
|
||||
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;
|
||||
},
|
||||
|
||||
async refresh () {
|
||||
const text = await this.api([`/rss/?remote=${atob(this.$route.params.source)}`, {text:true}]);
|
||||
this.feed = this.parse(text);
|
||||
},
|
||||
|
||||
...mapActions(["api"])
|
||||
},
|
||||
|
||||
async mounted () {
|
||||
await this.refresh();
|
||||
this.timer = setInterval(this.refresh, 300000);
|
||||
},
|
||||
|
||||
unmounted () {
|
||||
cancelInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -24,23 +24,85 @@
|
||||
offset-y
|
||||
>
|
||||
<v-list dense v-if="contextMenuItem">
|
||||
<v-list-item @click="setNTBA">
|
||||
<v-list-item-title v-if="contextMenuItem.ntba">Unset NTBA</v-list-item-title>
|
||||
<v-list-item-title v-else>Set NTBA</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="addToPlan" v-if="!contextMenuItem.ntba">
|
||||
<v-list-item-title>Add to plan</v-list-item-title>
|
||||
<template v-if="!selectOn">
|
||||
<v-list-item @click="setNTBA">
|
||||
<v-list-item-title v-if="contextMenuItem.ntba">Unset NTBA</v-list-item-title>
|
||||
<v-list-item-title v-else>Set NTBA</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="addToPlan" v-if="!contextMenuItem.ntba && !isPlanned(contextMenuItem)">
|
||||
<v-list-item-title>Add to plan</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="removeFromPlan" v-if="isPlanned(contextMenuItem)">
|
||||
<v-list-item-title>Remove from plan</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="showLineColourDialog">
|
||||
<v-list-item-title>Set colour…</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-list-item @click="showLineColourDialog">
|
||||
<v-list-item-title>Set colour…</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<v-divider></v-divider>
|
||||
<v-list-item>
|
||||
<v-list-item-action-text>Multi-select</v-list-item-action-text>
|
||||
<v-list-item-action><v-checkbox v-model="selectOn"></v-checkbox></v-list-item-action>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-dialog
|
||||
v-model="colourPickerShow"
|
||||
max-width="300"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>Choose line colour</v-card-title>
|
||||
<v-card-text>
|
||||
<v-color-picker
|
||||
v-model="selectedColour"
|
||||
show-swatches
|
||||
hide-canvas
|
||||
hide-inputs
|
||||
hide-mode-switch
|
||||
@update:color="selectedColour.alpha = 0.5"
|
||||
>
|
||||
</v-color-picker>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn small
|
||||
@click="selectedColour=null; setLineColour()"
|
||||
>
|
||||
Clear
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn small
|
||||
@click="setLineColour"
|
||||
>
|
||||
Set
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn small
|
||||
@click="colourPickerShow=false"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="items"
|
||||
item-key="line"
|
||||
:items-per-page.sync="itemsPerPage"
|
||||
:search="filter"
|
||||
:loading="loading"
|
||||
:fixed-header="true"
|
||||
:item-class="(item) => (activeItem == item && !edit) ? 'blue accent-1 elevation-3' : ''"
|
||||
:footer-props='{itemsPerPageOptions: [ 10, 25, 50, 100, 500, -1 ]}'
|
||||
:item-class="itemClass"
|
||||
:show-select="selectOn"
|
||||
v-model="selectedRows"
|
||||
@click:row="setActiveItem"
|
||||
@contextmenu:row="contextMenu"
|
||||
>
|
||||
@@ -77,8 +139,7 @@
|
||||
@click:append-outer="edit = null"
|
||||
>
|
||||
</v-text-field>
|
||||
<div v-else>
|
||||
{{item.remarks}}
|
||||
<div v-else v-html="$options.filters.markdownInline(item.remarks)">
|
||||
<v-btn v-if="edit === null"
|
||||
icon
|
||||
small
|
||||
@@ -112,7 +173,6 @@
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import DougalLineStatus from '@/components/line-status';
|
||||
|
||||
|
||||
export default {
|
||||
name: "LineList",
|
||||
|
||||
@@ -162,23 +222,31 @@ export default {
|
||||
}
|
||||
],
|
||||
items: [],
|
||||
selectOn: false,
|
||||
selectedRows: [],
|
||||
filter: null,
|
||||
num_lines: null,
|
||||
sequences: [],
|
||||
activeItem: null,
|
||||
edit: null, // {line, key, value}
|
||||
queuedReload: false,
|
||||
itemsPerPage: 25,
|
||||
|
||||
// Context menu stuff
|
||||
contextMenuShow: false,
|
||||
contextMenuX: 0,
|
||||
contextMenuY: 0,
|
||||
contextMenuItem: null
|
||||
contextMenuItem: null,
|
||||
|
||||
// Colour picker stuff
|
||||
colourPickerShow: false,
|
||||
selectedColour: null,
|
||||
styles: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['loading', 'serverEvent'])
|
||||
...mapGetters(['user', 'loading', 'serverEvent'])
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -202,15 +270,23 @@ export default {
|
||||
},
|
||||
|
||||
async serverEvent (event) {
|
||||
if (event.channel == "preplot_lines" && event.payload.pid == this.$route.params.project) {
|
||||
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
|
||||
// already had time to update the cache by the time our request
|
||||
// gets back to it.
|
||||
this.getLines();
|
||||
} else {
|
||||
this.queuedReload = true;
|
||||
if (event.payload.pid == this.$route.params.project) {
|
||||
if (event.channel == "preplot_lines") {
|
||||
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
|
||||
// already had time to update the cache by the time our request
|
||||
// gets back to it.
|
||||
this.getLines();
|
||||
} else {
|
||||
this.queuedReload = true;
|
||||
}
|
||||
} else if ([ "planned_lines", "raw_lines", "final_lines" ].includes(event.channel)) {
|
||||
if (!this.loading && !this.queuedReload) {
|
||||
this.getSequences();
|
||||
} else {
|
||||
this.queuedReload = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -218,19 +294,47 @@ 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;
|
||||
@@ -241,6 +345,7 @@ export default {
|
||||
},
|
||||
|
||||
setNTBA () {
|
||||
this.removeFromPlan();
|
||||
this.saveItem({
|
||||
line: this.contextMenuItem.line,
|
||||
key: 'ntba',
|
||||
@@ -255,7 +360,6 @@ export default {
|
||||
fsp: this.contextMenuItem.fsp,
|
||||
lsp: this.contextMenuItem.lsp
|
||||
}
|
||||
console.log("Plan", payload);
|
||||
const url = `/project/${this.$route.params.project}/plan`;
|
||||
const init = {
|
||||
method: "POST",
|
||||
@@ -265,6 +369,42 @@ export default {
|
||||
await this.api([url, init]);
|
||||
},
|
||||
|
||||
async removeFromPlan () {
|
||||
const plannedLine = this.sequences.find(i => i.status == "planned" && i.line == this.contextMenuItem.line);
|
||||
if (plannedLine && plannedLine.sequence) {
|
||||
const url = `/project/${this.$route.params.project}/plan/${plannedLine.sequence}`;
|
||||
const init = {
|
||||
method: "DELETE"
|
||||
}
|
||||
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;
|
||||
} else {
|
||||
delete item.meta.colour;
|
||||
}
|
||||
this.saveItem({line: item.line, key: "meta", value: item.meta});
|
||||
this.colourPickerShow = false;
|
||||
}
|
||||
},
|
||||
|
||||
editItem (item, key) {
|
||||
this.edit = {
|
||||
line: item.line,
|
||||
@@ -308,8 +448,13 @@ export default {
|
||||
},
|
||||
|
||||
async getSequences () {
|
||||
const url = `/project/${this.$route.params.project}/sequence`;
|
||||
this.sequences = await this.api([url]) || [];
|
||||
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");
|
||||
this.sequences.push(...planned);
|
||||
},
|
||||
|
||||
setActiveItem (item) {
|
||||
@@ -325,6 +470,11 @@ export default {
|
||||
this.getLines();
|
||||
this.getNumLines();
|
||||
this.getSequences();
|
||||
|
||||
// Initialise stylesheet
|
||||
const el = document.createElement("style");
|
||||
document.head.appendChild(el);
|
||||
this.styles = document.styleSheets[document.styleSheets.length-1];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,17 +5,32 @@
|
||||
<v-card-title>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title>
|
||||
{{
|
||||
$route.params.sequence
|
||||
? ($route.params.sequence.includes && $route.params.sequence.includes(";"))
|
||||
? `Sequences ${$route.params.sequence.split(";").sort().join(", ")}`
|
||||
: `Sequence ${$route.params.sequence}`
|
||||
: $route.params.date0
|
||||
? $route.params.date1
|
||||
? `Between ${$route.params.date0} and ${$route.params.date1}`
|
||||
: `On ${$route.params.date0}`
|
||||
: "All events"
|
||||
}}
|
||||
<span class="d-none d-lg-inline">
|
||||
{{
|
||||
$route.params.sequence
|
||||
? ($route.params.sequence.includes && $route.params.sequence.includes(";"))
|
||||
? `Sequences ${$route.params.sequence.split(";").sort().join(", ")}`
|
||||
: `Sequence ${$route.params.sequence}`
|
||||
: $route.params.date0
|
||||
? $route.params.date1
|
||||
? `Between ${$route.params.date0} and ${$route.params.date1}`
|
||||
: `On ${$route.params.date0}`
|
||||
: "All events"
|
||||
}}
|
||||
</span>
|
||||
<span class="d-lg-none">
|
||||
{{
|
||||
$route.params.sequence
|
||||
? ($route.params.sequence.includes && $route.params.sequence.includes(";"))
|
||||
? `${$route.params.sequence.split(";").sort().join(", ")}`
|
||||
: `${$route.params.sequence}`
|
||||
: $route.params.date0
|
||||
? $route.params.date1
|
||||
? `${$route.params.date0} ‒ ${$route.params.date1}`
|
||||
: `${$route.params.date0}`
|
||||
: ""
|
||||
}}
|
||||
</span>
|
||||
</v-toolbar-title>
|
||||
|
||||
<dougal-event-edit-dialog
|
||||
@@ -28,6 +43,38 @@
|
||||
:event-mode="online?'seq':'timed'"
|
||||
@save="saveEvent"
|
||||
></dougal-event-edit-dialog>
|
||||
|
||||
<v-menu v-if="$route.params.sequence">
|
||||
<template v-slot:activator="{on, attrs}">
|
||||
<v-btn class="ml-5" small v-on="on" v-bind="attrs">
|
||||
<span class="d-none d-lg-inline">Download as…</span>
|
||||
<v-icon right small>mdi-cloud-download</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list>
|
||||
<v-list-item
|
||||
:href="`/api/project/${$route.params.project}/event/-/${$route.params.sequence}?mime=application%2Fvnd.seis%2Bjson`"
|
||||
title="Download as a Multiseis-compatible Seis+JSON file."
|
||||
>Seis+JSON</v-list-item>
|
||||
<v-list-item
|
||||
:href="`/api/project/${$route.params.project}/event/-/${$route.params.sequence}?mime=application%2Fgeo%2Bjson`"
|
||||
title="Download as a QGIS-compatible GeoJSON file"
|
||||
>GeoJSON</v-list-item>
|
||||
<v-list-item
|
||||
:href="`/api/project/${$route.params.project}/event/-/${$route.params.sequence}?mime=application%2Fjson`"
|
||||
title="Download as a generic JSON file"
|
||||
>JSON</v-list-item>
|
||||
<v-list-item
|
||||
:href="`/api/project/${$route.params.project}/event/-/${$route.params.sequence}?mime=text%2Fhtml`"
|
||||
title="Download as an HTML formatted file"
|
||||
>HTML</v-list-item>
|
||||
<v-list-item
|
||||
:href="`/api/project/${$route.params.project}/event/-/${$route.params.sequence}?mime=application%2Fpdf`"
|
||||
title="Download as a Portable Document File"
|
||||
>PDF</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-text-field
|
||||
@@ -43,6 +90,7 @@
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="rows"
|
||||
:items-per-page.sync="itemsPerPage"
|
||||
item-key="tstamp"
|
||||
sort-by="tstamp"
|
||||
:sort-desc="true"
|
||||
@@ -50,6 +98,7 @@
|
||||
:custom-filter="searchTable"
|
||||
:loading="loading"
|
||||
fixed-header
|
||||
:footer-props='{itemsPerPageOptions: [ 10, 25, 50, 100, 500, -1 ]}'
|
||||
>
|
||||
|
||||
<template v-slot:item.tstamp="{value}">
|
||||
@@ -65,7 +114,7 @@
|
||||
@cancel="rowEditorCancel"
|
||||
@open="rowEditorOpen(item)"
|
||||
@close="rowEditorClose"
|
||||
> <div v-html="item.items.map(i => i.remarks).join('<br/>')"></div>
|
||||
> <div v-html="$options.filters.markdownInline(item.items.map(i => i.remarks).join('<br/>'))"></div>
|
||||
<template v-slot:input>
|
||||
<h3>{{
|
||||
editedRow.sequence
|
||||
@@ -306,10 +355,11 @@ export default {
|
||||
},
|
||||
selectedLabels: [],
|
||||
labelSearch: null,
|
||||
queuedReload: false
|
||||
queuedReload: false,
|
||||
itemsPerPage: 25
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
rows () {
|
||||
const rows = {};
|
||||
@@ -350,7 +400,7 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
...mapGetters(['loading', 'online', 'sequence', 'line', 'point', 'lineName', 'serverEvent']),
|
||||
...mapGetters(['user', 'loading', 'online', 'sequence', 'line', 'point', 'lineName', 'serverEvent']),
|
||||
...mapState({projectSchema: state => state.project.projectSchema})
|
||||
|
||||
},
|
||||
@@ -394,6 +444,14 @@ export default {
|
||||
if (!newVal && oldVal && this.queuedReload) {
|
||||
this.getEvents();
|
||||
}
|
||||
},
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
},
|
||||
@@ -515,6 +573,11 @@ export default {
|
||||
const promises = [];
|
||||
|
||||
for (const editedItem of this.editedRow.items) {
|
||||
|
||||
// Process special text in remarks
|
||||
if (editedItem.remarks) {
|
||||
editedItem.remarks = this.$options.filters.position(editedItem.remarks, editedItem);
|
||||
}
|
||||
|
||||
// Discard non user writable labels
|
||||
editedItem.labels = editedItem.labels.filter(l => this.labels[l].model.user);
|
||||
@@ -678,7 +741,7 @@ export default {
|
||||
},
|
||||
|
||||
async mounted () {
|
||||
await this.getLabelDefinitions()
|
||||
await this.getLabelDefinitions();
|
||||
this.getEventCount();
|
||||
this.getEvents();
|
||||
this.getPresetRemarks();
|
||||
|
||||
@@ -31,6 +31,7 @@ import 'leaflet-arrowheads'
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import ftstamp from '@/lib/FormatTimestamp'
|
||||
import zoomFitIcon from '@/assets/zoom-fit-best.svg'
|
||||
import { markdown, markdownInline } from '@/lib/markdown';
|
||||
|
||||
var map;
|
||||
|
||||
@@ -94,7 +95,7 @@ const layers = {
|
||||
},
|
||||
style (feature) {
|
||||
return {
|
||||
color: "magenta",
|
||||
color: "brown",
|
||||
opacity: 0.7
|
||||
}
|
||||
},
|
||||
@@ -108,7 +109,7 @@ const layers = {
|
||||
const speed = (p.length / (new Date(p.ts1) - new Date(p.ts0))) * 3.6/1.852 * 1000;
|
||||
|
||||
const remarks = p.remarks
|
||||
? "<hr/>"+p.remarks
|
||||
? "<hr/>"+markdownInline(p.remarks)
|
||||
: "";
|
||||
|
||||
const popup = `Planned sequence <b>${p.sequence}</b><br/>
|
||||
@@ -146,7 +147,7 @@ const layers = {
|
||||
: "";
|
||||
|
||||
const remarks = p.remarks
|
||||
? "<hr/>"+p.remarks
|
||||
? "<hr/>"+markdown(p.remarks)
|
||||
: "";
|
||||
|
||||
const popup = feature.geometry.type == "Point"
|
||||
@@ -181,7 +182,7 @@ const layers = {
|
||||
const p = feature.properties;
|
||||
|
||||
const remarks = p.remarks
|
||||
? "<hr/>"+p.remarks
|
||||
? "<hr/>"+markdown(p.remarks)
|
||||
: "";
|
||||
|
||||
const popup = feature.geometry.type == "Point"
|
||||
@@ -343,7 +344,7 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['loading', 'serverEvent', 'lineName', 'serverEvent']),
|
||||
...mapGetters(['user', 'loading', 'serverEvent', 'lineName', 'serverEvent']),
|
||||
...mapState({projectSchema: state => state.project.projectSchema})
|
||||
},
|
||||
|
||||
@@ -358,6 +359,12 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
user (newVal, oldVal) {
|
||||
if (newVal && (!oldVal || newVal.name != oldVal.name)) {
|
||||
this.initView();
|
||||
}
|
||||
},
|
||||
|
||||
serverEvent (event) {
|
||||
if (event.channel == "realtime" && event.payload && event.payload.new) {
|
||||
@@ -371,7 +378,7 @@ export default {
|
||||
rtLayer.update(geojson);
|
||||
}
|
||||
} else if (event.channel == "event" && event.payload.schema == this.projectSchema) {
|
||||
console.log("EVENT", event);
|
||||
//console.log("EVENT", event);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -487,6 +494,7 @@ export default {
|
||||
const zoom = map.getZoom();
|
||||
const o = [];
|
||||
const l = [];
|
||||
let value;
|
||||
if (includeLayers) {
|
||||
for (const overlay of Object.keys(tileMaps)) {
|
||||
if (map.hasLayer(tileMaps[overlay])) {
|
||||
@@ -498,14 +506,24 @@ export default {
|
||||
l.push(layer);
|
||||
}
|
||||
}
|
||||
document.location.hash = `${zoom}/${lat.toFixed(4)}/${lng.toFixed(4)}:${o.join(";")}:${l.join(";")}`;
|
||||
value = `${zoom}/${lat}/${lng}:${o.join(";")}:${l.join(";")}`;
|
||||
} else {
|
||||
document.location.hash = `${zoom}/${lat.toFixed(4)}/${lng.toFixed(4)}`;
|
||||
value = `${zoom}/${lat}/${lng}`;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
localStorage.setItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/view`, value);
|
||||
}
|
||||
},
|
||||
|
||||
decodeURL () {
|
||||
const parts = document.location.hash.substring(1).split(":").map(p => decodeURIComponent(p));
|
||||
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(";");
|
||||
let position = parts && parts[0].split("/").map(i => Number(i));
|
||||
@@ -515,6 +533,60 @@ 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];
|
||||
if (init.activeOverlays.includes(k)) {
|
||||
if (!map.hasLayer(l)) {
|
||||
l.addTo(map);
|
||||
}
|
||||
} else {
|
||||
map.removeLayer(l);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
tileMaps["No background"].addTo(map);
|
||||
}
|
||||
|
||||
if (init.activeLayers) {
|
||||
Object.keys(layers).forEach(k => {
|
||||
const l = layers[k];
|
||||
if (init.activeLayers.includes(k)) {
|
||||
if (!map.hasLayer(l)) {
|
||||
l.addTo(map);
|
||||
}
|
||||
} else {
|
||||
map.removeLayer(l);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
layers.OpenSeaMap.addTo(map);
|
||||
layers.Preplots.addTo(map);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
},
|
||||
|
||||
...mapActions(["api"])
|
||||
|
||||
@@ -541,8 +613,8 @@ export default {
|
||||
onEachFeature (feature, layer) {
|
||||
const p = feature.properties;
|
||||
const popup = (p.sequence
|
||||
? `Event @ ${p.tstamp}<br/>Sequence ${p.sequence}<br/>Point <b>${p.line} / ${p.point}</b><br/><hr/>${p.remarks}`
|
||||
: `Event @ ${p.tstamp}<br/><hr/>${p.remarks}`)
|
||||
? `Event @ ${p.tstamp}<br/>Sequence ${p.sequence}<br/>Point <b>${p.line} / ${p.point}</b><br/><hr/>${markdownInline(p.remarks)}`
|
||||
: `Event @ ${p.tstamp}<br/><hr/>${markdownInline(p.remarks)}`)
|
||||
+ (p.labels.length? `<br/>[<i>${p.labels.join(", ")}</i>]` : "");
|
||||
layer.bindTooltip(popup, {sticky: true});
|
||||
}
|
||||
@@ -553,25 +625,25 @@ export default {
|
||||
layers["Events (Other)"] = L.realtime(this.getEvents(i => i.properties.type != "qc"), eventsOptions());
|
||||
|
||||
layers["Events (Other)"].on('update', function (e) {
|
||||
console.log("Events (Other) update event", e);
|
||||
//console.log("Events (Other) update event", e);
|
||||
});
|
||||
|
||||
layers["Events (QC)"].on('add', function (e) {
|
||||
console.log("Events (QC) add event", e);
|
||||
//console.log("Events (QC) add event", e);
|
||||
e.target._src(data => e.target.update(data), err => console.error)
|
||||
});
|
||||
|
||||
layers["Events (QC)"].on('remove', function (e) {
|
||||
console.log("Events (QC) remove event", e);
|
||||
//console.log("Events (QC) remove event", e);
|
||||
});
|
||||
|
||||
layers["Events (Other)"].on('add', function (e) {
|
||||
console.log("Events (Other) add event", e);
|
||||
//console.log("Events (Other) add event", e);
|
||||
e.target._src(data => e.target.update(data), err => console.error)
|
||||
});
|
||||
|
||||
layers["Events (Other)"].on('remove', function (e) {
|
||||
console.log("Events (Other) remove event", e);
|
||||
//console.log("Events (Other) remove event", e);
|
||||
});
|
||||
|
||||
|
||||
@@ -627,7 +699,11 @@ export default {
|
||||
if (init.position) {
|
||||
this.refreshLayers();
|
||||
} else {
|
||||
this.fitProjectBounds();
|
||||
setTimeout(() => {
|
||||
if(!this.decodeURL().position) {
|
||||
this.fitProjectBounds();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// /usr/share/icons/breeze/actions/16/zoom-fit-best.svg
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="items"
|
||||
:items-per-page.sync="itemsPerPage"
|
||||
:search="filter"
|
||||
:loading="loading"
|
||||
:fixed-header="true"
|
||||
@@ -180,8 +181,7 @@
|
||||
@click:append-outer="edit = null"
|
||||
>
|
||||
</v-text-field>
|
||||
<div v-else>
|
||||
{{item.remarks}}
|
||||
<div v-else v-html="$options.filters.markdownInline(item.remarks)">
|
||||
<v-btn v-if="edit === null"
|
||||
icon
|
||||
small
|
||||
@@ -323,6 +323,7 @@ export default {
|
||||
activeItem: null,
|
||||
edit: null, // {sequence, key, value}
|
||||
queuedReload: false,
|
||||
itemsPerPage: 25,
|
||||
|
||||
plannerConfig: null,
|
||||
shiftAll: false, // Shift all sequences checkbox
|
||||
@@ -336,7 +337,7 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['loading', 'serverEvent'])
|
||||
...mapGetters(['user', 'loading', 'serverEvent'])
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -401,6 +402,14 @@ export default {
|
||||
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;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
color="primary"
|
||||
title="Accept all"
|
||||
@click.stop="accept(item)">
|
||||
<v-icon small>mdi-check-all</v-icon>
|
||||
<v-icon small :color="accepted(item) ? 'green' : ''">mdi-check-all</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:class="{'text--disabled': !hover}"
|
||||
@@ -117,7 +117,7 @@
|
||||
color="primary"
|
||||
title="Accept this value"
|
||||
@click="accept(item)">
|
||||
<v-icon small>mdi-check</v-icon>
|
||||
<v-icon small :color="(item.children && item.children.length == 0)? 'green':''">mdi-check</v-icon>
|
||||
</v-btn>
|
||||
</v-hover>
|
||||
</template>
|
||||
@@ -260,6 +260,17 @@ export default {
|
||||
}
|
||||
return sum;
|
||||
},
|
||||
|
||||
accepted (item) {
|
||||
if (item.children) {
|
||||
return item.children.every(child => this.accepted(child));
|
||||
}
|
||||
|
||||
if (item.labels) {
|
||||
return item.labels.includes("QCAccepted");
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
accept (item) {
|
||||
if (item.children) {
|
||||
@@ -478,7 +489,7 @@ export default {
|
||||
if (res) {
|
||||
this.items = res.results.map(i => this.transform(i)) || [];
|
||||
this.updatedOn = res.updatedOn;
|
||||
this.getQCLabels(); // No await?
|
||||
await this.getQCLabels();
|
||||
} else {
|
||||
this.items = [];
|
||||
this.updatedOn = null;
|
||||
|
||||
@@ -39,12 +39,14 @@
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="items"
|
||||
:items-per-page.sync="itemsPerPage"
|
||||
item-key="sequence"
|
||||
:server-items-length="num_rows"
|
||||
:search="filter"
|
||||
:custom-filter="customFilter"
|
||||
:loading="loading"
|
||||
:fixed-header="true"
|
||||
:footer-props='{itemsPerPageOptions: [ 10, 25, 50, 100, 500, -1 ]}'
|
||||
show-expand
|
||||
:item-class="(item) => activeItem == item ? 'blue accent-1 elevation-3' : ''"
|
||||
@click:row="setActiveItem"
|
||||
@@ -84,6 +86,7 @@
|
||||
</v-card-subtitle>
|
||||
<v-card-text v-if="edit && edit.sequence == item.sequence && edit.key == 'remarks'">
|
||||
<v-textarea
|
||||
class="markdown"
|
||||
autofocus
|
||||
placeholder="Enter your text here"
|
||||
:disabled="loading"
|
||||
@@ -91,8 +94,7 @@
|
||||
>
|
||||
</v-textarea>
|
||||
</v-card-text>
|
||||
<v-card-text v-else>
|
||||
{{ item.remarks }}
|
||||
<v-card-text v-else v-html="$options.filters.markdown(item.remarks)">
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-card outlined class="flex-grow-1" v-if="item.remarks_final !== null">
|
||||
@@ -123,6 +125,7 @@
|
||||
</v-card-subtitle>
|
||||
<v-card-text v-if="edit && edit.sequence == item.sequence && edit.key == 'remarks_final'">
|
||||
<v-textarea
|
||||
class="markdown"
|
||||
autofocus
|
||||
placeholder="Enter your text here"
|
||||
:disabled="loading"
|
||||
@@ -130,8 +133,7 @@
|
||||
>
|
||||
</v-textarea>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
{{ item.remarks_final }}
|
||||
<v-card-text v-html="$options.filters.markdown(item.remarks_final)">
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@@ -397,6 +399,7 @@ export default {
|
||||
activeItem: null,
|
||||
edit: null, // {sequence, key, value}
|
||||
queuedReload: false,
|
||||
itemsPerPage: 25,
|
||||
|
||||
// Planner related stuff
|
||||
preplots: null,
|
||||
@@ -411,7 +414,7 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['loading', 'serverEvent'])
|
||||
...mapGetters(['user', 'loading', 'serverEvent'])
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -460,6 +463,14 @@ export default {
|
||||
if (!newVal && oldVal && this.queuedReload) {
|
||||
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;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
@@ -137,6 +137,9 @@ app.map({
|
||||
post: [ mw.auth.access.write, mw.event.post ],
|
||||
put: [ mw.auth.access.write, mw.event.put ],
|
||||
delete: [ mw.auth.access.write, mw.event.delete ],
|
||||
'-/:sequence/': { // NOTE: We need to avoid conflict with the next endpoint ☹
|
||||
get: [ mw.event.get ],
|
||||
},
|
||||
':type/': {
|
||||
':id/': {
|
||||
// get: [ mw.event.get ],
|
||||
@@ -183,6 +186,9 @@ app.map({
|
||||
get: [ mw.gis.navdata.get ]
|
||||
}
|
||||
},
|
||||
'/rss/': {
|
||||
get: [ mw.rss.get ]
|
||||
}
|
||||
//
|
||||
// '/user': {
|
||||
// get: [ mw.user.get ],
|
||||
@@ -206,8 +212,10 @@ app.use(function (err, req, res, next) {
|
||||
|
||||
console.log("Error:", err);
|
||||
|
||||
res.set("Content-Type", "application/json");
|
||||
if (err instanceof Error && err.name != "UnauthorizedError") {
|
||||
console.error(err.stack);
|
||||
res.set("Content-Type", "text/plain");
|
||||
res.status(500).send('General internal error');
|
||||
maybeSendAlert(alert);
|
||||
} else if (typeof err === 'string') {
|
||||
|
||||
28
lib/www/server/api/middleware/event/get/geojson.js
Normal file
28
lib/www/server/api/middleware/event/get/geojson.js
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
const { event } = require('../../../../lib/db');
|
||||
|
||||
const geojson = async function (req, res, next) {
|
||||
try {
|
||||
const query = req.query;
|
||||
query.sequence = req.params.sequence;
|
||||
const events = await event.list(req.params.project, query);
|
||||
const response = {
|
||||
type: "FeatureCollection",
|
||||
features: events.filter(event => event.geometry).map(event => {
|
||||
const feature = {
|
||||
type: "Feature",
|
||||
geometry: event.geometry,
|
||||
properties: event
|
||||
};
|
||||
delete feature.properties.geometry;
|
||||
return feature;
|
||||
})
|
||||
};
|
||||
res.status(200).send(response);
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = geojson;
|
||||
27
lib/www/server/api/middleware/event/get/html.js
Normal file
27
lib/www/server/api/middleware/event/get/html.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const { event, sequence, configuration } = require('../../../../lib/db');
|
||||
const { transform } = require('../../../../lib/sse');
|
||||
const render = require('../../../../lib/render');
|
||||
|
||||
const html = async function (req, res, next) {
|
||||
try {
|
||||
const query = req.query;
|
||||
query.sequence = req.params.sequence;
|
||||
const events = await event.list(req.params.project, query);
|
||||
const sequences = await sequence.list(req.params.project, query);
|
||||
const seis = transform(events, sequences, {projectId: req.params.project});
|
||||
const templates = await configuration.get(req.params.project, "sse/templates");
|
||||
const template = templates[0].template;
|
||||
|
||||
const response = await render(seis, template);
|
||||
res.status(200).send(response);
|
||||
next();
|
||||
} catch (err) {
|
||||
if (err.message.startsWith("template")) {
|
||||
next({message: err.message});
|
||||
} else {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = html;
|
||||
29
lib/www/server/api/middleware/event/get/index.js
Normal file
29
lib/www/server/api/middleware/event/get/index.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const json = require('./json');
|
||||
const geojson = require('./geojson');
|
||||
const seis = require('./seis');
|
||||
const html = require('./html');
|
||||
const pdf = require('./pdf');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
try {
|
||||
const handlers = {
|
||||
"application/json": json,
|
||||
"application/geo+json": geojson,
|
||||
"application/vnd.seis+json": seis,
|
||||
"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);
|
||||
} else {
|
||||
res.status(406).send();
|
||||
next();
|
||||
}
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
16
lib/www/server/api/middleware/event/get/json.js
Normal file
16
lib/www/server/api/middleware/event/get/json.js
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
const { event } = require('../../../../lib/db');
|
||||
|
||||
const json = async function (req, res, next) {
|
||||
try {
|
||||
const query = req.query;
|
||||
query.sequence = req.params.sequence;
|
||||
const response = await event.list(req.params.project, query);
|
||||
res.status(200).send(response);
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = json;
|
||||
42
lib/www/server/api/middleware/event/get/pdf.js
Normal file
42
lib/www/server/api/middleware/event/get/pdf.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const fs = require('fs/promises');
|
||||
const Path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { event, sequence, configuration } = require('../../../../lib/db');
|
||||
const { transform } = require('../../../../lib/sse');
|
||||
const render = require('../../../../lib/render');
|
||||
const { url2pdf } = require('../../../../lib/selenium');
|
||||
|
||||
function tmpname (tmpdir="/dev/shm") {
|
||||
return Path.join(tmpdir, crypto.randomBytes(16).toString('hex')+".tmp");
|
||||
}
|
||||
|
||||
const pdf = async function (req, res, next) {
|
||||
const fname = tmpname();
|
||||
try {
|
||||
const query = req.query;
|
||||
query.sequence = req.params.sequence;
|
||||
const events = await event.list(req.params.project, query);
|
||||
const sequences = await sequence.list(req.params.project, query);
|
||||
const seis = transform(events, sequences, {projectId: req.params.project});
|
||||
const templates = await configuration.get(req.params.project, "sse/templates");
|
||||
const template = templates[0].template;
|
||||
|
||||
const html = await render(seis, template);
|
||||
|
||||
await fs.writeFile(fname, html);
|
||||
const pdf = Buffer.from(await url2pdf("file://"+fname), "base64");
|
||||
|
||||
res.status(200).send(pdf);
|
||||
next();
|
||||
} catch (err) {
|
||||
if (err.message.startsWith("template")) {
|
||||
next({message: err.message});
|
||||
} else {
|
||||
next(err);
|
||||
}
|
||||
} finally {
|
||||
await fs.unlink(fname);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = pdf;
|
||||
18
lib/www/server/api/middleware/event/get/seis.js
Normal file
18
lib/www/server/api/middleware/event/get/seis.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const { event, sequence } = require('../../../../lib/db');
|
||||
const { transform } = require('../../../../lib/sse');
|
||||
|
||||
const seis = async function (req, res, next) {
|
||||
try {
|
||||
const query = req.query;
|
||||
query.sequence = req.params.sequence;
|
||||
const events = await event.list(req.params.project, query);
|
||||
const sequences = await sequence.list(req.params.project, query);
|
||||
const response = transform(events, sequences, {projectId: req.params.project});
|
||||
res.status(200).send(response);
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = seis;
|
||||
@@ -1,291 +1,5 @@
|
||||
|
||||
const { event, sequence } = require('../../../../lib/db');
|
||||
|
||||
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 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()) {
|
||||
const SequenceNumber = event.sequence;
|
||||
if (SequenceNumber) {
|
||||
const sequence = sequences.find(s => s.sequence == SequenceNumber);
|
||||
if (!sequence) {
|
||||
throw Error(`Sequence ${SequenceNumber} not found in sequence list`);
|
||||
}
|
||||
|
||||
let SequenceObject = output.Sequences.find(s => s.SequenceNumber == SequenceNumber);
|
||||
if (!SequenceObject) {
|
||||
SequenceObject = {
|
||||
SequenceNumber,
|
||||
Entries: [],
|
||||
DglNumPoints: sequence.num_points,
|
||||
DglNumMissing: sequence.missing_shots,
|
||||
// NOTE: Distance & azimuth refer to raw data if the sequence
|
||||
// status is 'raw' and to final data if status is 'final'. In
|
||||
// the event of it being NTBP it depends on whether final data
|
||||
// exists or not.
|
||||
DglLength: sequence.length
|
||||
};
|
||||
[sequence.remarks, sequence.remarks_final].filter(i => !!i).forEach(i => {
|
||||
if (!SequenceObject.DglSequenceComments) {
|
||||
SequenceObject.DglSequenceComments = []
|
||||
}
|
||||
SequenceObject.DglSequenceComments.push(i);
|
||||
});
|
||||
|
||||
output.Sequences.push(SequenceObject);
|
||||
}
|
||||
|
||||
if (event.labels.includes("FSP")) {
|
||||
const entry = {
|
||||
"EntryType": "Start of line recording",
|
||||
"EntryTypeId": 3000,
|
||||
"Comment": event.remarks.toString(),
|
||||
"Heading": Number(sequence.azimuth.toFixed(1)),
|
||||
"IsReshoot": sequence.reshoot, // FIXME Add this property to sequence object
|
||||
"IsUndershoot": false,
|
||||
// NOTE https://gitlab.com/wgp/dougal/software/-/issues/12#note_419162674
|
||||
"LineNumber": sequence.line.toString(),
|
||||
"LineType": "Prime", // FIXME
|
||||
"ShotPointId": event.point,
|
||||
"Time": event.tstamp
|
||||
}
|
||||
|
||||
SequenceObject.Entries.push(entry);
|
||||
}
|
||||
|
||||
if (event.labels.includes("FGSP")) {
|
||||
const entry = {
|
||||
"EntryType": "Start good",
|
||||
"EntryTypeId": 3001,
|
||||
"Comment": event.remarks.toString(),
|
||||
"ShotPointId": event.point,
|
||||
"Time": event.tstamp
|
||||
}
|
||||
|
||||
SequenceObject.Entries.push(entry);
|
||||
}
|
||||
|
||||
if (event.labels.includes("FCSP")) {
|
||||
const entry = {
|
||||
"EntryType": "Start charged",
|
||||
"EntryTypeId": 3002,
|
||||
"Comment": event.remarks.toString(),
|
||||
"ShotPointId": event.point,
|
||||
"Time": event.tstamp
|
||||
}
|
||||
|
||||
SequenceObject.Entries.push(entry);
|
||||
}
|
||||
|
||||
if (event.labels.includes("LGFFSP")) {
|
||||
const entry = {
|
||||
"EntryType": "Last good Full Fold",
|
||||
"EntryTypeId": 3008,
|
||||
"Comment": event.remarks.toString(),
|
||||
"ShotPointId": event.point,
|
||||
"Time": event.tstamp
|
||||
}
|
||||
|
||||
SequenceObject.Entries.push(entry);
|
||||
}
|
||||
|
||||
if (event.labels.includes("LCFFSP")) {
|
||||
const entry = {
|
||||
"EntryType": "Last charged Full Fold",
|
||||
"EntryTypeId": 3009,
|
||||
"Comment": event.remarks.toString(),
|
||||
"ShotPointId": event.point,
|
||||
"Time": event.tstamp
|
||||
}
|
||||
|
||||
SequenceObject.Entries.push(entry);
|
||||
}
|
||||
|
||||
if (event.labels.includes("FDSP")) {
|
||||
const entry = {
|
||||
"EntryType": "Midnight",
|
||||
"EntryTypeId": 3003,
|
||||
"Comment": event.remarks.toString(),
|
||||
"ShotPointId": event.point,
|
||||
"Time": event.tstamp
|
||||
}
|
||||
|
||||
SequenceObject.Entries.push(entry);
|
||||
}
|
||||
|
||||
if (event.labels.includes("LCSP")) {
|
||||
const entry = {
|
||||
"EntryType": "End charged",
|
||||
"EntryTypeId": 3005,
|
||||
"Comment": event.remarks.toString(),
|
||||
"ShotPointId": event.point,
|
||||
"Time": event.tstamp
|
||||
}
|
||||
|
||||
SequenceObject.Entries.push(entry);
|
||||
}
|
||||
|
||||
if (event.labels.includes("LGSP")) {
|
||||
const entry = {
|
||||
"EntryType": "End good",
|
||||
"EntryTypeId": 3006,
|
||||
"Comment": event.remarks.toString(),
|
||||
"ShotPointId": event.point,
|
||||
"Time": event.tstamp
|
||||
}
|
||||
|
||||
SequenceObject.Entries.push(entry);
|
||||
}
|
||||
|
||||
if (event.labels.includes("LSP")) {
|
||||
const entry = {
|
||||
"EntryType": "End of line recording",
|
||||
"EntryTypeId": 3007,
|
||||
"Comment": event.remarks.toString(),
|
||||
"IsNTBP": sequence.status == "ntbp",
|
||||
// NOTE: Always pending, see
|
||||
// https://gitlab.com/wgp/dougal/software/-/issues/12
|
||||
"LineStatus": "Pending",
|
||||
"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")) {
|
||||
const entry = {
|
||||
"EntryType": "QC",
|
||||
"Comment": event.remarks.toString(),
|
||||
"ShotPointId": event.point,
|
||||
"Time": event.tstamp
|
||||
}
|
||||
SequenceObject.Entries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (exportGeneral) {
|
||||
// These are labels that we have already (potentially) exported
|
||||
const excluded = [
|
||||
"FSP", "FGSP", "FCSP", "LGFFSP",
|
||||
"LCFFSP", "LCSP", "LGSP", "LSP",
|
||||
"LDSP", "FDSP", "QC", "QCAccepted",
|
||||
"Internal", "Private", "Confidential",
|
||||
"NoExport"
|
||||
];
|
||||
if (!event.labels || !event.labels.some( l => excluded.includes(l) )) {
|
||||
const entry = {
|
||||
"Comment": event.remarks.toString(),
|
||||
"ShotPointId": event.point,
|
||||
"Time": event.tstamp
|
||||
}
|
||||
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);
|
||||
if (fgspIndex != -1) {
|
||||
const fsp = Object.assign({}, SequenceObject.Entries[fgspIndex], {
|
||||
"EntryType": "Start of line recording",
|
||||
"EntryTypeId": 3000,
|
||||
"Heading": Number(sequence.azimuth.toFixed(1)),
|
||||
"IsReshoot": sequence.reshoot,
|
||||
"IsUndershoot": false,
|
||||
"LineNumber": sequence.line.toString(),
|
||||
"LineType": "Prime", // FIXME
|
||||
});
|
||||
SequenceObject.Entries.splice(fgspIndex, 0, fsp);
|
||||
} else {
|
||||
// We have neither an FSP nor an FGSP, let us take the first
|
||||
// preplot shot from the sequence data as save that as FSP
|
||||
// (we intentionally omit adding an FGSP as we know nothing
|
||||
// about the user's intent).
|
||||
const fsp = {
|
||||
"EntryType": "Start of line recording",
|
||||
"EntryTypeId": 3000,
|
||||
"Comment": sequence.fsp_final
|
||||
? "(First preplot shot found in P1)"
|
||||
: "(First shot found in P1)",
|
||||
"Heading": Number(sequence.azimuth.toFixed(1)),
|
||||
"IsReshoot": sequence.reshoot, // FIXME Add this property to sequence object
|
||||
"IsUndershoot": false,
|
||||
// NOTE https://gitlab.com/wgp/dougal/software/-/issues/12#note_419162674
|
||||
"LineNumber": sequence.line.toString(),
|
||||
"LineType": "Prime", // FIXME
|
||||
"ShotPointId": sequence.fsp_final || sequence.fsp,
|
||||
"Time": sequence.ts0_final || sequence.ts0
|
||||
}
|
||||
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);
|
||||
if (lgspIndex != -1) {
|
||||
const lsp = Object.assign({}, SequenceObject.Entries[lgspIndex], {
|
||||
"EntryType": "End of line recording",
|
||||
"EntryTypeId": 3007,
|
||||
"IsNTBP": sequence.status == "ntbp",
|
||||
"LineStatus": "Pending",
|
||||
});
|
||||
SequenceObject.Entries.splice(lgspIndex+1, 0, lsp);
|
||||
} else {
|
||||
// See comment above concerning FSP
|
||||
const lsp = {
|
||||
"EntryType": "End of line recording",
|
||||
"EntryTypeId": 3007,
|
||||
"Comment": sequence.lsp_final
|
||||
? "(Last preplot shot found in P1)"
|
||||
: "(Last shot found in P1)",
|
||||
"IsNTBP": sequence.status == "ntbp",
|
||||
// NOTE: Always pending, see
|
||||
// https://gitlab.com/wgp/dougal/software/-/issues/12
|
||||
"LineStatus": "Pending",
|
||||
"ShotPointId": sequence.lsp_final || sequence.lsp,
|
||||
"Time": sequence.ts1_final || sequence.ts1
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
const { transform } = require('../../../../lib/sse');
|
||||
|
||||
const seis = async function (req, res, next) {
|
||||
try {
|
||||
|
||||
@@ -12,5 +12,6 @@ module.exports = {
|
||||
configuration: require('./configuration'),
|
||||
info: require('./info'),
|
||||
meta: require('./meta'),
|
||||
openapi: require('./openapi')
|
||||
openapi: require('./openapi'),
|
||||
rss: require('./rss')
|
||||
};
|
||||
|
||||
21
lib/www/server/api/middleware/rss/get.js
Normal file
21
lib/www/server/api/middleware/rss/get.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
module.exports = async function (req, res, next) {
|
||||
|
||||
try {
|
||||
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");
|
||||
res.send(await r.text());
|
||||
return;
|
||||
}
|
||||
}
|
||||
res.status(400).send();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
4
lib/www/server/api/middleware/rss/index.js
Normal file
4
lib/www/server/api/middleware/rss/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
module.exports = {
|
||||
get: require('./get')
|
||||
}
|
||||
@@ -1,5 +1,22 @@
|
||||
const { setSurvey } = require('../connection');
|
||||
|
||||
const { geometryAsString } = require('../../utils');
|
||||
|
||||
|
||||
function replaceMarkers (item, opts={}) {
|
||||
const textkey = opts.text || "remarks";
|
||||
|
||||
const text = item[textkey];
|
||||
|
||||
if (text && typeof text === "string") {
|
||||
item[textkey] = text
|
||||
.replace(/@POS(ITION)?@/g, geometryAsString(item, opts) || "(position unknown)")
|
||||
.replace(/@DMS@/g, geometryAsString(item, {...opts, dms:true}) || "(position unknown)")
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
async function list (projectId, opts = {}) {
|
||||
const client = await setSurvey(projectId);
|
||||
|
||||
@@ -29,7 +46,7 @@ async function list (projectId, opts = {}) {
|
||||
|
||||
const res = await client.query(text, filter[1]);
|
||||
client.release();
|
||||
return res.rows;
|
||||
return res.rows.map(i => replaceMarkers(i));
|
||||
}
|
||||
|
||||
module.exports = list;
|
||||
|
||||
@@ -45,7 +45,7 @@ async function list (projectId, opts = {}) {
|
||||
AND UNBOUNDED FOLLOWING
|
||||
)
|
||||
)
|
||||
SELECT s.*, incr, remarks, pl.ntba
|
||||
SELECT s.*, incr, remarks, pl.ntba, pl.meta
|
||||
FROM summary s
|
||||
INNER JOIN preplot_lines pl ON pl.class = 'V' AND s.line = pl.line
|
||||
ORDER BY ${sortKey} ${sortDir}
|
||||
|
||||
@@ -92,7 +92,7 @@ async function getLineName (client, projectId, payload) {
|
||||
const attempt = planned.filter(r => r.line == payload.line).concat(previous).length;
|
||||
const p = payload;
|
||||
const incr = p.lsp > p.fsp;
|
||||
const sequence = p.sequence;
|
||||
const sequence = p.sequence || 1;
|
||||
const line = p.line;
|
||||
return `${incr?"1":"2"}0${line}${attempt}${sequence.toString().padStart(3, "0")}S00000`;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,12 @@ async function byShot (client) {
|
||||
async function bySequence (client) {
|
||||
console.error("bySequence");
|
||||
const text = `
|
||||
SELECT sequence _id, 'raw' AS type, *
|
||||
SELECT sequence _id, 'raw' AS type, *,
|
||||
EXISTS(SELECT 1
|
||||
FROM raw_shots
|
||||
WHERE sequence = ss.sequence
|
||||
HAVING count(*) FILTER (WHERE meta ? 'smsrc') > 0
|
||||
) has_smsrc_data
|
||||
FROM sequences_summary ss
|
||||
WHERE NOT EXISTS (
|
||||
SELECT sequence
|
||||
|
||||
53
lib/www/server/lib/render.js
Normal file
53
lib/www/server/lib/render.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const Path = require('path');
|
||||
const nunjucks = require('nunjucks');
|
||||
const marked = require('marked');
|
||||
|
||||
function njkFind (ary, key, value) {
|
||||
if (!ary.find) {
|
||||
return ary[key];
|
||||
}
|
||||
if (typeof value == 'undefined') {
|
||||
return ary.find(i => i.hasOwnAttribute(key));
|
||||
} else {
|
||||
return ary.find(i => i[key] == value);
|
||||
}
|
||||
}
|
||||
|
||||
function njkPadStart (str, len, chr) {
|
||||
return String(str).padStart(len, chr);
|
||||
}
|
||||
|
||||
function njkTimestamp (arg) {
|
||||
if (typeof arg.toISOString === "function") {
|
||||
return arg.toISOString();
|
||||
}
|
||||
const ts = new Date(arg);
|
||||
if (!isNaN(ts)) {
|
||||
return ts.toISOString();
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
|
||||
function njkMarkdown (str) {
|
||||
return marked(String(str));
|
||||
}
|
||||
|
||||
function njkMarkdownInline (str) {
|
||||
return marked.parseInline(String(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('padStart', njkPadStart);
|
||||
nenv.addFilter('timestamp', njkTimestamp);
|
||||
nenv.addFilter('markdown', njkMarkdown);
|
||||
nenv.addFilter('markdownInline', njkMarkdownInline);
|
||||
|
||||
const view = nenv.render(Path.basename(template), data);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
module.exports = render;
|
||||
BIN
lib/www/server/lib/selenium/geckodriver
Executable file
BIN
lib/www/server/lib/selenium/geckodriver
Executable file
Binary file not shown.
52
lib/www/server/lib/selenium/index.js
Normal file
52
lib/www/server/lib/selenium/index.js
Normal file
@@ -0,0 +1,52 @@
|
||||
// TODO Append location to PATH
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const {Builder, By, Key, until} = require('selenium-webdriver');
|
||||
const firefox = require('selenium-webdriver/firefox');
|
||||
|
||||
const geckodriverPath = path.resolve(__dirname, "geckodriver");
|
||||
|
||||
// We launch a browser instance and then start an activity timer.
|
||||
// We shut down the browser after a period of inactivity, to
|
||||
// save memory.
|
||||
let driver = null;
|
||||
let timer = null;
|
||||
|
||||
function resetTimer () {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(shutdown, 120000); // Yup, hardcoded to two minutes. For now anyway
|
||||
}
|
||||
|
||||
async function launch () {
|
||||
resetTimer();
|
||||
if (!driver) {
|
||||
console.log("Launching Firefox");
|
||||
const options = new firefox.Options();
|
||||
driver = await new Builder()
|
||||
.forBrowser('firefox')
|
||||
.setFirefoxService(new firefox.ServiceBuilder(geckodriverPath))
|
||||
.setFirefoxOptions(options.headless())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
async function shutdown () {
|
||||
if (driver) {
|
||||
console.log("Shutting down Firefox");
|
||||
// This is an attempt at avoiding a race condition if someone
|
||||
// makes a call and resets the timer while the shutdown is in
|
||||
// progress.
|
||||
const d = driver;
|
||||
driver = null;
|
||||
await d.quit();
|
||||
}
|
||||
}
|
||||
|
||||
async function url2pdf (url) {
|
||||
await launch();
|
||||
await driver.get(url);
|
||||
return await driver.printPage({width: 21.0, height: 29.7});
|
||||
}
|
||||
|
||||
module.exports = { url2pdf };
|
||||
3
lib/www/server/lib/sse/index.js
Normal file
3
lib/www/server/lib/sse/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
transform: require('./transform')
|
||||
}
|
||||
289
lib/www/server/lib/sse/transform.js
Normal file
289
lib/www/server/lib/sse/transform.js
Normal file
@@ -0,0 +1,289 @@
|
||||
|
||||
|
||||
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 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()) {
|
||||
const SequenceNumber = event.sequence;
|
||||
if (SequenceNumber) {
|
||||
const sequence = sequences.find(s => s.sequence == SequenceNumber);
|
||||
if (!sequence) {
|
||||
throw Error(`Sequence ${SequenceNumber} not found in sequence list`);
|
||||
}
|
||||
|
||||
let SequenceObject = output.Sequences.find(s => s.SequenceNumber == SequenceNumber);
|
||||
if (!SequenceObject) {
|
||||
SequenceObject = {
|
||||
SequenceNumber,
|
||||
Entries: [],
|
||||
DglNumPoints: sequence.num_points,
|
||||
DglNumMissing: sequence.missing_shots,
|
||||
// NOTE: Distance & azimuth refer to raw data if the sequence
|
||||
// status is 'raw' and to final data if status is 'final'. In
|
||||
// the event of it being NTBP it depends on whether final data
|
||||
// exists or not.
|
||||
DglLength: sequence.length
|
||||
};
|
||||
[sequence.remarks, sequence.remarks_final].filter(i => !!i).forEach(i => {
|
||||
if (!SequenceObject.DglSequenceComments) {
|
||||
SequenceObject.DglSequenceComments = []
|
||||
}
|
||||
SequenceObject.DglSequenceComments.push(i);
|
||||
});
|
||||
|
||||
output.Sequences.push(SequenceObject);
|
||||
}
|
||||
|
||||
if (event.labels.includes("FSP")) {
|
||||
const entry = {
|
||||
"EntryType": "Start of line recording",
|
||||
"EntryTypeId": 3000,
|
||||
"Comment": event.remarks.toString(),
|
||||
"Heading": Number(sequence.azimuth.toFixed(1)),
|
||||
"IsReshoot": sequence.reshoot, // FIXME Add this property to sequence object
|
||||
"IsUndershoot": false,
|
||||
// NOTE https://gitlab.com/wgp/dougal/software/-/issues/12#note_419162674
|
||||
"LineNumber": sequence.line.toString(),
|
||||
"LineType": "Prime", // FIXME
|
||||
"ShotPointId": event.point,
|
||||
"Time": event.tstamp
|
||||
}
|
||||
|
||||
SequenceObject.Entries.push(entry);
|
||||
}
|
||||
|
||||
if (event.labels.includes("FGSP")) {
|
||||
const entry = {
|
||||
"EntryType": "Start good",
|
||||
"EntryTypeId": 3001,
|
||||
"Comment": event.remarks.toString(),
|
||||
"ShotPointId": event.point,
|
||||
"Time": event.tstamp
|
||||
}
|
||||
|
||||
SequenceObject.Entries.push(entry);
|
||||
}
|
||||
|
||||
if (event.labels.includes("FCSP")) {
|
||||
const entry = {
|
||||
"EntryType": "Start charged",
|
||||
"EntryTypeId": 3002,
|
||||
"Comment": event.remarks.toString(),
|
||||
"ShotPointId": event.point,
|
||||
"Time": event.tstamp
|
||||
}
|
||||
|
||||
SequenceObject.Entries.push(entry);
|
||||
}
|
||||
|
||||
if (event.labels.includes("LGFFSP")) {
|
||||
const entry = {
|
||||
"EntryType": "Last good Full Fold",
|
||||
"EntryTypeId": 3008,
|
||||
"Comment": event.remarks.toString(),
|
||||
"ShotPointId": event.point,
|
||||
"Time": event.tstamp
|
||||
}
|
||||
|
||||
SequenceObject.Entries.push(entry);
|
||||
}
|
||||
|
||||
if (event.labels.includes("LCFFSP")) {
|
||||
const entry = {
|
||||
"EntryType": "Last charged Full Fold",
|
||||
"EntryTypeId": 3009,
|
||||
"Comment": event.remarks.toString(),
|
||||
"ShotPointId": event.point,
|
||||
"Time": event.tstamp
|
||||
}
|
||||
|
||||
SequenceObject.Entries.push(entry);
|
||||
}
|
||||
|
||||
if (event.labels.includes("FDSP")) {
|
||||
const entry = {
|
||||
"EntryType": "Midnight",
|
||||
"EntryTypeId": 3003,
|
||||
"Comment": event.remarks.toString(),
|
||||
"ShotPointId": event.point,
|
||||
"Time": event.tstamp
|
||||
}
|
||||
|
||||
SequenceObject.Entries.push(entry);
|
||||
}
|
||||
|
||||
if (event.labels.includes("LCSP")) {
|
||||
const entry = {
|
||||
"EntryType": "End charged",
|
||||
"EntryTypeId": 3005,
|
||||
"Comment": event.remarks.toString(),
|
||||
"ShotPointId": event.point,
|
||||
"Time": event.tstamp
|
||||
}
|
||||
|
||||
SequenceObject.Entries.push(entry);
|
||||
}
|
||||
|
||||
if (event.labels.includes("LGSP")) {
|
||||
const entry = {
|
||||
"EntryType": "End good",
|
||||
"EntryTypeId": 3006,
|
||||
"Comment": event.remarks.toString(),
|
||||
"ShotPointId": event.point,
|
||||
"Time": event.tstamp
|
||||
}
|
||||
|
||||
SequenceObject.Entries.push(entry);
|
||||
}
|
||||
|
||||
if (event.labels.includes("LSP")) {
|
||||
const entry = {
|
||||
"EntryType": "End of line recording",
|
||||
"EntryTypeId": 3007,
|
||||
"Comment": event.remarks.toString(),
|
||||
"IsNTBP": sequence.status == "ntbp",
|
||||
// NOTE: Always pending, see
|
||||
// https://gitlab.com/wgp/dougal/software/-/issues/12
|
||||
"LineStatus": "Pending",
|
||||
"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")) {
|
||||
const entry = {
|
||||
"EntryType": "QC",
|
||||
"Comment": event.remarks.toString(),
|
||||
"ShotPointId": event.point,
|
||||
"Time": event.tstamp
|
||||
}
|
||||
SequenceObject.Entries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (exportGeneral) {
|
||||
// These are labels that we have already (potentially) exported
|
||||
const excluded = [
|
||||
"FSP", "FGSP", "FCSP", "LGFFSP",
|
||||
"LCFFSP", "LCSP", "LGSP", "LSP",
|
||||
"LDSP", "FDSP", "QC", "QCAccepted",
|
||||
"Internal", "Private", "Confidential",
|
||||
"NoExport"
|
||||
];
|
||||
if (!event.labels || !event.labels.some( l => excluded.includes(l) )) {
|
||||
const entry = {
|
||||
"Comment": event.remarks.toString(),
|
||||
"ShotPointId": event.point,
|
||||
"Time": event.tstamp
|
||||
}
|
||||
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);
|
||||
if (fgspIndex != -1) {
|
||||
const fsp = Object.assign({}, SequenceObject.Entries[fgspIndex], {
|
||||
"EntryType": "Start of line recording",
|
||||
"EntryTypeId": 3000,
|
||||
"Heading": Number(sequence.azimuth.toFixed(1)),
|
||||
"IsReshoot": sequence.reshoot,
|
||||
"IsUndershoot": false,
|
||||
"LineNumber": sequence.line.toString(),
|
||||
"LineType": "Prime", // FIXME
|
||||
});
|
||||
SequenceObject.Entries.splice(fgspIndex, 0, fsp);
|
||||
} else {
|
||||
// We have neither an FSP nor an FGSP, let us take the first
|
||||
// preplot shot from the sequence data as save that as FSP
|
||||
// (we intentionally omit adding an FGSP as we know nothing
|
||||
// about the user's intent).
|
||||
const fsp = {
|
||||
"EntryType": "Start of line recording",
|
||||
"EntryTypeId": 3000,
|
||||
"Comment": sequence.fsp_final
|
||||
? "(First preplot shot found in P1)"
|
||||
: "(First shot found in P1)",
|
||||
"Heading": Number(sequence.azimuth.toFixed(1)),
|
||||
"IsReshoot": sequence.reshoot, // FIXME Add this property to sequence object
|
||||
"IsUndershoot": false,
|
||||
// NOTE https://gitlab.com/wgp/dougal/software/-/issues/12#note_419162674
|
||||
"LineNumber": sequence.line.toString(),
|
||||
"LineType": "Prime", // FIXME
|
||||
"ShotPointId": sequence.fsp_final || sequence.fsp,
|
||||
"Time": sequence.ts0_final || sequence.ts0
|
||||
}
|
||||
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);
|
||||
if (lgspIndex != -1) {
|
||||
const lsp = Object.assign({}, SequenceObject.Entries[lgspIndex], {
|
||||
"EntryType": "End of line recording",
|
||||
"EntryTypeId": 3007,
|
||||
"IsNTBP": sequence.status == "ntbp",
|
||||
"LineStatus": "Pending",
|
||||
});
|
||||
SequenceObject.Entries.splice(lgspIndex+1, 0, lsp);
|
||||
} else {
|
||||
// See comment above concerning FSP
|
||||
const lsp = {
|
||||
"EntryType": "End of line recording",
|
||||
"EntryTypeId": 3007,
|
||||
"Comment": sequence.lsp_final
|
||||
? "(Last preplot shot found in P1)"
|
||||
: "(Last shot found in P1)",
|
||||
"IsNTBP": sequence.status == "ntbp",
|
||||
// NOTE: Always pending, see
|
||||
// https://gitlab.com/wgp/dougal/software/-/issues/12
|
||||
"LineStatus": "Pending",
|
||||
"ShotPointId": sequence.lsp_final || sequence.lsp,
|
||||
"Time": sequence.ts1_final || sequence.ts1
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
module.exports = transform;
|
||||
36
lib/www/server/lib/utils/dms.js
Normal file
36
lib/www/server/lib/utils/dms.js
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
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 λ+" "+φ;
|
||||
}
|
||||
|
||||
module.exports = dms;
|
||||
36
lib/www/server/lib/utils/geometryAsString.js
Normal file
36
lib/www/server/lib/utils/geometryAsString.js
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
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) {
|
||||
if (geometry.type == "Point") {
|
||||
if (formatDMS) {
|
||||
str = dms(geometry.coordinates[1], geometry.coordinates[0]);
|
||||
} else {
|
||||
str = `${geometry.coordinates[1].toFixed(6)}, ${geometry.coordinates[0].toFixed(6)}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (str) {
|
||||
if (opts.url) {
|
||||
if (typeof opts.url === 'string') {
|
||||
str = `[${str}](${opts.url.replace("$x", geometry.coordinates[0]).replace("$y", geometry.coordinates[1])})`;
|
||||
} else {
|
||||
str = `[${str}](geo:${geometry.coordinates[0]},${geometry.coordinates[1]})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
module.exports = geometryAsString;
|
||||
5
lib/www/server/lib/utils/index.js
Normal file
5
lib/www/server/lib/utils/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
module.exports = {
|
||||
geometryAsString: require('./geometryAsString'),
|
||||
dms: require('./dms')
|
||||
};
|
||||
3765
lib/www/server/package-lock.json
generated
3765
lib/www/server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,9 +14,12 @@
|
||||
"express": "^4.17.1",
|
||||
"express-jwt": "^6.0.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"marked": "^2.0.3",
|
||||
"netmask": "^1.0.6",
|
||||
"node-fetch": "^2.6.1",
|
||||
"nunjucks": "^3.2.3",
|
||||
"pg": "^8.3.3",
|
||||
"selenium-webdriver": "*",
|
||||
"ws": "^7.3.1",
|
||||
"yaml": "^2.0.0-0"
|
||||
},
|
||||
|
||||
@@ -301,6 +301,9 @@ components:
|
||||
ntba:
|
||||
type: boolean
|
||||
description: Not to be acquired flag. If `true`, all the points in this line are ignored in production statistics.
|
||||
meta:
|
||||
type: object
|
||||
description: Arbitrary JSON data associated with this shotpoint.
|
||||
|
||||
|
||||
Shotpoint:
|
||||
@@ -813,6 +816,32 @@ paths:
|
||||
|
||||
|
||||
/project/{project}/sequence/{sequence}:
|
||||
get:
|
||||
description: Get a sequence
|
||||
tags: [ "sequences" ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Project"
|
||||
- $ref: "#/components/parameters/Sequence"
|
||||
responses:
|
||||
"200":
|
||||
description: Sequence data.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Sequence"
|
||||
application/vnd.seis+json:
|
||||
schema:
|
||||
type: object
|
||||
application/geo+json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GeoJSONFeature"
|
||||
text/html:
|
||||
schema:
|
||||
type: string
|
||||
application/pdf:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
patch:
|
||||
summary: Update sequence information.
|
||||
description: All of the properties are optional. Users may specify only the one(s) they need, other properties remain unchanged. An empty object body causes the resource not to be modified at all. Final sequence properties only have effect if a final P1 file exists for the relevant sequence.
|
||||
|
||||
Reference in New Issue
Block a user