Initial commit

This commit is contained in:
D. Berge
2020-08-08 23:59:13 +02:00
commit 4c5d29494c
113 changed files with 16479 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
<template>
<v-app id="dougal">
<dougal-navigation></dougal-navigation>
<v-main>
<router-view></router-view>
</v-main>
<dougal-footer></dougal-footer>
<v-snackbar v-model="snack"
:color="snackColour"
:timeout="6000"
>
{{ snackText }}
<template v-slot:action="{ attrs }">
<v-btn
text
v-bind="attrs"
@click="snack = false"
>
Close
</v-btn>
</template>
</v-snackbar>
</v-app>
</template>
<style lang="stylus">
@import '../node_modules/typeface-roboto/index.css'
@import '../node_modules/@mdi/font/css/materialdesignicons.css'
</style>
<script>
import DougalNavigation from './components/navigation';
import DougalFooter from './components/footer';
export default {
name: 'Dougal',
components: {
DougalNavigation,
DougalFooter
},
data: () => ({
snack: false
}),
computed: {
snackText () { return this.$store.state.snack.snackText },
snackColour () { return this.$store.state.snack.snackColour }
},
watch: {
"$vuetify.theme.dark": {
handler (newValue) {
localStorage.setItem("darkTheme", newValue);
}
},
snackText (newVal) {
this.snack = !!newVal;
}
},
mounted () {
// Local Storage values are always strings
this.$vuetify.theme.dark = localStorage.getItem("darkTheme") == "true";
}
};
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
</defs>
<path style="fill:currentColor;fill-opacity:1;stroke:none"
d="M 2 2 L 2 5 L 3 5 L 3 3 L 5 3 L 5 2 L 3 2 L 2 2 z M 8 2 L 6 4 L 10 4 L 8 2 z M 11 2 L 11 3 L 13 3 L 13 5 L 14 5 L 14 2 L 13 2 L 11 2 z M 5 5 L 5 11 L 11 11 L 11 5 L 5 5 z M 4 6 L 2 8 L 4 10 L 4 6 z M 6 6 L 10 6 L 10 10 L 6 10 L 6 6 z M 12 6 L 12 10 L 14 8 L 12 6 z M 2 11 L 2 14 L 3 14 L 5 14 L 5 13 L 3 13 L 3 11 L 2 11 z M 13 11 L 13 13 L 11 13 L 11 14 L 14 14 L 14 13 L 14 11 L 13 11 z M 6 12 L 8 14 L 10 12 L 6 12 z "
class="ColorScheme-Text"
/>
</svg>

After

Width:  |  Height:  |  Size: 758 B

View File

@@ -0,0 +1,39 @@
<!-- kate: replace-tabs on; indent-width 2; -->
<template>
<v-footer app>
<small>&copy; {{year}} <a href="https://aaltronav.eu/" target="_blank" class="brand">Aaltronav</a></small>
<v-spacer></v-spacer>
<div title="Night mode">
<v-switch
class="ma-auto"
flat
hide-details
v-model="$vuetify.theme.dark"
append-icon="mdi-weather-night"
></v-switch>
</div>
</v-footer>
</template>
<style>
@font-face {
font-family: "Bank Gothic Medium";
src: local("Bank Gothic Medium"), url("/fonts/bank-gothic-medium.woff");
}
.brand {
font-family: "Bank Gothic Medium";
}
</style>
<script>
export default {
name: 'DougalFooter',
computed: {
year () {
const date = new Date();
return date.getUTCFullYear();
}
}
};
</script>

View File

@@ -0,0 +1,110 @@
<template>
<v-app-bar
app
clipped-left
>
<v-img src="https://aaltronav.eu/media/aaltronav-logo.svg"
contain
max-height="32px" max-width="32px"
></v-img>
<v-toolbar-title class="mx-2" @click="$router.push('/')" style="cursor: pointer;">Dougal</v-toolbar-title>
<v-spacer></v-spacer>
<v-breadcrumbs :items="path"></v-breadcrumbs>
<template v-if="$route.name != 'Login'">
<v-btn text link to="/login" v-if="!$root.user">Log in</v-btn>
<template v-else>
<v-btn title="Edit profile" disabled>
{{$root.user.name}}
</v-btn>
<v-btn class="ml-2" title="Log out" link to="/?logout=1">
<v-icon>mdi-logout</v-icon>
</v-btn>
</template>
</template>
<template v-slot:extension v-if="$route.matched.find(i => i.name == 'Project')">
<v-tabs :value="tab" show-arrows align-with-title>
<v-tab v-for="tab, index in tabs" :key="index" link :to="tabLink(tab.href)" v-text="tab.text"></v-tab>
</v-tabs>
</template>
</v-app-bar>
</template>
<script>
export default {
name: 'DougalNavigation',
data() {
return {
drawer: false,
tabs: [
{ href: "summary", text: "Summary" },
{ href: "lines", text: "Lines" },
{ href: "sequences", text: "Sequences" },
{ href: "calendar", text: "Calendar" },
{ href: "log", text: "Log" },
{ href: "map", text: "Map" }
],
path: []
};
},
computed: {
tab () {
return this.tabs.findIndex(t => t.href == this.$route.path.split(/\/+/)[3]);
}
},
watch: {
"$route" (newVal, oldVal) {
if (newVal.matched != oldVal.matched) {
this.breadcrumbs();
}
},
"$store.state.project.projectId" () {
this.breadcrumbs();
}
},
methods: {
tabLink (href) {
return `/projects/${this.$route.params.project}/${href}`;
},
breadcrumbs () {
this.path = this.$route.matched
.map(i => {
return {
breadcrumbs: i.meta && i.meta.breadcrumbs,
ctx: i.instances.default
}
})
.filter(i => i.breadcrumbs)
.flat()
.map(i => {
return i.breadcrumbs.map( breadcrumb => {
const o = {};
for (const key of Object.keys(breadcrumb)) {
o[key] = typeof breadcrumb[key] === 'function'
? i.ctx && breadcrumb[key](i.ctx)
: breadcrumb[key];
}
return o;
});
})
.flat();
}
},
mounted () {
this.breadcrumbs();
}
};
</script>

View File

@@ -0,0 +1,11 @@
export default function FormatTimestamp (str) {
const d = new Date(str);
if (isNaN(d)) {
return str;
} else {
// Get rid of milliseconds
return d.toISOString().substring(0,19)+"Z";
}
}

View File

@@ -0,0 +1,42 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import vuetify from './plugins/vuetify'
Vue.config.productionTip = false
new Vue({
data () {
return {
apiUrl: "/api",
snack: false,
snackText: null,
snackColour: null,
user: null
}
},
methods: {
markdown (value) {
return typeof value == "string"
? marked(value)
: value;
},
showSnack(text, colour = "primary") {
console.log("showSnack", text, colour);
this.snackColour = colour;
this.snackText = text;
this.snack = true;
}
},
router,
store,
vuetify,
render: h => h(App)
}).$mount('#app')

View File

@@ -0,0 +1,7 @@
import Vue from 'vue';
import Vuetify from 'vuetify/lib';
Vue.use(Vuetify);
export default new Vuetify({
});

View File

@@ -0,0 +1,115 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import Project from '../views/Project.vue'
import ProjectList from '../views/ProjectList.vue'
import ProjectSummary from '../views/ProjectSummary.vue'
import LineList from '../views/LineList.vue'
import LineSummary from '../views/LineSummary.vue'
import SequenceList from '../views/SequenceList.vue'
import SequenceSummary from '../views/SequenceSummary.vue'
import Calendar from '../views/Calendar.vue'
import Log from '../views/Log.vue'
import Map from '../views/Map.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// 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/About.vue')
},
{
pathToRegexpOptions: { strict: true },
path: "/projects",
redirect: "/projects/"
},
{
pathToRegexpOptions: { strict: true },
path: "/projects/",
component: ProjectList,
meta: {
breadcrumbs: [
{ text: "Projects", href: "/projects", disabled: true }
]
}
},
{
pathToRegexpOptions: { strict: true },
path: "/projects/:project",
redirect: "/projects/:project/"
},
{
pathToRegexpOptions: { strict: true },
path: "/projects/:project/",
name: "Project",
component: Project,
meta: {
breadcrumbs: [
{ text: "Projects", href: "/projects" },
{
text: (ctx) => ctx.$store.state.project.projectName || "…",
href: (ctx) => `/projects/${ctx.$store.state.project.projectId || ctx.$route.params.project || ""}/`
}
]
},
children: [
{
path: "",
redirect: "summary"
},
{
path: "summary",
component: ProjectSummary
},
{
path: "lines/",
name: "LineList",
component: LineList
},
{
path: "lines/:line",
name: "Line",
component: LineSummary
},
{
path: "sequences",
component: SequenceList
},
{
path: "sequences/:sequence",
component: SequenceSummary
},
{
path: "calendar",
component: Calendar
},
{
path: "log",
component: Log
},
{
path: "map",
component: Map
}
]
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router

View File

@@ -0,0 +1,16 @@
import Vue from 'vue'
import Vuex from 'vuex'
import api from './modules/api'
import snack from './modules/snack'
import project from './modules/project'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
api,
snack,
project
}
})

