diff --git a/backend/context-service/resources/ATM/schemas.py b/backend/context-service/resources/ATM/schemas.py
index c065325b..ca12d54b 100644
--- a/backend/context-service/resources/ATM/schemas.py
+++ b/backend/context-service/resources/ATM/schemas.py
@@ -10,16 +10,30 @@ class PlaneMetadataSchemaATM(MetadataSchema):
Current_airspeed = Float()
Latitude = Float()
Longitude = Float()
+ # True heading, degrees clockwise from north (0-360). Optional
+ heading = Float(required=False)
+ # True if another aircraft is inside this one's protected zone right now.
+ in_los = fields.Boolean(required=False)
wpList = List(Dict())
+class ShapeMetadataSchemaATM(MetadataSchema):
+ name = String(required=True)
+ # SECTOR | WEATHER | VOLCANIC | OBSTACLE, see build_shapes_payload() in
+ # ai4realnet_rl_batch_bridge.py.
+ kind = String(required=False)
+ coordinates = List(List(Float()), required=True)
+
+
class MetadataSchemaATM(MetadataSchema):
airplanes = List(fields.Nested(PlaneMetadataSchemaATM), required=True)
-
+ shapes = List(fields.Nested(ShapeMetadataSchemaATM), required=False)
+
# Backward compatibility: optional fields for the single airplane case
ApDest = Dict(required=False)
Current_airspeed = Float(required=False)
Latitude = Float(required=False)
Longitude = Float(required=False)
+ heading = Float(required=False)
wpList = List(Dict(), required=False)
@pre_load
@@ -27,7 +41,7 @@ def handle_backward_compatibility(self, data, **kwargs):
# If the new 'airplanes' field is not provided, assume the old format.
if 'airplanes' not in data:
airplane = {}
- for field in ['ApDest', 'Current_airspeed', 'Latitude', 'Longitude', 'wpList']:
+ for field in ['ApDest', 'Current_airspeed', 'Latitude', 'Longitude', 'heading', 'wpList']:
if field in data:
airplane[field] = data[field]
# Provide a default id_plane if not present.
diff --git a/frontend/public/img/icons/map_markers/ATM.svg b/frontend/public/img/icons/map_markers/ATM.svg
index f11e3495..5a22a247 100644
--- a/frontend/public/img/icons/map_markers/ATM.svg
+++ b/frontend/public/img/icons/map_markers/ATM.svg
@@ -1 +1,48 @@
-
\ No newline at end of file
+
+
diff --git a/frontend/src/components/organisms/Map.vue b/frontend/src/components/organisms/Map.vue
index be58466b..7fc954e2 100644
--- a/frontend/src/components/organisms/Map.vue
+++ b/frontend/src/components/organisms/Map.vue
@@ -6,6 +6,19 @@
:url="tileLayer"
layer-type="base"
name="OpenStreetMap" />
+
+
+ {{ polygon.id.replace(/^shape-/, '') }}
+
+
+
+
+
+
{{ waypoint.id }}
+
+ :class-name="
+ 'context-marker ' +
+ waypoint.severity +
+ ($route.params.entity === 'ATM' ? ' context-marker-plain' : '')
+ ">
+
+
@@ -65,11 +111,13 @@
import 'leaflet/dist/leaflet.css'
import {
+ LCircle,
LCircleMarker,
LControlScale,
LIcon,
LMap,
LMarker,
+ LPolygon,
LPolyline,
LTileLayer,
LTooltip
@@ -100,6 +148,11 @@ const props = withDefaults(
const mapStore = useMapStore()
const appStore = useAppStore()
+// Protected-zone ring drawn around each ATM aircraft context marker.
+// Leaflet's LCircle radius is in meters; 1 NM = 1852 m.
+const PROTECTED_ZONE_RADIUS_NM = 5
+const PROTECTED_ZONE_RADIUS_M = PROTECTED_ZONE_RADIUS_NM * 1852
+
const lockView = ref(true)
const zoom = ref(6)
const map = ref()
@@ -140,6 +193,10 @@ onUnmounted(() => {
}
.context-marker {
+ // Overrides leaflet.css's .leaflet-div-icon defaults (white fill + grey
+ // border) now that this marker is rendered as an L.divIcon (needed so the
+ // ATM plane icon inside it can be rotated -- see Map.vue's LIcon usage).
+ border: none !important;
transition: var(--duration);
background: var(--color-success);
border-radius: var(--radius-circular);
@@ -151,6 +208,11 @@ onUnmounted(() => {
background: var(--color-error);
}
}
+// ATM-only: no colored circular badge behind the plane icon
+.context-marker.context-marker-plain {
+ background: transparent !important;
+ padding: 0 !important;
+}
.cab-map-lockview {
width: calc(var(--unit) * 5);
display: flex;
diff --git a/frontend/src/entities/ATM/CAB/Context.vue b/frontend/src/entities/ATM/CAB/Context.vue
index 3f87ea19..3020a45f 100644
--- a/frontend/src/entities/ATM/CAB/Context.vue
+++ b/frontend/src/entities/ATM/CAB/Context.vue
@@ -9,10 +9,11 @@ import { onBeforeMount, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import Context from '@/components/organisms/CAB/Context.vue'
import Map from '@/components/organisms/Map.vue'
-import type { AirplaneContext, LegacyContext, ContextType } from '@/entities/ATM/types'
+import type { AirplaneContext, LegacyContext, ContextType, ShapeContext } from '@/entities/ATM/types'
import { useAppStore } from '@/stores/app'
import { useMapStore } from '@/stores/components/map'
import { useServicesStore } from '@/stores/services'
+import type { Polygon } from '@/types/components/map'
const { t, locale } = useI18n()
const servicesStore = useServicesStore()
@@ -21,6 +22,47 @@ const appStore = useAppStore()
const faulty = ref(false)
+// Styling per shape kind, see build_shapes_payload() in
+// ai4realnet_rl_batch_bridge.py for where `kind` comes from.
+const SHAPE_STYLE: Record, NonNullable> = {
+ SECTOR: { color: 'var(--color-secondary)', weight: 1, dashArray: '4 4', fill: false },
+ WEATHER: {
+ // MEDIUM criticality / warning
+ color: 'var(--color-warning)',
+ weight: 2,
+ fill: true,
+ fillColor: 'var(--color-warning)',
+ fillOpacity: 0.2
+ },
+ VOLCANIC: {
+ // MEDIUM criticality / warning
+ color: 'var(--color-warning)',
+ weight: 2,
+ fill: true,
+ fillColor: 'var(--color-warning)',
+ fillOpacity: 0.25
+ },
+ OBSTACLE: {
+ color: 'var(--color-error)',
+ weight: 2,
+ fill: true,
+ fillColor: 'var(--color-error)',
+ fillOpacity: 0.1
+ }
+}
+
+function addShapes(shapes: ShapeContext[]) {
+ mapStore.removeCategoryPolygon('SHAPE')
+ for (const shape of shapes) {
+ mapStore.addPolygon({
+ id: `shape-${shape.name}`,
+ points: shape.coordinates,
+ category: 'SHAPE',
+ options: SHAPE_STYLE[shape.kind ?? 'OBSTACLE'] ?? SHAPE_STYLE.OBSTACLE
+ })
+ }
+}
+
onBeforeMount(async () => {
locale.value = `en-ATM`
await servicesStore.getContext('ATM', (context: { data: ContextType }) => {
@@ -29,11 +71,15 @@ onBeforeMount(async () => {
mapStore.removeCategoryWaypoint('ROUTE')
// 2- add new markers and ROUTE waypoints
if ('airplanes' in context.data) {
+ addShapes(context.data.shapes ?? [])
context.data.airplanes.forEach((airplane: AirplaneContext) => {
mapStore.addContextWaypoint({
lat: airplane.Latitude,
lng: airplane.Longitude,
- id: `plane-${airplane.id_plane}`
+ // Bare acid (from bluesky) as the label (e.g. "AC1")
+ id: `${airplane.id_plane}`,
+ heading: airplane.heading,
+ inLos: airplane.in_los
})
// build the route waypoints
const waypoints = [
@@ -67,11 +113,13 @@ onBeforeMount(async () => {
})
} else {
// Legacy context data handling
+ addShapes([])
const legacy = context.data as LegacyContext
mapStore.addContextWaypoint({
lat: legacy.Latitude,
lng: legacy.Longitude,
- id: t('map.context')
+ id: t('map.context'),
+ heading: legacy.heading
})
const waypoints = [
...(legacy.wpList
diff --git a/frontend/src/entities/ATM/CAB/Timeline.vue b/frontend/src/entities/ATM/CAB/Timeline.vue
index 3da1cc63..27570db8 100644
--- a/frontend/src/entities/ATM/CAB/Timeline.vue
+++ b/frontend/src/entities/ATM/CAB/Timeline.vue
@@ -1,7 +1,7 @@
- {{ $t('cab.timeline') }}
-
+ {{ $t('cab.timeline') }} ({{ tzLabel }})
+
diff --git a/frontend/src/entities/ATM/types.ts b/frontend/src/entities/ATM/types.ts
index f5bdba48..8149a02b 100644
--- a/frontend/src/entities/ATM/types.ts
+++ b/frontend/src/entities/ATM/types.ts
@@ -3,6 +3,10 @@ export type AirplaneContext = {
Current_airspeed: number;
Latitude: number;
Longitude: number;
+ // True heading, degrees clockwise from north (0-360). Optional.
+ heading?: number;
+ // True if another aircraft is currently inside this one's protected zone.
+ in_los?: boolean;
ApDest?: {
apcity: string;
apid: Uppercase;
@@ -22,6 +26,7 @@ export type LegacyContext = {
Current_airspeed: number;
Latitude: number;
Longitude: number;
+ heading?: number;
ApDest?: {
apcity: string;
apid: Uppercase;
@@ -37,7 +42,15 @@ export type LegacyContext = {
}[];
};
-export type ContextType = { airplanes: AirplaneContext[] } | LegacyContext;
+export type ShapeContext = {
+ name: string;
+ kind?: 'SECTOR' | 'WEATHER' | 'VOLCANIC' | 'OBSTACLE' | string;
+ coordinates: [number, number][];
+};
+
+export type ContextType =
+ | { airplanes: AirplaneContext[]; shapes?: ShapeContext[] }
+ | LegacyContext;
export type ATM = {
Context: ContextType;
diff --git a/frontend/src/stores/components/map.ts b/frontend/src/stores/components/map.ts
index 767de2dc..1e13abf7 100644
--- a/frontend/src/stores/components/map.ts
+++ b/frontend/src/stores/components/map.ts
@@ -1,18 +1,20 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
-import type { Polyline, Waypoint } from '@/types/components/map'
+import type { Polygon, Polyline, Waypoint } from '@/types/components/map'
import { addOrUpdate, remove } from '@/utils/utils'
export const useMapStore = defineStore('map', () => {
const waypoints = ref([])
const polylines = ref([])
const contextWaypoints = ref([])
+ const polygons = ref([])
function reset() {
resetWaypoints()
resetPolylines()
resetContextWaypoints()
+ resetPolygons()
}
function addWaypoint(waypoint: Waypoint) {
@@ -54,10 +56,28 @@ export const useMapStore = defineStore('map', () => {
contextWaypoints.value.splice(0, contextWaypoints.value.length)
}
+ function addPolygon(polygon: Polygon) {
+ addOrUpdate(polygons.value, polygon, (el) => el.id === polygon.id)
+ }
+
+ function removePolygon(polygon: Polygon) {
+ remove(polygons.value, (el) => el.id === polygon.id)
+ }
+
+ function removeCategoryPolygon(category: string) {
+ for (const polygon of polygons.value.filter((p) => p.category === category))
+ remove(polygons.value, (el) => el.id === polygon.id)
+ }
+
+ function resetPolygons() {
+ polygons.value.splice(0, polygons.value.length)
+ }
+
return {
waypoints,
polylines,
contextWaypoints,
+ polygons,
reset,
addWaypoint,
removeWaypoint,
@@ -68,6 +88,10 @@ export const useMapStore = defineStore('map', () => {
resetPolylines,
addContextWaypoint,
removeContextWaypoint,
- resetContextWaypoints
+ resetContextWaypoints,
+ addPolygon,
+ removePolygon,
+ removeCategoryPolygon,
+ resetPolygons
}
})
diff --git a/frontend/src/types/components/map.ts b/frontend/src/types/components/map.ts
index 67d143b1..585bd17c 100644
--- a/frontend/src/types/components/map.ts
+++ b/frontend/src/types/components/map.ts
@@ -6,6 +6,13 @@ export type Waypoint = {
id: string
category?: Uppercase
permanentTooltip?: boolean
+ // True heading, degrees clockwise from north (0-360), used to rotate the
+ // marker icon. Only meaningful for context waypoints that represent a
+ // heading object (e.g. ATM aircraft); leave unset for plain waypoints.
+ heading?: number
+ // True if another aircraft is currently inside this one's protected zone
+ // (ATM only)
+ inLos?: boolean
options?: Partial<{
stroke: boolean
radius: number
@@ -24,3 +31,19 @@ export type Polyline = {
color?: string
}
}
+
+export type Polygon = {
+ id: string
+ points: [number, number][]
+ category?: Uppercase
+ options?: Partial<{
+ stroke: boolean
+ color: string
+ weight: number
+ opacity: number
+ fill: boolean
+ fillColor: string
+ fillOpacity: number
+ dashArray: string
+ }>
+}