parent
3eaf721ffd
commit
b517e5463d
6 changed files with 222 additions and 49 deletions
frontend/src
|
@ -32,6 +32,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<MapCircle v-if="selectedPoint?.accuracy != null && mapObj"
|
||||
:map="mapObj"
|
||||
:latitude="selectedPoint.latitude"
|
||||
:longitude="selectedPoint.longitude"
|
||||
:radius="selectedPoint.accuracy" />
|
||||
|
||||
<PointInfo :point="selectedPoint"
|
||||
:device="selectedPoint ? devicesById[selectedPoint?.deviceId] : null"
|
||||
ref="popup"
|
||||
|
@ -94,7 +100,7 @@
|
|||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import Map from 'ol/Map';
|
||||
import Overlay from 'ol/Overlay';
|
||||
import MapCircle from './MapCircle.vue';
|
||||
import Point from 'ol/geom/Point';
|
||||
import PointInfo from './PointInfo.vue';
|
||||
import VectorLayer from 'ol/layer/Vector';
|
||||
|
@ -141,6 +147,7 @@ export default {
|
|||
FilterButton,
|
||||
FilterForm,
|
||||
FloatingButton,
|
||||
MapCircle,
|
||||
MapSelectOverlay,
|
||||
PointInfo,
|
||||
Timeline,
|
||||
|
@ -154,7 +161,6 @@ export default {
|
|||
mapView: null as Optional<View>,
|
||||
pointToRemove: null as Optional<GPSPoint>,
|
||||
pointsLayer: null as Optional<VectorLayer>,
|
||||
popup: null as Optional<Overlay>,
|
||||
refreshPoints: 0,
|
||||
routesLayer: null as Optional<VectorLayer>,
|
||||
selectedFeature: null as Optional<Feature>,
|
||||
|
@ -197,6 +203,14 @@ export default {
|
|||
return acc
|
||||
}, {})
|
||||
},
|
||||
|
||||
mapObj(): Map | null {
|
||||
if (!this.map) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.map as Map
|
||||
},
|
||||
},
|
||||
|
||||
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>
|
||||
</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="postal-code" v-if="point.postalCode">{{ point.postalCode }}</p>
|
||||
<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 {
|
||||
color: var(--color-heading);
|
||||
font-weight: bold;
|
||||
|
|
|
@ -164,7 +164,7 @@ export default {
|
|||
})
|
||||
},
|
||||
|
||||
graphData() {
|
||||
graphData(): { labels: Date[], datasets: any[] } {
|
||||
const datasets = []
|
||||
if (this.showMetrics.altitude) {
|
||||
datasets.push(
|
||||
|
|
|
@ -2,9 +2,15 @@
|
|||
import GPSPoint from '../models/GPSPoint'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
earthRadius: 6378e3, // metres
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
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 φ2 = q.latitude * Math.PI / 180
|
||||
const Δφ = (q.latitude - p.latitude) * Math.PI / 180
|
||||
|
|
|
@ -1,64 +1,66 @@
|
|||
import type { Optional } from "./Types";
|
||||
|
||||
class GPSPoint {
|
||||
public id: number;
|
||||
public latitude: number;
|
||||
public longitude: number;
|
||||
public altitude: number;
|
||||
public altitude?: Optional<number>;
|
||||
public deviceId: string;
|
||||
public address: string;
|
||||
public locality: string;
|
||||
public country: string;
|
||||
public postalCode: string;
|
||||
public description?: string;
|
||||
public battery?: number;
|
||||
public speed?: number;
|
||||
public accuracy?: number;
|
||||
public address?: Optional<string>;
|
||||
public locality?: Optional<string>;
|
||||
public country?: Optional<string>;
|
||||
public postalCode?: Optional<string>;
|
||||
public description?: Optional<string>;
|
||||
public battery?: Optional<number>;
|
||||
public speed?: Optional<number>;
|
||||
public accuracy?: Optional<number>;
|
||||
public timestamp: Date;
|
||||
|
||||
constructor({
|
||||
id,
|
||||
latitude,
|
||||
longitude,
|
||||
altitude,
|
||||
deviceId,
|
||||
address,
|
||||
locality,
|
||||
country,
|
||||
postalCode,
|
||||
description,
|
||||
battery,
|
||||
speed,
|
||||
accuracy,
|
||||
timestamp,
|
||||
}: {
|
||||
constructor(data: {
|
||||
id: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
altitude: number;
|
||||
altitude?: number;
|
||||
deviceId: string;
|
||||
address: string;
|
||||
locality: string;
|
||||
country: string;
|
||||
postalCode: string;
|
||||
address?: string;
|
||||
locality?: string;
|
||||
country?: string;
|
||||
postalCode?: string;
|
||||
description?: string;
|
||||
battery?: number;
|
||||
speed?: number;
|
||||
accuracy?: number;
|
||||
timestamp: Date;
|
||||
timestamp?: Date;
|
||||
}) {
|
||||
this.id = id;
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
this.altitude = altitude;
|
||||
this.deviceId = deviceId;
|
||||
this.address = address;
|
||||
this.locality = locality;
|
||||
this.country = country;
|
||||
this.postalCode = postalCode;
|
||||
this.description = description;
|
||||
this.battery = battery;
|
||||
this.speed = speed;
|
||||
this.accuracy = accuracy;
|
||||
this.timestamp = timestamp;
|
||||
this.id = data.id;
|
||||
this.latitude = data.latitude;
|
||||
this.longitude = data.longitude;
|
||||
this.altitude = data.altitude;
|
||||
this.deviceId = data.deviceId;
|
||||
this.address = data.address;
|
||||
this.locality = data.locality;
|
||||
this.country = data.country;
|
||||
this.postalCode = data.postalCode;
|
||||
this.description = data.description;
|
||||
this.battery = data.battery;
|
||||
this.speed = data.speed;
|
||||
this.accuracy = data.accuracy;
|
||||
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