View File

@@ -0,0 +1,18 @@
async function api ({state, commit, dispatch}, [resource, init = {}]) {
try {
// this.loading = true;
const res = await fetch(`${state.apiUrl}${resource}`, init);
if (res.ok) {
return await res.json();
} else {
await dispatch('showSnack', [res.statusText, "warning"]);
}
} catch (err) {
await dispatch('showSnack', [err, "error"]);
} finally {
// this.loading = false;
}
}
export default { api };

View File

@@ -0,0 +1,6 @@
import state from './state'
import getters from './getters'
import actions from './actions'
import mutations from './mutations'
export default { state, getters, actions, mutations };

View File

@@ -0,0 +1,5 @@
const state = () => ({
apiUrl: "/api"
});
export default state;

View File

@@ -0,0 +1,13 @@
async function getProject ({commit, dispatch}, projectId) {
const res = await dispatch('api', [`/project/${String(projectId).toLowerCase()}`]);
if (res) {
commit('setProjectName', res.name);
commit('setProjectId', res.pid);
const recentProjects = JSON.parse(localStorage.getItem("recentProjects") || "[]")
recentProjects.unshift(res);
localStorage.setItem("recentProjects", JSON.stringify(recentProjects.slice(0, 3)));
}
}
export default { getProject };

View File

@@ -0,0 +1,6 @@
import state from './state'
import getters from './getters'
import actions from './actions'
import mutations from './mutations'
export default { state, getters, actions, mutations };

View File

@@ -0,0 +1,10 @@
function setProjectId (state, pid) {
state.projectId = pid;
}
function setProjectName (state, name) {
state.projectName = name;
}
export default { setProjectId, setProjectName };

View File

@@ -0,0 +1,6 @@
const state = () => ({
projectId: null,
projectName: null
});
export default state;

View File

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

View File

@@ -0,0 +1,6 @@
import state from './state'
import getters from './getters'
import actions from './actions'
import mutations from './mutations'
export default { state, getters, actions, mutations };

View File

@@ -0,0 +1,10 @@
function setSnackText (state, text) {
state.snackText = text;
}
function setSnackColour (state, colour) {
state.snackColour = colour;
}
export default { setSnackText, setSnackColour };

View File

@@ -0,0 +1,6 @@
const state = () => ({
snackText: null,
snackColour: null
});
export default state;

View File

@@ -0,0 +1,5 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

View File

