parent
3eaf721ffd
commit
b517e5463d
6 changed files with 222 additions and 49 deletions
frontend/src
|
@ -32,6 +32,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<MapCircle v-if="selectedPoint?.accuracy != null && mapObj"
|
||||||
|
:map="mapObj"
|
||||||
|
:latitude="selectedPoint.latitude"
|
||||||
|
:longitude="selectedPoint.longitude"
|
||||||
|
:radius="selectedPoint.accuracy" />
|
||||||
|
|
||||||
<PointInfo :point="selectedPoint"
|
<PointInfo :point="selectedPoint"
|
||||||
:device="selectedPoint ? devicesById[selectedPoint?.deviceId] : null"
|
:device="selectedPoint ? devicesById[selectedPoint?.deviceId] : null"
|
||||||
ref="popup"
|
ref="popup"
|
||||||
|
@ -94,7 +100,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import Map from 'ol/Map';
|
import Map from 'ol/Map';
|
||||||
import Overlay from 'ol/Overlay';
|
import MapCircle from './MapCircle.vue';
|
||||||
import Point from 'ol/geom/Point';
|
import Point from 'ol/geom/Point';
|
||||||
import PointInfo from './PointInfo.vue';
|
import PointInfo from './PointInfo.vue';
|
||||||
import VectorLayer from 'ol/layer/Vector';
|
import VectorLayer from 'ol/layer/Vector';
|
||||||
|
@ -141,6 +147,7 @@ export default {
|
||||||
FilterButton,
|
FilterButton,
|
||||||
FilterForm,
|
FilterForm,
|
||||||
FloatingButton,
|
FloatingButton,
|
||||||
|
MapCircle,
|
||||||
MapSelectOverlay,
|
MapSelectOverlay,
|
||||||
PointInfo,
|
PointInfo,
|
||||||
Timeline,
|
Timeline,
|
||||||
|
@ -154,7 +161,6 @@ export default {
|
||||||
mapView: null as Optional<View>,
|
mapView: null as Optional<View>,
|
||||||
pointToRemove: null as Optional<GPSPoint>,
|
pointToRemove: null as Optional<GPSPoint>,
|
||||||
pointsLayer: null as Optional<VectorLayer>,
|
pointsLayer: null as Optional<VectorLayer>,
|
||||||
popup: null as Optional<Overlay>,
|
|
||||||
refreshPoints: 0,
|
refreshPoints: 0,
|
||||||
routesLayer: null as Optional<VectorLayer>,
|
routesLayer: null as Optional<VectorLayer>,
|
||||||
selectedFeature: null as Optional<Feature>,
|
selectedFeature: null as Optional<Feature>,
|
||||||
|
@ -197,6 +203,14 @@ export default {
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mapObj(): Map | null {
|
||||||
|
if (!this.map) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.map as Map
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
130
frontend/src/components/MapCircle.vue
Normal file
130
frontend/src/components/MapCircle.vue
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
<template>
|
||||||
|
<div class="circle"
|
||||||
|
ref="circle"
|
||||||
|
:style="circleStyle" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Geo from '../mixins/Geo.vue';
|
||||||
|
import Map from 'ol/Map';
|
||||||
|
import Overlay from 'ol/Overlay';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [Geo],
|
||||||
|
props: {
|
||||||
|
map: {
|
||||||
|
type: Map,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
latitude: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
longitude: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
radius: {
|
||||||
|
// Unit: meters
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: 'var(--color-accent)',
|
||||||
|
},
|
||||||
|
|
||||||
|
borderColor: {
|
||||||
|
type: String,
|
||||||
|
default: 'rgba(0, 0, 0, 0.2)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
overlay: null as Overlay | null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
center(): { x: number | null, y: number | null } {
|
||||||
|
const pixel = this.map.getPixelFromCoordinate([this.longitude, this.latitude]);
|
||||||
|
if (!pixel) {
|
||||||
|
return {
|
||||||
|
x: null,
|
||||||
|
y: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: pixel[0],
|
||||||
|
y: pixel[1],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
circleStyle() {
|
||||||
|
if (!(this.center.x && this.center.y)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: `${this.radiusPx * 2}px`,
|
||||||
|
height: `${this.radiusPx * 2}px`,
|
||||||
|
backgroundColor: this.color,
|
||||||
|
border: `1px solid ${this.borderColor}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
radiusPx() {
|
||||||
|
const center = this.map.getPixelFromCoordinate([this.longitude, this.latitude]);
|
||||||
|
const radiusLatOffset = 90 * (this.radius / this.earthRadius) * Math.cos(this.latitude * Math.PI / 180);
|
||||||
|
const radius = this.map.getPixelFromCoordinate([this.longitude, this.latitude + radiusLatOffset]);
|
||||||
|
return Math.abs(center[1] - radius[1]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
bind() {
|
||||||
|
this.overlay = new Overlay({
|
||||||
|
element: this.$el,
|
||||||
|
positioning: 'center-center',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.map.addOverlay(this.overlay as Overlay);
|
||||||
|
|
||||||
|
// Force show the Overlay
|
||||||
|
(this.overlay as Overlay).setPosition([this.longitude, this.latitude]);
|
||||||
|
},
|
||||||
|
|
||||||
|
unbind() {
|
||||||
|
if (this.overlay) {
|
||||||
|
this.map.removeOverlay(this.overlay as Overlay);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.bind();
|
||||||
|
},
|
||||||
|
|
||||||
|
unmounted() {
|
||||||
|
this.unbind();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.circle {
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
opacity: 0.25;
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -60,6 +60,12 @@
|
||||||
<span>{{ point.battery }}%</span>
|
<span>{{ point.battery }}%</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p class="accuracy" v-if="point.accuracy">
|
||||||
|
<font-awesome-icon icon="fas fa-ruler" />
|
||||||
|
<span class="title">Accuracy:</span>
|
||||||
|
<span class="value">{{ point.accuracy }} m</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p class="locality" v-if="point.locality">{{ point.locality }}</p>
|
<p class="locality" v-if="point.locality">{{ point.locality }}</p>
|
||||||
<p class="postal-code" v-if="point.postalCode">{{ point.postalCode }}</p>
|
<p class="postal-code" v-if="point.postalCode">{{ point.postalCode }}</p>
|
||||||
<p class="country" v-if="country">
|
<p class="country" v-if="country">
|
||||||
|
@ -353,6 +359,21 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.accuracy {
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
opacity: 0.75;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.timestamp {
|
.timestamp {
|
||||||
color: var(--color-heading);
|
color: var(--color-heading);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
|
@ -164,7 +164,7 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
graphData() {
|
graphData(): { labels: Date[], datasets: any[] } {
|
||||||
const datasets = []
|
const datasets = []
|
||||||
if (this.showMetrics.altitude) {
|
if (this.showMetrics.altitude) {
|
||||||
datasets.push(
|
datasets.push(
|
||||||
|
|
|
@ -2,9 +2,15 @@
|
||||||
import GPSPoint from '../models/GPSPoint'
|
import GPSPoint from '../models/GPSPoint'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
earthRadius: 6378e3, // metres
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
latLngToDistance(p: GPSPoint, q: GPSPoint): number {
|
latLngToDistance(p: GPSPoint, q: GPSPoint): number {
|
||||||
const R = 6371e3 // metres
|
const R = this.earthRadius // metres
|
||||||
const φ1 = p.latitude * Math.PI / 180 // φ, λ in radians
|
const φ1 = p.latitude * Math.PI / 180 // φ, λ in radians
|
||||||
const φ2 = q.latitude * Math.PI / 180
|
const φ2 = q.latitude * Math.PI / 180
|
||||||
const Δφ = (q.latitude - p.latitude) * Math.PI / 180
|
const Δφ = (q.latitude - p.latitude) * Math.PI / 180
|
||||||
|
|
|
@ -1,64 +1,66 @@
|
||||||
|
import type { Optional } from "./Types";
|
||||||
|
|
||||||
class GPSPoint {
|
class GPSPoint {
|
||||||
public id: number;
|
public id: number;
|
||||||
public latitude: number;
|
public latitude: number;
|
||||||
public longitude: number;
|
public longitude: number;
|
||||||
public altitude: number;
|
public altitude?: Optional<number>;
|
||||||
public deviceId: string;
|
public deviceId: string;
|
||||||
public address: string;
|
public address?: Optional<string>;
|
||||||
public locality: string;
|
public locality?: Optional<string>;
|
||||||
public country: string;
|
public country?: Optional<string>;
|
||||||
public postalCode: string;
|
public postalCode?: Optional<string>;
|
||||||
public description?: string;
|
public description?: Optional<string>;
|
||||||
public battery?: number;
|
public battery?: Optional<number>;
|
||||||
public speed?: number;
|
public speed?: Optional<number>;
|
||||||
public accuracy?: number;
|
public accuracy?: Optional<number>;
|
||||||
public timestamp: Date;
|
public timestamp: Date;
|
||||||
|
|
||||||
constructor({
|
constructor(data: {
|
||||||
id,
|
|
||||||
latitude,
|
|
||||||
longitude,
|
|
||||||
altitude,
|
|
||||||
deviceId,
|
|
||||||
address,
|
|
||||||
locality,
|
|
||||||
country,
|
|
||||||
postalCode,
|
|
||||||
description,
|
|
||||||
battery,
|
|
||||||
speed,
|
|
||||||
accuracy,
|
|
||||||
timestamp,
|
|
||||||
}: {
|
|
||||||
id: number;
|
id: number;
|
||||||
latitude: number;
|
latitude: number;
|
||||||
longitude: number;
|
longitude: number;
|
||||||
altitude: number;
|
altitude?: number;
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
address: string;
|
address?: string;
|
||||||
locality: string;
|
locality?: string;
|
||||||
country: string;
|
country?: string;
|
||||||
postalCode: string;
|
postalCode?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
battery?: number;
|
battery?: number;
|
||||||
speed?: number;
|
speed?: number;
|
||||||
accuracy?: number;
|
accuracy?: number;
|
||||||
timestamp: Date;
|
timestamp?: Date;
|
||||||
}) {
|
}) {
|
||||||
this.id = id;
|
this.id = data.id;
|
||||||
this.latitude = latitude;
|
this.latitude = data.latitude;
|
||||||
this.longitude = longitude;
|
this.longitude = data.longitude;
|
||||||
this.altitude = altitude;
|
this.altitude = data.altitude;
|
||||||
this.deviceId = deviceId;
|
this.deviceId = data.deviceId;
|
||||||
this.address = address;
|
this.address = data.address;
|
||||||
this.locality = locality;
|
this.locality = data.locality;
|
||||||
this.country = country;
|
this.country = data.country;
|
||||||
this.postalCode = postalCode;
|
this.postalCode = data.postalCode;
|
||||||
this.description = description;
|
this.description = data.description;
|
||||||
this.battery = battery;
|
this.battery = data.battery;
|
||||||
this.speed = speed;
|
this.speed = data.speed;
|
||||||
this.accuracy = accuracy;
|
this.accuracy = data.accuracy;
|
||||||
this.timestamp = timestamp;
|
this.timestamp = data.timestamp || new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromLatLng({
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
}: {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
}) {
|
||||||
|
return new GPSPoint({
|
||||||
|
id: 0,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
deviceId: '',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue