Skip to main content

Overview

MapMarkerService manages the creation, update, and animation of aircraft markers on the Leaflet map. It handles:
  • Creating custom plane icons with rotation and status indicators
  • Smooth marker animations between position updates
  • Click event handling for flight selection
  • Intelligent filtering of invalid position updates
  • Marker lifecycle management (creation and cleanup)
The service uses the leaflet-moving-rotated-marker plugin to provide smooth, animated transitions between flight positions.

Location

src/app/features/flights/services/map-marker.service.ts

Methods

makePlaneIcon()

Creates a custom Leaflet DivIcon for displaying aircraft on the map. The icon includes rotation, status classes, and selection state.
flight
Flight
required
Flight object containing aircraft data including ground status
selected
boolean
required
Whether this flight is currently selected
Returns
L.DivIcon
Leaflet DivIcon with appropriate classes and SVG content
Icon Classes:
  • plane-marker - Base class for all plane icons
  • plane--air - Applied when flight is in the air
  • plane--ground - Applied when flight is on the ground
  • is-selected - Applied when flight is selected
Signature:
makePlaneIcon(flight: Flight, selected: boolean): L.DivIcon
Usage Example:
const icon = this.mapMarkerService.makePlaneIcon(flight, false);
const marker = L.marker([lat, lng], { 
  icon,
  rotationAngle: flight.heading ?? 0 
});
Source: map-marker.service.ts:31-49

updateMarkers()

Updates all flight markers on the map. This method:
  • Removes markers for flights that no longer exist
  • Creates new markers for new flights
  • Animates existing markers to new positions
  • Applies intelligent filtering to reject invalid position updates
flights
Flight[]
required
Array of current flights to display on the map
markersLayer
L.LayerGroup
required
Leaflet layer group to add/remove markers from
Signature:
updateMarkers(flights: Flight[], markersLayer: L.LayerGroup): void
Animation Parameters:
  • Duration: 12000ms (12 seconds) for smooth movement
  • Easing: ‘easeInOutQuart’ for natural acceleration/deceleration
  • Rotation: Synchronized with marker movement
Usage Example:
import { Component, effect, inject } from '@angular/core';
import { MapMarkerService } from '../services/map-marker.service';
import * as L from 'leaflet';

@Component({
  selector: 'app-flights-map',
  // ...
})
export class FlightsMapComponent {
  private readonly markerService = inject(MapMarkerService);
  private markersLayer: L.LayerGroup = L.layerGroup();

  constructor() {
    effect(() => {
      const flights = this.flights();
      if (this.map) {
        this.markerService.updateMarkers(flights, this.markersLayer);
      }
    });
  }
}
Source: map-marker.service.ts:51-109

updateIcons()

Updates marker icons to reflect selection state. Resets all markers to normal state, then applies selected styling to the currently selected flight.
flights
Flight[]
required
Array of all flights
selectedId
string | null
required
ICAO24 identifier of the selected flight, or null if none selected
Signature:
updateIcons(flights: Flight[], selectedId: string | null): void
Usage Example:
import { Component, effect, inject } from '@angular/core';
import { MapMarkerService } from '../services/map-marker.service';
import { FlightsStoreService } from '../services/flights-store.service';

@Component({
  selector: 'app-flights-map',
  // ...
})
export class FlightsMapComponent {
  private readonly markerService = inject(MapMarkerService);
  private readonly store = inject(FlightsStoreService);

  constructor() {
    effect(() => {
      const selectedId = this.store.selectedFlightId();
      const flights = this.flights();
      
      if (this.map && flights.length) {
        this.markerService.updateIcons(flights, selectedId);
      }
    });
  }
}
Source: map-marker.service.ts:134-152

Position Update Filtering

The service includes intelligent filtering to reject invalid position updates. This prevents unrealistic jumps and ensures smooth animations.

Rejection Criteria

Outdated Positions (from shouldAcceptUpdate() at map-marker.service.ts:169-207):
  • Rejects updates with older timestamps than current position
Duplicate Positions:
  • Rejects updates less than 500 meters from current position
Unrealistic Speed:
  • Calculates implied speed between positions using Haversine formula
  • Rejects updates implying speed > 1300 km/h

Haversine Distance Calculation

The service uses the Haversine formula to calculate great-circle distances between coordinates:
private haversineKm(
  lat1: number, 
  lon1: number, 
  lat2: number, 
  lon2: number
): number {
  const toRad = (v: number) => v * Math.PI / 180;
  const dLat = toRad(lat2 - lat1);
  const dLon = toRad(lon2 - lon1);
  
  const a =
    Math.sin(dLat / 2) ** 2 +
    Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * 
    Math.sin(dLon / 2) ** 2;
  
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  
  return EARTH_RADIUS_KM * c;  // 6371 km
}
Source: map-marker.service.ts:158-167

Real-World Usage

From flights-map.component.ts:22-47, here’s the complete integration:
import { 
  Component, 
  effect, 
  inject, 
  AfterViewInit, 
  OnDestroy 
} from '@angular/core';
import * as L from 'leaflet';
import 'leaflet-moving-rotated-marker';
import { MapMarkerService } from '../services/map-marker.service';
import { FlightsStoreService } from '../services/flights-store.service';
import { Flight } from '../models/flight.model';

@Component({
  selector: 'app-flights-map',
  // ...
})
export class FlightsMapComponent implements AfterViewInit, OnDestroy {
  flights = input<Flight[]>([]);
  
  private readonly markerService = inject(MapMarkerService);
  private readonly store = inject(FlightsStoreService);
  
  private map!: L.Map;
  private markersLayer: L.LayerGroup = L.layerGroup();

  constructor() {
    // Update marker positions when flights change
    effect(() => {
      const flights = this.flights();
      if (this.map) {
        this.markerService.updateMarkers(flights, this.markersLayer);
      }
    });

    // Update marker icons when selection changes
    effect(() => {
      const selectedId = this.store.selectedFlightId();
      const flights = this.flights();
      
      if (this.map && flights.length) {
        this.markerService.updateIcons(flights, selectedId);
      }
    });
  }

  ngAfterViewInit(): void {
    this.initiateMap();
    
    // Initial marker creation after map is ready
    setTimeout(() => {
      const flights = this.flights();
      if (flights.length) {
        this.markerService.updateMarkers(flights, this.markersLayer);
      }
    }, 50);
  }

  ngOnDestroy(): void {
    if (this.map) {
      this.map.remove();
    }
  }

  private initiateMap(): void {
    this.map = L.map('map', {
      center: [40, 0],
      zoom: 6,
      zoomControl: false,
    });
    
    this.markersLayer.addTo(this.map);
  }
}

Marker Click Handling

Markers automatically handle click events to toggle flight selection (from map-marker.service.ts:77-84):
marker.on('click', () => {
  const currentSelected = this.store.selectedFlightId();
  
  if (currentSelected === flight.icao24) {
    // Clicking selected flight deselects it
    this.store.clearSelection();
  } else {
    // Clicking unselected flight selects it
    this.store.setSelectedFlightId(flight.icao24);
  }
});

Internal State

The service maintains an internal Map of markers:
private markers = new Map<string, L.Marker>();
  • Key: Flight ICAO24 identifier
  • Value: Leaflet Marker instance
This allows efficient lookup and updates without recreating markers unnecessarily.

Dependencies

The service automatically injects:
  • FlightsStoreService - For accessing selected flight state
External dependencies:
  • leaflet - Map and marker functionality
  • leaflet-moving-rotated-marker - Smooth animated transitions

See Also

Build docs developers (and LLMs) love