@@ -0,0 +1,177 @@
<template>
<div>
<v-sheet height="64">
<v-toolbar flat color="white">
<v-menu bottom right>
<template v-slot:activator="{ on, attrs }">
<v-btn
outlined
color="grey darken-2"
v-bind="attrs"
v-on="on"
>
<span>Go to</span>
<v-icon right>mdi-menu-down</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item @click="setFirst">
<v-list-item-title>First sequence</v-list-item-title>
</v-list-item>
<v-list-item @click="setLast">
<v-list-item-title>Last sequence</v-list-item-title>
</v-list-item>
<v-list-item @click="setToday">
<v-list-item-title>Today</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-btn fab text small color="grey darken-2" @click="prev">
<v-icon small>mdi-chevron-left</v-icon>
</v-btn>
<v-btn fab text small color="grey darken-2" @click="next">
<v-icon small>mdi-chevron-right</v-icon>
</v-btn>
<v-toolbar-title v-if="$refs.calendar">
{{ $refs.calendar.title }}
</v-toolbar-title>
<v-spacer></v-spacer>
<v-menu bottom right>
<template v-slot:activator="{ on, attrs }">
<v-btn
outlined
color="grey darken-2"
v-bind="attrs"
v-on="on"
>
<span>{{ typeLabel }}</span>
<v-icon right>mdi-menu-down</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item @click="type = 'day'">
<v-list-item-title>Day</v-list-item-title>
</v-list-item>
<v-list-item @click="type = 'week'">
<v-list-item-title>Week</v-list-item-title>
</v-list-item>
<v-list-item @click="type = 'month'">
<v-list-item-title>Month</v-list-item-title>
</v-list-item>
<v-list-item @click="type = '4day'">
<v-list-item-title>4 days</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-toolbar>
</v-sheet>
<v-sheet>
<v-calendar
ref="calendar"
v-model="focus"
:events="events"
:event-color="getEventColour"
color="primary"
:type="type"
:locale-first-day-of-year="4"
:weekdays="weekdays"
:show-week="true"
></v-calendar>
</v-sheet>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
name: "Calendar",
data () {
return {
weekdays: [1, 2, 3, 4, 5, 6, 0],
type: "week",
focus: "",
events: [
],
loading: false,
options: {
sortBy: "sequence"
}
};
},
computed: {
typeLabel () {
const labels = {
month: 'Month',
week: 'Week',
day: 'Day',
'4day': '4 Days',
};
return labels[this.type];
}
},
methods: {
async getEvents () {
this.loading = true;
const query = new URLSearchParams(this.options);
const url = `/project/${this.$route.params.project}/sequence?${query.toString()}`;
const finalSequences = await this.api([url]) || [];
this.events = finalSequences.map(s => {
const e = {};
//e.start = s.ts0.substring(0,10)+" "+s.ts0.substring(11,19)
//e.end = s.ts1.substring(0,10)+" "+s.ts1.substring(11,19)
e.start = new Date(s.ts0);
e.end = new Date(s.ts1);
e.timed = true;
e.colour = "orange";
e.name = `Sequence ${s.sequence}`;
return e;
});
this.loading = false;
},
getEventColour (event) {
return event.colour;
},
setToday () {
this.focus = "";
},
setFirst () {
this.focus = this.events[0].start;
},
setLast () {
this.focus = this.events[this.events.length-1].start;
},
prev () {
this.$refs.calendar.prev()
},
next () {
this.$refs.calendar.next()
},
...mapActions(["api"])
},
mounted () {
this.getEvents();
}
}
</script>

View File

@@ -0,0 +1,39 @@
<template>
<v-container fluid>
<v-row>
<v-col>
<v-card class="mx-auto" max-width="600" tile>
<v-card-title style="word-break: normal;">Dougal Seismic Production &amp; Data Analysis Tool</v-card-title>
<v-card-text>
<v-list two-line subheader>
<v-subheader>Recent projects</v-subheader>
<v-list-item-group>
<v-list-item link to="/projects/eq20221">
<v-list-item-content>
<v-list-item-title>EQ20221</v-list-item-title>
<v-list-item-subtitle>Snorre PRM</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</v-list>
</v-card-text>
<v-card-actions>
<v-btn text color="primary" to="/projects/">All projects</v-btn>
<v-spacer></v-spacer>
<v-btn text color="success">New project</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script>
// @ is an alias to /src
export default {
name: 'Home',
components: {
}
}
</script>

View File

@@ -0,0 +1,118 @@
<template>
<v-container fluid>
<v-data-table
:headers="headers"
:items="items"
:server-items-length="num_lines"
:options.sync="options"
:loading="loading"
:fixed-header="true"
>
<template v-slot:item.length="props">
<span>{{ Math.round(props.value) }} m</span>
</template>
<template v-slot:item.azimuth="props">
<span>{{ props.value.toFixed(1) }} °</span>
</template>
</v-data-table>
</v-container>
</template>
<script>
import { mapActions } from 'vuex';
export default {
name: "LineList",
data () {
return {
headers: [
{
value: "line",
text: "Line"
},
{
value: "status",
text: "Status"
},
{
value: "fsp",
text: "FSP",
align: "end"
},
{
value: "lsp",
text: "LSP",
align: "end"
},
{
value: "num_points",
text: "Num. points",
align: "end"
},
{
value: "length",
text: "Length",
align: "end"
},
{
value: "azimuth",
text: "Azimuth",
align: "end"
},
{
value: "remarks",
text: "Remarks"
}
],
items: [],
options: {},
loading: false,
num_lines: null
}
},
watch: {
options: {
handler () {
this.getLines();
},
deep: true
}
},
methods: {
async getNumLines () {
const projectInfo = await this.api([`/project/${this.$route.params.project}`]);
this.num_lines = projectInfo.lines;
},
async getLines () {
this.loading = true;
const query = new URLSearchParams(this.options);
if (this.options.itemsPerPage < 0) {
query.delete("itemsPerPage");
}
const url = `/project/${this.$route.params.project}/line?${query.toString()}`;
this.items = await this.api([url]) || [];
this.loading = false;
},
...mapActions(["api"])
},
mounted () {
this.getLines();
this.getNumLines();
}
}
</script>

