Added timeline with points shown on hover.
This commit is contained in:
parent
b7c6ae1f55
commit
a833d43586
7 changed files with 320 additions and 15 deletions
frontend
52
frontend/package-lock.json
generated
52
frontend/package-lock.json
generated
|
@ -13,10 +13,13 @@
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.8",
|
"@fortawesome/vue-fontawesome": "^3.0.8",
|
||||||
|
"chart.js": "^4.4.8",
|
||||||
|
"chartjs-adapter-date-fns": "^3.0.0",
|
||||||
"countries-list": "^3.1.1",
|
"countries-list": "^3.1.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"ol": "^10.4.0",
|
"ol": "^10.4.0",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
|
"vue-chartjs": "^5.3.2",
|
||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -1328,6 +1331,12 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@kurkle/color": {
|
||||||
|
"version": "0.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||||
|
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
|
@ -2531,6 +2540,28 @@
|
||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chart.js": {
|
||||||
|
"version": "4.4.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz",
|
||||||
|
"integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@kurkle/color": "^0.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"pnpm": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chartjs-adapter-date-fns": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"chart.js": ">=2.8.0",
|
||||||
|
"date-fns": ">=2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
@ -2662,6 +2693,17 @@
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/de-indent": {
|
"node_modules/de-indent": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||||
|
@ -5556,6 +5598,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-chartjs": {
|
||||||
|
"version": "5.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.2.tgz",
|
||||||
|
"integrity": "sha512-NrkbRRoYshbXbWqJkTN6InoDVwVb90C0R7eAVgMWcB9dPikbruaOoTFjFYHE/+tNPdIe6qdLCDjfjPHQ0fw4jw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"chart.js": "^4.1.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-eslint-parser": {
|
"node_modules/vue-eslint-parser": {
|
||||||
"version": "9.4.3",
|
"version": "9.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",
|
||||||
|
|
|
@ -18,10 +18,13 @@
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.8",
|
"@fortawesome/vue-fontawesome": "^3.0.8",
|
||||||
|
"chart.js": "^4.4.8",
|
||||||
|
"chartjs-adapter-date-fns": "^3.0.0",
|
||||||
"countries-list": "^3.1.1",
|
"countries-list": "^3.1.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"ol": "^10.4.0",
|
"ol": "^10.4.0",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
|
"vue-chartjs": "^5.3.2",
|
||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
@import './base.css';
|
@import './base.css';
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
max-width: 1280px;
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<main>
|
<main>
|
||||||
<div class="loading" v-if="loading">Loading...</div>
|
<div class="loading" v-if="loading">Loading...</div>
|
||||||
<div class="map-wrapper" v-else>
|
<div class="map-body" v-else>
|
||||||
<div id="map">
|
<div id="map">
|
||||||
<div class="time-range" v-if="oldestPoint && newestPoint">
|
<div class="time-range" v-if="oldestPoint && newestPoint">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -46,6 +46,12 @@
|
||||||
:value="showControls" />
|
:value="showControls" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="timeline">
|
||||||
|
<Timeline :loading="loading"
|
||||||
|
:points="gpsPoints"
|
||||||
|
@point-hover="highlightPoint(pointsLayer, $event)" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
@ -70,6 +76,7 @@ import MapView from '../mixins/MapView.vue';
|
||||||
import Paginate from '../mixins/Paginate.vue';
|
import Paginate from '../mixins/Paginate.vue';
|
||||||
import Points from '../mixins/Points.vue';
|
import Points from '../mixins/Points.vue';
|
||||||
import Routes from '../mixins/Routes.vue';
|
import Routes from '../mixins/Routes.vue';
|
||||||
|
import Timeline from './Timeline.vue';
|
||||||
import URLQueryHandler from '../mixins/URLQueryHandler.vue';
|
import URLQueryHandler from '../mixins/URLQueryHandler.vue';
|
||||||
|
|
||||||
useGeographic()
|
useGeographic()
|
||||||
|
@ -89,13 +96,13 @@ export default {
|
||||||
FilterButton,
|
FilterButton,
|
||||||
FilterForm,
|
FilterForm,
|
||||||
PointInfo,
|
PointInfo,
|
||||||
|
Timeline,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
map: null as Nullable<Map>,
|
map: null as Nullable<Map>,
|
||||||
mappedPoints: [] as Point[],
|
|
||||||
mapView: null as Nullable<View>,
|
mapView: null as Nullable<View>,
|
||||||
pointsLayer: null as Nullable<VectorLayer>,
|
pointsLayer: null as Nullable<VectorLayer>,
|
||||||
popup: null as Nullable<Overlay>,
|
popup: null as Nullable<Overlay>,
|
||||||
|
@ -106,6 +113,21 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
groupedGPSPoints(): GPSPoint[] {
|
||||||
|
return this.groupPoints(this.gpsPoints)
|
||||||
|
},
|
||||||
|
|
||||||
|
mappedPoints(): Record<string, Point> {
|
||||||
|
return this.toMappedPoints(this.groupedGPSPoints)
|
||||||
|
.reduce((acc: Record<string, Point>, point: Point) => {
|
||||||
|
// @ts-expect-error
|
||||||
|
acc[point.values_.id] = point
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async fetch(): Promise<GPSPoint[]> {
|
async fetch(): Promise<GPSPoint[]> {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
@ -137,11 +159,10 @@ export default {
|
||||||
this.locationQuery = prevPageQuery
|
this.locationQuery = prevPageQuery
|
||||||
},
|
},
|
||||||
|
|
||||||
createMap(gpsPoints: GPSPoint[]): Map {
|
createMap(): Map {
|
||||||
this.mappedPoints = this.toMappedPoints(gpsPoints)
|
this.pointsLayer = this.createPointsLayer(Object.values(this.mappedPoints) as Point[])
|
||||||
this.pointsLayer = this.createPointsLayer(this.mappedPoints as Point[])
|
this.routesLayer = this.createRoutesLayer(Object.values(this.mappedPoints) as Point[])
|
||||||
this.routesLayer = this.createRoutesLayer(this.mappedPoints as Point[])
|
this.mapView = this.mapView || this.createMapView(this.gpsPoints)
|
||||||
this.mapView = this.mapView || this.createMapView(gpsPoints)
|
|
||||||
const map = new Map({
|
const map = new Map({
|
||||||
target: 'map',
|
target: 'map',
|
||||||
layers: [
|
layers: [
|
||||||
|
@ -254,14 +275,13 @@ export default {
|
||||||
this.hasPrevPage = gpsPoints.length > 1
|
this.hasPrevPage = gpsPoints.length > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mappedPoints = this.toMappedPoints(this.gpsPoints)
|
|
||||||
if (this.mapView) {
|
if (this.mapView) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.refreshMapView(this.mapView, this.gpsPoints)
|
this.refreshMapView(this.mapView, this.gpsPoints)
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.refreshPointsLayer(this.pointsLayer, this.mappedPoints)
|
this.refreshPointsLayer(this.pointsLayer, Object.values(this.mappedPoints))
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.refreshRoutesLayer(this.routesLayer, this.mappedPoints)
|
this.refreshRoutesLayer(this.routesLayer, Object.values(this.mappedPoints))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deep: true,
|
deep: true,
|
||||||
|
@ -271,7 +291,7 @@ export default {
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.initQuery()
|
this.initQuery()
|
||||||
this.gpsPoints = await this.fetch()
|
this.gpsPoints = await this.fetch()
|
||||||
this.map = this.createMap(this.gpsPoints)
|
this.map = this.createMap()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -280,17 +300,33 @@ export default {
|
||||||
@use "@/styles/common.scss" as *;
|
@use "@/styles/common.scss" as *;
|
||||||
@import "ol/ol.css";
|
@import "ol/ol.css";
|
||||||
|
|
||||||
|
$timeline-height: 7.5em;
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
#map {
|
#map {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: calc(100% - #{$timeline-height});
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -336,6 +372,17 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
width: 100%;
|
||||||
|
height: $timeline-height;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes unroll {
|
@keyframes unroll {
|
||||||
from {
|
from {
|
||||||
transform: translateY(7.5em);
|
transform: translateY(7.5em);
|
||||||
|
|
146
frontend/src/components/Timeline.vue
Normal file
146
frontend/src/components/Timeline.vue
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
<template>
|
||||||
|
<div class="timeline-container">
|
||||||
|
<h1 v-if="loading">Loading...</h1>
|
||||||
|
<h1 v-else-if="!points.length">No data to display</h1>
|
||||||
|
<div class="timeline" v-else>
|
||||||
|
<Line :data="graphData" :options="graphOptions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
CategoryScale,
|
||||||
|
Chart as ChartJS,
|
||||||
|
LineElement,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
TimeScale,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from 'chart.js';
|
||||||
|
|
||||||
|
import { Line } from 'vue-chartjs';
|
||||||
|
import 'chartjs-adapter-date-fns';
|
||||||
|
|
||||||
|
import GPSPoint from '../models/GPSPoint';
|
||||||
|
|
||||||
|
ChartJS.register(
|
||||||
|
CategoryScale,
|
||||||
|
LineElement,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
TimeScale,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emits: ['point-hover'],
|
||||||
|
components: {
|
||||||
|
Line,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
points: {
|
||||||
|
type: Array as () => GPSPoint[],
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
graphData() {
|
||||||
|
return {
|
||||||
|
labels: this.points.map((point: GPSPoint) => point.timestamp),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Altitude (m)',
|
||||||
|
backgroundColor: '#7979f8',
|
||||||
|
borderColor: '#5959a8',
|
||||||
|
fill: false,
|
||||||
|
data: this.points.map((point: GPSPoint) => point.altitude),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
graphOptions() {
|
||||||
|
return {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
elements: {
|
||||||
|
point: {
|
||||||
|
borderWidth: 1,
|
||||||
|
hoverRadius: 4,
|
||||||
|
hoverBorderWidth: 2,
|
||||||
|
},
|
||||||
|
line: {
|
||||||
|
tension: 0.5,
|
||||||
|
borderWidth: 2,
|
||||||
|
fill: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
onHover: (_: MouseEvent, activeElements: any) => {
|
||||||
|
if (activeElements.length) {
|
||||||
|
const index = activeElements[0].index;
|
||||||
|
const point = this.points[index];
|
||||||
|
this.$emit('point-hover', point);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
grid: {
|
||||||
|
drawOnChartArea: true,
|
||||||
|
drawTicks: true,
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
tooltipFormat: 'MMM dd, HH:mm',
|
||||||
|
unit: 'minute',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Date'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Altitude (m)'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.timeline-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -16,16 +16,38 @@ const maxZoom = 18
|
||||||
|
|
||||||
useGeographic()
|
useGeographic()
|
||||||
|
|
||||||
|
const pointStyles = {
|
||||||
|
default: new Style({
|
||||||
|
image: new Circle({
|
||||||
|
radius: 6,
|
||||||
|
fill: new Fill({ color: 'aquamarine' }),
|
||||||
|
stroke: new Stroke({ color: 'blue', width: 1 }),
|
||||||
|
}),
|
||||||
|
zIndex: Infinity, // Ensure that points are always displayed above other layers
|
||||||
|
}),
|
||||||
|
|
||||||
|
highlighted: new Style({
|
||||||
|
image: new Circle({
|
||||||
|
radius: 10,
|
||||||
|
fill: new Fill({ color: 'rgba(255, 0, 0, 0.5)' }),
|
||||||
|
stroke: new Stroke({ color: '#FF0000', width: 2 }),
|
||||||
|
}),
|
||||||
|
zIndex: Infinity, // Ensure that points are always displayed above other layers
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [Geo, Units],
|
mixins: [Geo, Units],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
metersTolerance: 20,
|
metersTolerance: 20,
|
||||||
|
highlightedPointId: null,
|
||||||
|
highlightedFeature: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
groupPoints(points: GPSPoint[]) {
|
groupPoints(points: GPSPoint[]): GPSPoint[] {
|
||||||
if (!points.length) {
|
if (!points.length) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -93,8 +115,12 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
toMappedPoints(gpsPoints: GPSPoint[]): Point[] {
|
toMappedPoints(gpsPoints: GPSPoint[]): Point[] {
|
||||||
return this.groupPoints(gpsPoints).map(
|
return gpsPoints.map(
|
||||||
(gps: GPSPoint) => new Point([gps.longitude, gps.latitude])
|
(gps: GPSPoint) => {
|
||||||
|
const point = new Point([gps.longitude, gps.latitude])
|
||||||
|
point.setProperties(gps)
|
||||||
|
return point
|
||||||
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -143,6 +169,27 @@ export default {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
highlightPoint(layer: VectorLayer, point: GPSPoint) {
|
||||||
|
const feature = layer.getSource().getClosestFeatureToCoordinate([point.longitude, point.latitude])
|
||||||
|
if (feature) {
|
||||||
|
if (point.id === this.highlightedPointId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the previous highlighted point, if any
|
||||||
|
if (this.highlightedPointId) {
|
||||||
|
const prevFeature = this.highlightedFeature
|
||||||
|
if (prevFeature) {
|
||||||
|
prevFeature.setStyle(pointStyles.default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
feature.setStyle(pointStyles.highlighted)
|
||||||
|
this.highlightedPointId = point.id
|
||||||
|
this.highlightedFeature = feature
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -13,3 +13,12 @@ export default {
|
||||||
<Map />
|
<Map />
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
Loading…
Add table
Reference in a new issue