Skip to content

Commit 9a76cf7

Browse files
frontend: MiniMap: Add first version
Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
1 parent 668761c commit 9a76cf7

1 file changed

Lines changed: 228 additions & 0 deletions

File tree

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
<template>
2+
<div class="mini-map-container">
3+
<div
4+
v-if="!has_position"
5+
class="no-gps-overlay d-flex align-center justify-center"
6+
>
7+
<div class="text-center">
8+
<v-icon
9+
size="48"
10+
color="grey"
11+
>
12+
mdi-map-marker-off
13+
</v-icon>
14+
<div class="grey--text mt-2">
15+
Waiting for GPS...
16+
</div>
17+
</div>
18+
</div>
19+
<div
20+
ref="mapContainer"
21+
class="map"
22+
/>
23+
</div>
24+
</template>
25+
26+
<script lang="ts">
27+
import 'leaflet/dist/leaflet.css'
28+
29+
import L from 'leaflet'
30+
import Vue from 'vue'
31+
32+
import blueboatMarker from '@/assets/img/map-markers/blueboat-marker.png'
33+
import brov2Marker from '@/assets/img/map-markers/brov2-marker.png'
34+
import genericMarker from '@/assets/img/map-markers/generic-vehicle-marker.png'
35+
import mavlink2rest from '@/libs/MAVLink2Rest'
36+
import { GpsFixType } from '@/libs/MAVLink2Rest/mavlink2rest-ts/messages/mavlink2rest-enum'
37+
import { GlobalPositionInt, GpsRawInt } from '@/libs/MAVLink2Rest/mavlink2rest-ts/messages/mavlink2rest-message'
38+
import autopilot_data from '@/store/autopilot'
39+
import autopilot_manager from '@/store/autopilot_manager'
40+
41+
const marker_size = 48
42+
43+
function vehicleMarkerUrl(): string {
44+
const vehicle_type = autopilot_manager.vehicle_type?.toLowerCase() ?? ''
45+
if (vehicle_type.includes('submarine')) return brov2Marker
46+
if (vehicle_type.includes('boat')) return blueboatMarker
47+
return genericMarker
48+
}
49+
50+
function createVehicleIcon(): L.DivIcon {
51+
const url = vehicleMarkerUrl()
52+
const size = `width: ${marker_size}px; height: ${marker_size}px;`
53+
return L.divIcon({
54+
html: `<img src="${url}" style="${size}">`,
55+
iconSize: [marker_size, marker_size],
56+
iconAnchor: [marker_size / 2, marker_size / 2],
57+
className: 'vehicle-marker-icon',
58+
})
59+
}
60+
61+
export default Vue.extend({
62+
name: 'MiniMap',
63+
data: () => ({
64+
map: null as L.Map | null,
65+
vehicle_marker: null as L.Marker | null,
66+
trail: null as L.Polyline | null,
67+
trail_points: [] as L.LatLng[],
68+
latitude: 0,
69+
longitude: 0,
70+
heading: 0,
71+
has_position: false,
72+
has_gps_fix: false,
73+
max_trail_points: 500,
74+
position_listener: null as ReturnType<typeof mavlink2rest.startListening> | null,
75+
gps_fix_listener: null as ReturnType<typeof mavlink2rest.startListening> | null,
76+
}),
77+
watch: {
78+
has_position(val: boolean) {
79+
if (val) {
80+
this.$nextTick(() => {
81+
this.map?.invalidateSize()
82+
})
83+
}
84+
},
85+
},
86+
mounted() {
87+
this.initMap()
88+
this.startListening()
89+
},
90+
beforeDestroy() {
91+
this.position_listener?.discard()
92+
this.gps_fix_listener?.discard()
93+
this.map?.remove()
94+
},
95+
methods: {
96+
initMap() {
97+
const container = this.$refs.mapContainer as HTMLElement
98+
if (!container) return
99+
100+
this.map = L.map(container, {
101+
center: [0, 0],
102+
zoom: 16,
103+
zoomControl: false,
104+
attributionControl: false,
105+
dragging: true,
106+
scrollWheelZoom: true,
107+
})
108+
109+
L.tileLayer('/cache/tile.openstreetmap.org/{z}/{x}/{y}.png', {
110+
maxZoom: 16,
111+
}).addTo(this.map)
112+
113+
L.control.attribution({
114+
position: 'bottomright',
115+
prefix: false,
116+
}).addAttribution('OSM').addTo(this.map)
117+
118+
this.trail = L.polyline([], {
119+
color: '#2699D0',
120+
weight: 2,
121+
opacity: 0.7,
122+
}).addTo(this.map)
123+
124+
this.vehicle_marker = L.marker([0, 0], {
125+
icon: createVehicleIcon(),
126+
}).addTo(this.map)
127+
},
128+
startListening() {
129+
this.gps_fix_listener = mavlink2rest.startListening('GPS_RAW_INT')
130+
this.gps_fix_listener.setCallback((message) => {
131+
if (message?.header.system_id !== autopilot_data.system_id || message?.header.component_id !== 1) return
132+
const gps = message?.message as GpsRawInt
133+
const fix = gps.fix_type?.type
134+
this.has_gps_fix = fix !== undefined
135+
&& fix !== GpsFixType.GPS_FIX_TYPE_NO_GPS
136+
&& fix !== GpsFixType.GPS_FIX_TYPE_NO_FIX
137+
}).setFrequency(1)
138+
139+
this.position_listener = mavlink2rest.startListening('GLOBAL_POSITION_INT')
140+
this.position_listener.setCallback((message) => {
141+
if (message?.header.system_id !== autopilot_data.system_id || message?.header.component_id !== 1) return
142+
if (!this.has_gps_fix) return
143+
144+
const pos = message?.message as GlobalPositionInt
145+
this.latitude = pos.lat / 1e7
146+
this.longitude = pos.lon / 1e7
147+
if (this.latitude === 0 && this.longitude === 0) return
148+
if (pos.hdg !== 65535) {
149+
this.heading = pos.hdg / 100
150+
}
151+
152+
const was_positioned = this.has_position
153+
this.has_position = true
154+
155+
this.updateVehicle()
156+
157+
if (!was_positioned) {
158+
this.$nextTick(() => {
159+
this.map?.invalidateSize()
160+
this.map?.setView([this.latitude, this.longitude], 18)
161+
})
162+
}
163+
}).setFrequency(1)
164+
},
165+
updateVehicle() {
166+
if (!this.map || !this.vehicle_marker || !this.trail) return
167+
168+
const pos: L.LatLngExpression = [this.latitude, this.longitude]
169+
170+
this.vehicle_marker.setLatLng(pos)
171+
172+
// Only recenter when the vehicle approaches the viewport edge (inner 60% threshold)
173+
const bounds = this.map.getBounds().pad(-0.2)
174+
if (!bounds.contains(pos)) {
175+
this.map.panTo(pos)
176+
}
177+
178+
const icon_el = this.vehicle_marker.getElement()
179+
const img = icon_el?.querySelector('img')
180+
if (img) {
181+
img.style.transform = `rotate(${this.heading}deg)`
182+
}
183+
184+
this.trail_points.push(L.latLng(this.latitude, this.longitude))
185+
if (this.trail_points.length > this.max_trail_points) {
186+
this.trail_points.shift()
187+
}
188+
this.trail.setLatLngs(this.trail_points)
189+
},
190+
},
191+
})
192+
</script>
193+
194+
<style scoped>
195+
.mini-map-container {
196+
width: 100%;
197+
height: 100%;
198+
position: relative;
199+
min-height: 100px;
200+
padding: 8px;
201+
}
202+
203+
.map {
204+
width: 100%;
205+
height: 100%;
206+
border-radius: 8px;
207+
z-index: 0;
208+
}
209+
210+
.no-gps-overlay {
211+
position: absolute;
212+
top: 8px;
213+
left: 8px;
214+
right: 8px;
215+
bottom: 8px;
216+
background: rgba(0, 0, 0, 0.3);
217+
z-index: 1;
218+
border-radius: 8px;
219+
pointer-events: none;
220+
}
221+
</style>
222+
223+
<style>
224+
.vehicle-marker-icon {
225+
background: none !important;
226+
border: none !important;
227+
}
228+
</style>

0 commit comments

Comments
 (0)