View File

@@ -0,0 +1,13 @@
<template>
<div>
LineSummary
</div>
</template>
<script>
export default {
name: "LineSummary"
}
</script>

View File

@@ -0,0 +1,13 @@
<template>
<div>
Log
</div>
</template>
<script>
export default {
name: "Log"
}
</script>

View File

@@ -0,0 +1,276 @@
<template>
<div id="map">
</div>
</template>
<style lang="sass">
@import '../../node_modules/leaflet/dist/leaflet.css'
</style>
<style>
#map {
height: 100%;
background-color: #dae4f7;
}
.theme--dark #map {
background-color: #111;
}
</style>
<script>
import L from 'leaflet'
import { mapActions } from 'vuex'
import ftstamp from '@/lib/FormatTimestamp'
import zoomFitIcon from '@/assets/zoom-fit-best.svg'
var map;
const tileMaps = {
"No background": L.tileLayer("/nullmap"), // FIXME
"Norwegian nautical charts": L.tileLayer('https://opencache.statkart.no/gatekeeper/gk/gk.open_gmaps?layers=sjokartraster&zoom={z}&x={x}&y={y}', {
attribution: '<a href="http://www.kartverket.no/" target="_blank">Kartverket</a>'
}),
"GEBCO": L.tileLayer.wms("https://www.gebco.net/data_and_products/gebco_web_services/2019/mapserv?", {layers: "GEBCO_2019_Grid_2", attribution: "<a href='https://www.gebco.net/' target='_blank' title='General Bathymetric Chart of the Oceans'>GEBCO</a>"}),
"OpenStreetMap": L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}),
"MODIS Satellite": L.tileLayer('https://map1.vis.earthdata.nasa.gov/wmts-webmerc/MODIS_Terra_CorrectedReflectance_TrueColor/default/{time}/{tilematrixset}{maxZoom}/{z}/{y}/{x}.{format}', {
attribution: 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System (<a href="https://earthdata.nasa.gov">ESDIS</a>) with funding provided by NASA/HQ.',
bounds: [[-85.0511287776, -179.999999975], [85.0511287776, 179.999999975]],
minZoom: 1,
maxZoom: 9,
format: 'jpg',
time: '',
tilematrixset: 'GoogleMapsCompatible_Level'
})
};
const layers = {
"OpenSeaMap": L.tileLayer('https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', {
attribution: 'Map data: &copy; <a href="http://www.openseamap.org">OpenSeaMap</a> contributors'
}),
"Preplots": L.geoJSON(null, {
pointToLayer (point, latlng) {
return L.circle(latlng, {
radius: 1,
color: "#3388ff",
stroke: false,
fillOpacity: 0.8
});
},
onEachFeature (feature, layer) {
const popup = feature.geometry.type == "Point"
? `Preplot<br/>Point <b>${feature.properties.line} / ${feature.properties.point}</b>`
: `Preplot<br/>Line <b>${feature.properties.line}</b>`;
layer.bindTooltip(popup, {sticky: true});
},
}),
"Raw lines": L.geoJSON(null, {
pointToLayer (point, latlng) {
return L.circle(latlng, {
radius: 3,
color: "red",
stroke: false,
fillOpacity: 0.8
});
},
style (feature) {
return {
color: "red"
}
},
onEachFeature (feature, layer) {
const popup = feature.geometry.type == "Point"
? `Raw sequence ${feature.properties.sequence}<br/>Point <b>${feature.properties.line} / ${feature.properties.point}</b><br/>${feature.properties.objref}<br/>${feature.properties.tstamp}`
: `Raw sequence ${feature.properties.sequence}<br/>
Line <b>${feature.properties.line}</b><br/>
${feature.properties.num_points} points, ???? m<br/>
????<br/>
<table><tr><td>????</td><td>${ftstamp(feature.properties.ts0)}</td></tr><tr><td>????</td><td>${ftstamp(feature.properties.ts1)}</td></tr></table>`;
layer.bindTooltip(popup, {sticky: true});
}
}),
"Final lines": L.geoJSON(null, {
pointToLayer (point, latlng) {
return L.circle(latlng, {
radius: 3,
color: "orange",
stroke: false,
fillOpacity: 0.8
});
},
style (feature) {
return {
color: "orange"
}
},
onEachFeature (feature, layer) {
const p = feature.properties;
const popup = feature.geometry.type == "Point"
? `Final sequence ${p.sequence}<br/>Point <b>${p.line} / ${p.point}</b><br/>${p.objref}<br/>${p.tstamp}`
: `Final sequence ${p.sequence}<br/>
Line <b>${p.line}</b><br/>
${p.num_points} points${p.missing_shots ? " <i>("+p.missing_shots+" missing)</i>" : ""}<br/>
${Math.round(p.length)} m ${p.azimuth.toFixed(1)}°<br/>
${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>`;
// : `Final sequence ${feature.properties.sequence}<br/>Line <b>${feature.properties.line}</b><br/>${feature.properties.num_points} points<br/>From ${feature.properties.ts0}<br/>until ${feature.properties.ts0}`;
layer.bindTooltip(popup, {sticky: true});
}
})
};
export default {
name: "Map",
data () {
return {
//map: null,
loading: false,
layerRefreshConfig: [
{
layer: layers.Preplots,
url: (query = "") => {
return map.getZoom() < 18
? `/project/${this.$route.params.project}/gis/preplot/line`
: `/project/${this.$route.params.project}/gis/preplot/point?${query.toString()}`;
}
},
{
layer: layers["Raw lines"],
url: (query = "") => {
return map.getZoom() < 17
? `/project/${this.$route.params.project}/gis/raw/line`
: `/project/${this.$route.params.project}/gis/raw/point?${query.toString()}`;
}
},
{
layer: layers["Final lines"],
url: (query = "") => {
return map.getZoom() < 17
? `/project/${this.$route.params.project}/gis/final/line`
: `/project/${this.$route.params.project}/gis/final/point?${query.toString()}`;
}
}
]
};
},
methods: {
async fitProjectBounds () {
try {
this.loading = true;
const res = await fetch(`${this.$root.apiUrl}/project/${this.$route.params.project}/gis`);
if (res.ok) {
const bbox = new L.GeoJSON(await res.json());
map.fitBounds(bbox.getBounds());
} else {
this.$root.showSnack(res.statusText, "warning");
}
} catch (err) {
this.$root.showSnack(err, "error")
} finally {
this.loading = false;
}
},
async refreshLayers (layerset) {
const bounds = map.getBounds().pad(0.3);
const bboxScale = map.getZoom()/5;
const bbox = [
bounds._southWest.lng,
bounds._southWest.lat,
bounds._northEast.lng,
bounds._northEast.lat
].map(i => i.toFixed(bboxScale)).join(",");
const limit = 10000;
const query = new URLSearchParams({bbox, limit});
for (const l of this.layerRefreshConfig.filter(i => !layerset || layerset.includes(i.layer))) {
if (map.hasLayer(l.layer)) {
// Firing all refresh events asynchronously, which is OK provided
// we don't have hundreds of layers to be refreshed.
await this.api([l.url(query)]).then( (layer) => {
l.layer.clearLayers();
if ((layer.features && layer.features.length < limit) || ("length" in layer && layer.length < limit)) {
l.layer.addData(layer);
} else {
console.log("Too much data from", l.url(query));
}
})
}
}
},
...mapActions(["api"])
},
mounted () {
map = L.map('map', {maxZoom: 22});
tileMaps["No background"].addTo(map);
layers.OpenSeaMap.addTo(map);
layers.Preplots.addTo(map);
const layerControl = L.control.layers(tileMaps, layers).addTo(map);
map.setView([0, 0], 3);
let moveStart = map.getBounds().pad(0.3);
let zoomStart = map.getZoom();
console.log("MAP", map);
map.on('movestart', () => {
moveStart = map.getBounds().pad(0.3);
zoomStart = map.getZoom();
});
map.on('moveend', () => {
console.log("Contained", moveStart.contains(map.getBounds()), map.getZoom() != zoomStart);
if (!moveStart.contains(map.getBounds()) || map.getZoom() != zoomStart) {
this.refreshLayers();
}
});
this.layerRefreshConfig.forEach( l => {
l.layer.on('add', ({target}) => this.refreshLayers([target]));
});
this.fitProjectBounds();
// /usr/share/icons/breeze/actions/16/zoom-fit-best.svg
const fitProjectBounds = this.fitProjectBounds;
const FitToBoundsControl = L.Control.extend({
onAdd (map) {
const widget = L.DomUtil.create('div');
widget.className = "leaflet-touch leaflet-bar leaflet-control";
//widget.appendChild(document.getElementById("zoom-fit-best-icon"));
widget.innerHTML = `<a href="#"><img src="${zoomFitIcon}""></a>`;
widget.setAttribute("title", "Fit to bounds");
widget.addEventListener("click", fitProjectBounds);
return widget;
},
onRemove (map) {
this.removeEventListener("click", fitProjectBounds);
}
});
function fitToBoundsControl (opts) {
return new FitToBoundsControl(opts);
}
fitToBoundsControl({position: "topleft"}).addTo(map);
}
}
</script>

