Add custom icon support and routing functionality to map adapters
- Add icon parameter to IMapAdapter.addMarker() supporting DivIcon (Leaflet) and HTMLElement (MapLibre) - Implement routing with leaflet-routing-machine and @maplibre/maplibre-gl-directions - Add marker management methods: hasMarker, updateMarkerIcon, updateMarkerPopup, addMarkerClickHandler - Add route management methods: addRoute, updateRoute, removeRoute, removeAllRoutes - Update MapFacade to proxy all new marker and route management methods - Add RouteOptions interface with customizable line styles and waypoint options 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: David Nguyen <david.nguyen@goutezplanb.com>
This commit is contained in:
@@ -20,7 +20,9 @@
|
||||
"@angular/common": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"@angular/core": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"maplibre-gl": "^5.0.0",
|
||||
"leaflet": "^1.0.0 || ^2.0.0"
|
||||
"leaflet": "^1.0.0 || ^2.0.0",
|
||||
"leaflet-routing-machine": "^3.0.0",
|
||||
"@maplibre/maplibre-gl-directions": "^0.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import {
|
||||
import L,{
|
||||
Map,
|
||||
TileLayer,
|
||||
Marker,
|
||||
Popup,
|
||||
polygon
|
||||
polygon, DivIcon
|
||||
} from 'leaflet';
|
||||
import 'leaflet-routing-machine';
|
||||
|
||||
import {
|
||||
getLngLat,
|
||||
IMapAdapter,
|
||||
LatLng,
|
||||
MapOptions, Zone,
|
||||
MapOptions,
|
||||
Zone,
|
||||
RouteOptions,
|
||||
} from './map-adapter.interface';
|
||||
|
||||
export class LeafletAdapter implements IMapAdapter<Marker> {
|
||||
@@ -18,6 +21,7 @@ export class LeafletAdapter implements IMapAdapter<Marker> {
|
||||
private deliveryCheckMarker?: Marker;
|
||||
private markers: Record<string, Marker> = {};
|
||||
private popup?: Popup;
|
||||
private routes: Record<string, any> = {};
|
||||
|
||||
init(container: HTMLElement, options: MapOptions): void {
|
||||
this.map = new Map(container).setView(options.center, options.zoom);
|
||||
@@ -35,22 +39,26 @@ export class LeafletAdapter implements IMapAdapter<Marker> {
|
||||
this.map.setZoom(zoom);
|
||||
}
|
||||
|
||||
addMarker(latLng: LatLng, options: { id?: string; color?: string }) {
|
||||
if (options.id) {
|
||||
addMarker(latLng: LatLng, options?: { id?: string; color?: string ; icon? : DivIcon }) {
|
||||
const markerOptions: L.MarkerOptions = {};
|
||||
if (options?.icon) {
|
||||
markerOptions.icon = options.icon;
|
||||
}
|
||||
|
||||
if (options?.id) {
|
||||
if (this.markers[options.id]) {
|
||||
this.map.removeLayer(this.markers[options.id]);
|
||||
}
|
||||
const marker = new Marker(latLng)
|
||||
const marker = new Marker(latLng, markerOptions);
|
||||
marker.addTo(this.map);
|
||||
this.markers[options.id] = marker;
|
||||
} else {
|
||||
if (this.deliveryCheckMarker) {
|
||||
this.map.removeLayer(this.deliveryCheckMarker);
|
||||
}
|
||||
this.deliveryCheckMarker = new Marker(latLng).addTo(this.map);
|
||||
this.deliveryCheckMarker = new Marker(latLng, markerOptions).addTo(this.map);
|
||||
}
|
||||
}
|
||||
|
||||
getMarker(id: string): Marker | undefined {
|
||||
return this.markers[id];
|
||||
}
|
||||
@@ -72,6 +80,31 @@ export class LeafletAdapter implements IMapAdapter<Marker> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasMarker(id: string): boolean {
|
||||
return !!this.markers[id];
|
||||
}
|
||||
|
||||
updateMarkerIcon(id: string, icon: any): void {
|
||||
const marker = this.markers[id];
|
||||
if (marker) {
|
||||
marker.setIcon(icon);
|
||||
}
|
||||
}
|
||||
|
||||
updateMarkerPopup(id: string, popup: any): void {
|
||||
const marker = this.markers[id];
|
||||
if (marker) {
|
||||
marker.bindPopup(popup);
|
||||
}
|
||||
}
|
||||
|
||||
addMarkerClickHandler(id: string, handler: (e: any) => void): void {
|
||||
const marker = this.markers[id];
|
||||
if (marker) {
|
||||
marker.on('click', handler);
|
||||
}
|
||||
}
|
||||
addZone(zones: Zone[]): void
|
||||
{
|
||||
this.updateZone(zones);
|
||||
@@ -128,12 +161,67 @@ export class LeafletAdapter implements IMapAdapter<Marker> {
|
||||
this.popup = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
addRoute(id: string, waypoints: LatLng[], options?: RouteOptions): void {
|
||||
this.removeRoute(id);
|
||||
|
||||
const routeOptions: any = {
|
||||
waypoints: waypoints.map(wp => L.latLng(wp[0], wp[1])),
|
||||
lineOptions: {
|
||||
styles: [
|
||||
{
|
||||
color: options?.lineStyle?.color || '#3388ff',
|
||||
opacity: options?.lineStyle?.opacity ?? 1,
|
||||
weight: options?.lineStyle?.weight || 4
|
||||
}
|
||||
],
|
||||
extendToWaypoints: false,
|
||||
addWaypoints: false,
|
||||
missingRouteTolerance: 10
|
||||
},
|
||||
createMarker: options?.showMarkers ? undefined : () => false,
|
||||
draggableWaypoints: options?.draggableWaypoints ?? false,
|
||||
routeWhileDragging: false,
|
||||
summaryTemplate: '',
|
||||
distanceTemplate: '',
|
||||
timeTemplate: '',
|
||||
show: true
|
||||
};
|
||||
|
||||
if (options?.serviceUrl) {
|
||||
console.log('serviceUrl', options);
|
||||
routeOptions.router = L.Routing.osrmv1({
|
||||
serviceUrl: options.serviceUrl
|
||||
});
|
||||
}
|
||||
|
||||
const control = L.Routing
|
||||
.control(routeOptions)
|
||||
.addTo(this.map);
|
||||
this.routes[id] = control;
|
||||
}
|
||||
|
||||
updateRoute(id: string, waypoints: LatLng[], options?: RouteOptions): void {
|
||||
this.addRoute(id, waypoints, options);
|
||||
}
|
||||
|
||||
removeRoute(id: string): void {
|
||||
if (this.routes[id]) {
|
||||
this.routes[id].remove();
|
||||
delete this.routes[id];
|
||||
}
|
||||
}
|
||||
|
||||
removeAllRoutes(): void {
|
||||
Object.keys(this.routes).forEach(id => this.removeRoute(id));
|
||||
}
|
||||
|
||||
on(type: string, event: (e: any) => void): void
|
||||
{
|
||||
this.map.on(type, event);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.removeAllRoutes();
|
||||
this.map.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Map, Marker, NavigationControl, GeoJSONSource, Popup } from 'maplibre-gl';
|
||||
import {getLngLat, IMapAdapter, LatLng, MapOptions, Zone} from './map-adapter.interface';
|
||||
|
||||
import {getLngLat, IMapAdapter, LatLng, MapOptions, Zone, RouteOptions} from './map-adapter.interface';
|
||||
import MapLibreGlDirections from "@maplibre/maplibre-gl-directions";
|
||||
export class LibreAdapter implements IMapAdapter<Marker> {
|
||||
private map!: Map;
|
||||
private deliveryCheckMarker?: Marker;
|
||||
private markers: Record<string, Marker> = {};
|
||||
private popup?: Popup;
|
||||
private routes: Record<string, any> = {};
|
||||
private routeLayers: Record<string, [string, string]> = {};
|
||||
init(container: HTMLElement, options: MapOptions): void {
|
||||
this.map = new Map({
|
||||
container,
|
||||
@@ -25,15 +27,24 @@ export class LibreAdapter implements IMapAdapter<Marker> {
|
||||
this.map.setZoom(zoom);
|
||||
}
|
||||
|
||||
addMarker(latLng: LatLng, options: { id?: string; color?: string }) {
|
||||
addMarker(latLng: LatLng, options?: { id?: string; color?: string; icon?: HTMLElement }) {
|
||||
const coords = getLngLat(latLng);
|
||||
|
||||
if (options.id) {
|
||||
const markerOptions: any = {};
|
||||
if (options?.icon) {
|
||||
markerOptions.element = options.icon;
|
||||
} else if (options?.color) {
|
||||
markerOptions.color = options.color;
|
||||
} else {
|
||||
markerOptions.color = 'red';
|
||||
}
|
||||
|
||||
if (options?.id) {
|
||||
if (this.markers[options.id]) {
|
||||
this.markers[options.id].remove();
|
||||
}
|
||||
|
||||
const marker = new Marker({ color: options.color || 'red' })
|
||||
const marker = new Marker(markerOptions)
|
||||
.setLngLat(coords)
|
||||
.addTo(this.map);
|
||||
this.markers[options.id] = marker;
|
||||
@@ -41,7 +52,7 @@ export class LibreAdapter implements IMapAdapter<Marker> {
|
||||
if (this.deliveryCheckMarker) {
|
||||
this.deliveryCheckMarker.remove();
|
||||
}
|
||||
this.deliveryCheckMarker = new Marker({ color: options?.color || 'red' })
|
||||
this.deliveryCheckMarker = new Marker(markerOptions)
|
||||
.setLngLat(coords)
|
||||
.addTo(this.map);
|
||||
}
|
||||
@@ -71,6 +82,31 @@ export class LibreAdapter implements IMapAdapter<Marker> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasMarker(id: string): boolean {
|
||||
return !!this.markers[id];
|
||||
}
|
||||
|
||||
updateMarkerIcon(id: string, icon: HTMLElement): void {
|
||||
const marker = this.markers[id];
|
||||
if (marker) {
|
||||
marker.getElement().replaceWith(icon);
|
||||
}
|
||||
}
|
||||
|
||||
updateMarkerPopup(id: string, popup: Popup): void {
|
||||
const marker = this.markers[id];
|
||||
if (marker) {
|
||||
marker.setPopup(popup);
|
||||
}
|
||||
}
|
||||
|
||||
addMarkerClickHandler(id: string, handler: (e: any) => void): void {
|
||||
const marker = this.markers[id];
|
||||
if (marker) {
|
||||
marker.getElement().addEventListener('click', handler);
|
||||
}
|
||||
}
|
||||
addZone(zones: Zone[]): void
|
||||
{
|
||||
this.updateZone(zones);
|
||||
@@ -166,11 +202,95 @@ export class LibreAdapter implements IMapAdapter<Marker> {
|
||||
this.popup = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
addRoute(id: string, waypoints: LatLng[], options?: RouteOptions): void {
|
||||
this.removeRoute(id);
|
||||
|
||||
// Convert waypoints to GeoJSON LineString format [lng, lat]
|
||||
const coordinates = waypoints.map(wp => getLngLat(wp));
|
||||
|
||||
const sourceId = `route-${id}`;
|
||||
const layerId = `route-layer-${id}`;
|
||||
|
||||
// Add the route as a GeoJSON source
|
||||
this.map.addSource(sourceId, {
|
||||
type: 'geojson',
|
||||
data: {
|
||||
type: 'Feature',
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: 'LineString',
|
||||
coordinates: coordinates
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add the route layer
|
||||
this.map.addLayer({
|
||||
id: layerId,
|
||||
type: 'line',
|
||||
source: sourceId,
|
||||
layout: {
|
||||
'line-join': 'round',
|
||||
'line-cap': 'round'
|
||||
},
|
||||
paint: {
|
||||
'line-color': options?.lineStyle?.color || '#3388ff',
|
||||
'line-width': options?.lineStyle?.weight || 4,
|
||||
'line-opacity': options?.lineStyle?.opacity ?? 1
|
||||
}
|
||||
});
|
||||
|
||||
this.routeLayers[id] = [sourceId, layerId];
|
||||
|
||||
// Optionally add markers for waypoints
|
||||
if (options?.showMarkers) {
|
||||
waypoints.forEach((wp, index) => {
|
||||
const markerId = `${id}-waypoint-${index}`;
|
||||
this.addMarker(wp, { id: markerId, color: options.lineStyle?.color || '#3388ff' });
|
||||
this.routes[markerId] = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateRoute(id: string, waypoints: LatLng[], options?: RouteOptions): void {
|
||||
this.addRoute(id, waypoints, options);
|
||||
}
|
||||
|
||||
removeRoute(id: string): void {
|
||||
if (this.routeLayers[id]) {
|
||||
const [sourceId, layerId] = this.routeLayers[id];
|
||||
|
||||
if (this.map.getLayer(layerId)) {
|
||||
this.map.removeLayer(layerId);
|
||||
}
|
||||
|
||||
if (this.map.getSource(sourceId)) {
|
||||
this.map.removeSource(sourceId);
|
||||
}
|
||||
|
||||
delete this.routeLayers[id];
|
||||
}
|
||||
|
||||
// Remove waypoint markers if they exist
|
||||
Object.keys(this.routes).forEach(key => {
|
||||
if (key.startsWith(`${id}-waypoint-`)) {
|
||||
this.removeMarker(key);
|
||||
delete this.routes[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeAllRoutes(): void {
|
||||
Object.keys(this.routeLayers).forEach(id => this.removeRoute(id));
|
||||
}
|
||||
|
||||
on(type: string, event: (e: any) => void): void
|
||||
{
|
||||
this.map.on(type, event);
|
||||
}
|
||||
destroy(): void {
|
||||
this.removeAllRoutes();
|
||||
this.map.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,18 +24,47 @@ export function getLngLat(latLng: LatLng): [number, number] {
|
||||
return [latLng[1], latLng[0]];
|
||||
}
|
||||
|
||||
export interface RouteLineStyle {
|
||||
color?: string;
|
||||
opacity?: number;
|
||||
weight?: number;
|
||||
}
|
||||
|
||||
export interface RouteOptions {
|
||||
serviceUrl?: string;
|
||||
lineStyle?: RouteLineStyle;
|
||||
showMarkers?: boolean;
|
||||
draggableWaypoints?: boolean;
|
||||
}
|
||||
|
||||
export interface IMapAdapter<TMarker = any> {
|
||||
init(container: HTMLElement, options: MapOptions): void;
|
||||
setCenter(latLng: LatLng): void;
|
||||
setZoom(zoom: number): void;
|
||||
addMarker(latLng: LatLng, options?: { color?: string }): void;
|
||||
|
||||
// Marker management
|
||||
addMarker(latLng: LatLng, options?: {id?: string; color?: string; icon?: any}): void;
|
||||
getMarker(id: string): TMarker | undefined;
|
||||
getAllMarkers(): Record<string, TMarker>;
|
||||
removeMarker(id?: string): void;
|
||||
hasMarker(id: string): boolean;
|
||||
updateMarkerIcon(id: string, icon: any): void;
|
||||
updateMarkerPopup(id: string, popup: any): void;
|
||||
addMarkerClickHandler(id: string, handler: (e: any) => void): void;
|
||||
|
||||
// Zone management
|
||||
addZone(zone: Zone[]): void;
|
||||
updateZone(zone: Zone[]): void;
|
||||
openZonePopup(zone: Zone) : void
|
||||
closePopup(): void;
|
||||
|
||||
// Route management
|
||||
addRoute(id: string, waypoints: LatLng[], options?: RouteOptions): void;
|
||||
updateRoute(id: string, waypoints: LatLng[], options?: RouteOptions): void;
|
||||
removeRoute(id: string): void;
|
||||
removeAllRoutes(): void;
|
||||
|
||||
// Utilities
|
||||
destroy(): void;
|
||||
on(
|
||||
type: string,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {IMapAdapter, MapOptions, LatLng, getLngLat, Zone} from './map-adapter.interface';
|
||||
import {IMapAdapter, MapOptions, LatLng, getLngLat, Zone, RouteOptions} from './map-adapter.interface';
|
||||
import { LibreAdapter } from './libre-adapter';
|
||||
import { LeafletAdapter } from './leaflet-adapter';
|
||||
|
||||
@@ -30,7 +30,7 @@ export class MapFacade implements IMapAdapter<any> {
|
||||
this.adapter.setZoom(zoom);
|
||||
}
|
||||
|
||||
addMarker(latLng: LatLng, options: { id?: string; color?: string }): void {
|
||||
addMarker(latLng: LatLng, options: { id?: string; color?: string; icon?: any}): void {
|
||||
this.adapter.addMarker(latLng, options);
|
||||
}
|
||||
|
||||
@@ -45,6 +45,23 @@ export class MapFacade implements IMapAdapter<any> {
|
||||
removeMarker(id?: string): void {
|
||||
this.adapter.removeMarker(id);
|
||||
}
|
||||
|
||||
hasMarker(id: string): boolean {
|
||||
return this.adapter.hasMarker(id);
|
||||
}
|
||||
|
||||
updateMarkerIcon(id: string, icon: any): void {
|
||||
this.adapter.updateMarkerIcon(id, icon);
|
||||
}
|
||||
|
||||
updateMarkerPopup(id: string, popup: any): void {
|
||||
this.adapter.updateMarkerPopup(id, popup);
|
||||
}
|
||||
|
||||
addMarkerClickHandler(id: string, handler: (e: any) => void): void {
|
||||
this.adapter.addMarkerClickHandler(id, handler);
|
||||
}
|
||||
|
||||
addZone(zones: Zone[]): void
|
||||
{
|
||||
this.adapter.addZone(zones);
|
||||
@@ -61,6 +78,23 @@ export class MapFacade implements IMapAdapter<any> {
|
||||
{
|
||||
this.adapter.closePopup();
|
||||
}
|
||||
|
||||
addRoute(id: string, waypoints: LatLng[], options?: RouteOptions): void {
|
||||
this.adapter.addRoute(id, waypoints, options);
|
||||
}
|
||||
|
||||
updateRoute(id: string, waypoints: LatLng[], options?: RouteOptions): void {
|
||||
this.adapter.updateRoute(id, waypoints, options);
|
||||
}
|
||||
|
||||
removeRoute(id: string): void {
|
||||
this.adapter.removeRoute(id);
|
||||
}
|
||||
|
||||
removeAllRoutes(): void {
|
||||
this.adapter.removeAllRoutes();
|
||||
}
|
||||
|
||||
on(type: string, event: (e: any) => void): void
|
||||
{
|
||||
this.adapter.on(type, event);
|
||||
|
||||
Reference in New Issue
Block a user