parent
2dd3a54a8a
commit
b5b1e03e35
3 changed files with 111 additions and 7 deletions
frontend/src
|
@ -29,6 +29,7 @@
|
||||||
|
|
||||||
<PointInfo :point="selectedPoint"
|
<PointInfo :point="selectedPoint"
|
||||||
ref="popup"
|
ref="popup"
|
||||||
|
@remove="onRemove"
|
||||||
@close="selectedPoint = null" />
|
@close="selectedPoint = null" />
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
@ -57,6 +58,19 @@
|
||||||
@show-metrics="setShowMetrics" />
|
@show-metrics="setShowMetrics" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
:visible="true"
|
||||||
|
:disabled="loading"
|
||||||
|
v-if="pointToRemove"
|
||||||
|
@close="pointToRemove = null"
|
||||||
|
@confirm="removePoint">
|
||||||
|
<template #title>
|
||||||
|
Remove point
|
||||||
|
</template>
|
||||||
|
|
||||||
|
Are you sure you want to remove this point?
|
||||||
|
</ConfirmDialog>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -72,7 +86,9 @@ import { useGeographic } from 'ol/proj';
|
||||||
|
|
||||||
import type { Optional } from '../models/Types';
|
import type { Optional } from '../models/Types';
|
||||||
import Api from '../mixins/Api.vue';
|
import Api from '../mixins/Api.vue';
|
||||||
|
import ConfirmDialog from '../elements/ConfirmDialog.vue';
|
||||||
import Dates from '../mixins/Dates.vue';
|
import Dates from '../mixins/Dates.vue';
|
||||||
|
import Feature from 'ol/Feature';
|
||||||
import FilterButton from './filter/ToggleButton.vue';
|
import FilterButton from './filter/ToggleButton.vue';
|
||||||
import FilterForm from './filter/Form.vue';
|
import FilterForm from './filter/Form.vue';
|
||||||
import GPSPoint from '../models/GPSPoint';
|
import GPSPoint from '../models/GPSPoint';
|
||||||
|
@ -99,6 +115,7 @@ export default {
|
||||||
],
|
],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
|
ConfirmDialog,
|
||||||
FilterButton,
|
FilterButton,
|
||||||
FilterForm,
|
FilterForm,
|
||||||
PointInfo,
|
PointInfo,
|
||||||
|
@ -110,10 +127,13 @@ export default {
|
||||||
loading: false,
|
loading: false,
|
||||||
map: null as Optional<Map>,
|
map: null as Optional<Map>,
|
||||||
mapView: null as Optional<View>,
|
mapView: null as Optional<View>,
|
||||||
|
pointToRemove: null as Optional<GPSPoint>,
|
||||||
pointsLayer: null as Optional<VectorLayer>,
|
pointsLayer: null as Optional<VectorLayer>,
|
||||||
popup: null as Optional<Overlay>,
|
popup: null as Optional<Overlay>,
|
||||||
queryInitialized: false,
|
queryInitialized: false,
|
||||||
|
refreshPoints: 0,
|
||||||
routesLayer: null as Optional<VectorLayer>,
|
routesLayer: null as Optional<VectorLayer>,
|
||||||
|
selectedFeature: null as Optional<Point>,
|
||||||
selectedPoint: null as Optional<GPSPoint>,
|
selectedPoint: null as Optional<GPSPoint>,
|
||||||
showControls: false,
|
showControls: false,
|
||||||
showMetrics: new TimelineMetricsConfiguration(),
|
showMetrics: new TimelineMetricsConfiguration(),
|
||||||
|
@ -122,10 +142,14 @@ export default {
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
groupedGPSPoints(): GPSPoint[] {
|
groupedGPSPoints(): GPSPoint[] {
|
||||||
|
// Reference refreshPoints to force reactivity
|
||||||
|
this.refreshPoints;
|
||||||
return this.groupPoints(this.gpsPoints)
|
return this.groupPoints(this.gpsPoints)
|
||||||
},
|
},
|
||||||
|
|
||||||
mappedPoints(): Record<string, Point> {
|
mappedPoints(): Record<string, Point> {
|
||||||
|
// Reference refreshPoints to force reactivity
|
||||||
|
this.refreshPoints;
|
||||||
return this.toMappedPoints(this.groupedGPSPoints)
|
return this.toMappedPoints(this.groupedGPSPoints)
|
||||||
.reduce((acc: Record<string, Point>, point: Point) => {
|
.reduce((acc: Record<string, Point>, point: Point) => {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
|
@ -148,6 +172,50 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async removePoint() {
|
||||||
|
if (!this.pointToRemove) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
this.deletePoints([this.pointToRemove])
|
||||||
|
if (this.selectedFeature) {
|
||||||
|
const routeFeatures = this.routesLayer?.getSource().getFeatures().filter((f: Feature) => {
|
||||||
|
const [start, end] = (f.getGeometry() as any).getCoordinates()
|
||||||
|
return (
|
||||||
|
(
|
||||||
|
this.pointToRemove?.longitude === start[0] &&
|
||||||
|
this.pointToRemove?.latitude === start[1]
|
||||||
|
) || (
|
||||||
|
this.pointToRemove?.longitude === end[0] &&
|
||||||
|
this.pointToRemove?.latitude === end[1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (routeFeatures.length) {
|
||||||
|
this.routesLayer?.getSource().removeFeatures(routeFeatures)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pointsLayer?.getSource().removeFeature(this.selectedFeature)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gpsPoints = this.gpsPoints.filter((p: GPSPoint) => p.id !== this.pointToRemove?.id)
|
||||||
|
this.refreshPoints++
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
this.pointToRemove = null
|
||||||
|
this.selectedPoint = null
|
||||||
|
this.selectedFeature = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onRemove(point: GPSPoint) {
|
||||||
|
this.pointToRemove = point
|
||||||
|
this.$emit('remove', this.point)
|
||||||
|
},
|
||||||
|
|
||||||
fetchNextPage() {
|
fetchNextPage() {
|
||||||
const nextPageQuery = this.nextPageQuery()
|
const nextPageQuery = this.nextPageQuery()
|
||||||
if (!nextPageQuery) {
|
if (!nextPageQuery) {
|
||||||
|
@ -190,12 +258,22 @@ export default {
|
||||||
return map
|
return map
|
||||||
},
|
},
|
||||||
|
|
||||||
|
refreshMap() {
|
||||||
|
// @ts-ignore
|
||||||
|
this.refreshMapView(this.mapView, this.gpsPoints)
|
||||||
|
// @ts-ignore
|
||||||
|
this.refreshPointsLayer(this.pointsLayer, Object.values(this.mappedPoints))
|
||||||
|
// @ts-ignore
|
||||||
|
this.refreshRoutesLayer(this.routesLayer, Object.values(this.mappedPoints))
|
||||||
|
},
|
||||||
|
|
||||||
bindClick(map: Map) {
|
bindClick(map: Map) {
|
||||||
map.on('click', (event) => {
|
map.on('click', (event) => {
|
||||||
this.showControls = false
|
this.showControls = false
|
||||||
const feature = map.forEachFeatureAtPixel(event.pixel, (feature) => feature)
|
const feature = map.forEachFeatureAtPixel(event.pixel, (feature) => feature)
|
||||||
|
|
||||||
if (feature) {
|
if (feature) {
|
||||||
|
this.selectedFeature = feature
|
||||||
const point = this.gpsPoints.find((gps: GPSPoint) => {
|
const point = this.gpsPoints.find((gps: GPSPoint) => {
|
||||||
const [longitude, latitude] = (feature.getGeometry() as any).getCoordinates()
|
const [longitude, latitude] = (feature.getGeometry() as any).getCoordinates()
|
||||||
return gps.longitude === longitude && gps.latitude === latitude
|
return gps.longitude === longitude && gps.latitude === latitude
|
||||||
|
@ -210,6 +288,7 @@ export default {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.selectedPoint = null
|
this.selectedPoint = null
|
||||||
|
this.selectedFeature = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -318,12 +397,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.mapView) {
|
if (this.mapView) {
|
||||||
// @ts-ignore
|
this.refreshMap()
|
||||||
this.refreshMapView(this.mapView, this.gpsPoints)
|
|
||||||
// @ts-ignore
|
|
||||||
this.refreshPointsLayer(this.pointsLayer, Object.values(this.mappedPoints))
|
|
||||||
// @ts-ignore
|
|
||||||
this.refreshRoutesLayer(this.routesLayer, Object.values(this.mappedPoints))
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deep: true,
|
deep: true,
|
||||||
|
|
|
@ -30,6 +30,12 @@
|
||||||
<span class="continent">{{ country.continent }}</span>
|
<span class="continent">{{ country.continent }}</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="timestamp" v-if="timeString">{{ timeString }}</p>
|
<p class="timestamp" v-if="timeString">{{ timeString }}</p>
|
||||||
|
|
||||||
|
<div class="remove">
|
||||||
|
<button title="Remove" @click="$emit('remove', point)">
|
||||||
|
<font-awesome-icon icon="fas fa-trash-alt" /> Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,7 +51,7 @@ import Dates from '../mixins/Dates.vue';
|
||||||
import GPSPoint from '../models/GPSPoint';
|
import GPSPoint from '../models/GPSPoint';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emit: ['close'],
|
emit: ['close', 'remove'],
|
||||||
mixins: [Dates],
|
mixins: [Dates],
|
||||||
props: {
|
props: {
|
||||||
point: {
|
point: {
|
||||||
|
@ -157,5 +163,22 @@ export default {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.remove {
|
||||||
|
font-size: 0.85em;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--vt-c-red-fg-light);
|
||||||
|
margin-left: -0.5em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--vt-c-red-fg-dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -20,6 +20,13 @@ export default {
|
||||||
)
|
)
|
||||||
.sort((a: GPSPoint, b: GPSPoint) => a.timestamp.getTime() - b.timestamp.getTime())
|
.sort((a: GPSPoint, b: GPSPoint) => a.timestamp.getTime() - b.timestamp.getTime())
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async deletePoints(points: GPSPoint[]) {
|
||||||
|
await this.request('/gpsdata', {
|
||||||
|
method: 'DELETE',
|
||||||
|
body: points.map((point: GPSPoint) => point.id),
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Add table
Reference in a new issue