View File

@@ -0,0 +1,36 @@
<template>
<v-container fluid fill-height class="ma-0 pa-0">
<v-row no-gutters align="stretch" class="fill-height">
<v-col cols="12">
<!-- Show component here according to selected route -->
<keep-alive>
<router-view></router-view>
</keep-alive>
</v-col>
</v-row>
</v-container>
</template>
<script>
import { mapActions } from 'vuex'
export default {
name: 'Home',
components: {
},
data () {
return {
//
}
},
methods: {
...mapActions(["getProject"])
},
async mounted () {
await this.getProject(this.$route.params.project);
}
}
</script>

View File

@@ -0,0 +1,111 @@
<template>
<v-container fluid>
<v-data-table
:headers="headers"
:items="items"
:options.sync="options"
:loading="loading"
>
<template v-slot:item.pid="props">
<a :href="`/projects/${props.item.pid}`">{{props.value}}</a>
</template>
<template v-slot:item.progress="props">
{{ props.item.preplot_points ? (props.item.unique_shots/props.item.preplot_points*100).toFixed(1)+'%' : "" }}
<v-progress-linear v-if="props.item.preplot_points"
height="2"
:value="(props.item.unique_shots/props.item.preplot_points*100)"
/>
</template>
</v-data-table>
</v-container>
</template>
<style>
td p:last-of-type {
margin-bottom: 0;
}
</style>
<script>
import { mapActions } from 'vuex';
export default {
name: "ProjectList",
data () {
return {
headers: [
{
value: "pid",
text: "Project ID"
},
{
value: "name",
text: "Name"
},
{
value: "lines",
text: "Lines"
},
{
value: "sequences",
text: "Sequences"
},
{
value: "unique_shots",
text: "Shots"
},
{
value: "progress",
text: "% Complete"
},
],
items: [],
options: {},
loading: false
}
},
methods: {
async list () {
this.items = await this.api(["/project/"]) || [];
},
async summary (item) {
const details = await this.api([`/project/${item.pid}/summary`]);
if (details) {
return Object.assign({}, details, item);
}
},
async load () {
this.loading = true;
await this.list();
const promises = [];
for (const key in this.items) {
const item = this.items[key];
const promise = this.summary(item)
.then( expanded => {
if (expanded) {
this.$set(this.items, key, expanded);
}
});
promises.push(promise);
}
Promise.all(promises).finally( () => this.loading = false);
},
...mapActions(["api"])
},
mounted () {
this.load();
}
}
</script>

View File

@@ -0,0 +1,13 @@
<template>
<div>
ProjectSummary
</div>
</template>
<script>
export default {
name: "ProjectSummary"
}
</script>

View File

@@ -0,0 +1,13 @@
<template>
<div>
SequenceList
</div>
</template>
<script>
export default {
name: "SequenceList"
}
</script>

View File

@@ -0,0 +1,13 @@
<template>
<div>
SequenceSummary
</div>
</template>
<script>
export default {
name: "SequenceSummary"
}
</script>