| import type { LatLng } from '../../types/location'; |
|
|
| export interface LeafletMapMarker { |
| lat: number; |
| lng: number; |
| title?: string; |
| type?: 'origin' | 'destination' | 'pickup' | 'default'; |
| } |
|
|
| export interface LeafletMapPolyline { |
| points: LatLng[]; |
| color?: string; |
| weight?: number; |
| } |
|
|
| export interface LeafletMapOptions { |
| centerLat: number; |
| centerLng: number; |
| zoom?: number; |
| markers?: LeafletMapMarker[]; |
| polylines?: LeafletMapPolyline[]; |
| showCurrentLocation?: boolean; |
| interactive?: boolean; |
| showRoute?: boolean; |
| } |
|
|
| |
| |
| |
| |
| |
| export function getLeafletMapHtml(options: LeafletMapOptions): string { |
| const { |
| centerLat, |
| centerLng, |
| zoom = 14, |
| markers = [], |
| polylines = [], |
| showCurrentLocation = false, |
| interactive = true, |
| } = options; |
|
|
| const markersJson = JSON.stringify(markers); |
| const polylinesJson = JSON.stringify(polylines); |
|
|
| return `<!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> |
| <link |
| rel="stylesheet" |
| href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" |
| integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" |
| crossorigin="" |
| /> |
| <style> |
| * { margin: 0; padding: 0; box-sizing: border-box; } |
| html, body { height: 100%; width: 100%; overflow: hidden; } |
| #map { width: 100%; height: 100%; } |
| |
| /* Pulsing user-location marker */ |
| .pulse-marker { |
| width: 18px; |
| height: 18px; |
| background: #4CAF50; |
| border: 3px solid #FFFFFF; |
| border-radius: 50%; |
| box-shadow: 0 0 0 rgba(76, 175, 80, 0.4); |
| animation: pulse 2s ease-in-out infinite; |
| } |
| @keyframes pulse { |
| 0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.6); } |
| 70% { box-shadow: 0 0 0 16px rgba(76, 175, 80, 0); } |
| 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); } |
| } |
| |
| .origin-marker { |
| width: 14px; |
| height: 14px; |
| background: #1B6B4D; |
| border: 3px solid #FFFFFF; |
| border-radius: 50%; |
| box-shadow: 0 1px 4px rgba(0,0,0,0.3); |
| } |
| .destination-marker { |
| width: 14px; |
| height: 14px; |
| background: #E8A838; |
| border: 3px solid #FFFFFF; |
| border-radius: 50%; |
| box-shadow: 0 1px 4px rgba(0,0,0,0.3); |
| } |
| .default-marker { |
| width: 12px; |
| height: 12px; |
| background: #1565C0; |
| border: 2px solid #FFFFFF; |
| border-radius: 50%; |
| box-shadow: 0 1px 4px rgba(0,0,0,0.3); |
| } |
| </style> |
| </head> |
| <body> |
| <div id="map"></div> |
| |
| <script |
| src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" |
| integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" |
| crossorigin="" |
| ></script> |
| <script> |
| (function () { |
| var centerLat = ${centerLat}; |
| var centerLng = ${centerLng}; |
| var zoom = ${zoom}; |
| var interactive = ${interactive}; |
| var showCurrentLocation = ${showCurrentLocation}; |
| var markers = ${markersJson}; |
| var polylines = ${polylinesJson}; |
| |
| var map = L.map('map', { |
| center: [centerLat, centerLng], |
| zoom: zoom, |
| zoomControl: interactive, |
| attributionControl: interactive, |
| dragging: interactive, |
| scrollWheelZoom: interactive, |
| doubleClickZoom: interactive, |
| touchZoom: interactive, |
| boxZoom: interactive, |
| keyboard: interactive, |
| tap: interactive, |
| }); |
| |
| if (interactive) { |
| L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { |
| attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', |
| maxZoom: 19, |
| }).addTo(map); |
| } else { |
| L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { |
| maxZoom: 19, |
| attributionControl: false, |
| }).addTo(map); |
| map.attributionControl && map.removeControl(map.attributionControl); |
| } |
| |
| // Current location marker |
| var currentLocMarker = null; |
| if (showCurrentLocation) { |
| var pulseIcon = L.divIcon({ |
| className: '', |
| html: '<div class="pulse-marker"></div>', |
| iconSize: [18, 18], |
| iconAnchor: [9, 9], |
| }); |
| currentLocMarker = L.marker([centerLat, centerLng], { icon: pulseIcon }).addTo(map); |
| } |
| |
| // Custom markers |
| var markerInstances = []; |
| markers.forEach(function (m) { |
| var cssClass = 'default-marker'; |
| if (m.type === 'origin') cssClass = 'origin-marker'; |
| if (m.type === 'destination') cssClass = 'destination-marker'; |
| |
| var icon = L.divIcon({ |
| className: '', |
| html: '<div class="' + cssClass + '"></div>', |
| iconSize: [14, 14], |
| iconAnchor: [7, 7], |
| }); |
| |
| var marker = L.marker([m.lat, m.lng], { icon: icon }).addTo(map); |
| if (m.title) { |
| marker.bindTooltip(m.title, { direction: 'top', offset: [0, -10] }); |
| } |
| marker.on('click', function () { |
| sendMsg({ event: 'marker_tap', data: { lat: m.lat, lng: m.lng, title: m.title, type: m.type } }); |
| }); |
| markerInstances.push(marker); |
| }); |
| |
| // Polylines |
| polylines.forEach(function (p) { |
| var latLngs = p.points.map(function (pt) { return [pt.lat, pt.lng]; }); |
| L.polyline(latLngs, { |
| color: p.color || '#1B6B4D', |
| weight: p.weight || 5, |
| opacity: 0.8, |
| lineCap: 'round', |
| lineJoin: 'round', |
| }).addTo(map); |
| }); |
| |
| // Map move events |
| if (interactive) { |
| map.on('moveend', function () { |
| var c = map.getCenter(); |
| sendMsg({ event: 'map_move', data: { lat: c.lat, lng: c.lng, zoom: map.getZoom() } }); |
| }); |
| } |
| |
| function sendMsg(obj) { |
| if (window.ReactNativeWebView) { |
| window.ReactNativeWebView.postMessage(JSON.stringify(obj)); |
| } |
| } |
| |
| // Fit bounds to markers if we have multiple |
| if (markerInstances.length >= 2) { |
| var group = L.featureGroup(markerInstances); |
| map.fitBounds(group.getBounds().pad(0.15)); |
| } |
| |
| // Notify ready |
| sendMsg({ event: 'map_ready', data: { lat: centerLat, lng: centerLng, zoom: zoom } }); |
| |
| // Expose methods for the RN side to call via injectedJavaScript |
| window.setCenter = function (lat, lng, z) { |
| map.setView([lat, lng], z || map.getZoom()); |
| }; |
| window.addMarker = function (lat, lng, type, title) { |
| var cssClass = 'default-marker'; |
| if (type === 'origin') cssClass = 'origin-marker'; |
| if (type === 'destination') cssClass = 'destination-marker'; |
| var icon = L.divIcon({ |
| className: '', |
| html: '<div class="' + cssClass + '"></div>', |
| iconSize: [14, 14], |
| iconAnchor: [7, 7], |
| }); |
| var marker = L.marker([lat, lng], { icon: icon }).addTo(map); |
| if (title) marker.bindTooltip(title, { direction: 'top', offset: [0, -10] }); |
| return marker; |
| }; |
| window.updateCurrentLocation = function (lat, lng) { |
| if (currentLocMarker) { |
| currentLocMarker.setLatLng([lat, lng]); |
| } else { |
| var pulseIcon = L.divIcon({ |
| className: '', |
| html: '<div class="pulse-marker"></div>', |
| iconSize: [18, 18], |
| iconAnchor: [9, 9], |
| }); |
| currentLocMarker = L.marker([lat, lng], { icon: pulseIcon }).addTo(map); |
| } |
| }; |
| window.fitToMarkers = function () { |
| if (markerInstances.length >= 2) { |
| map.fitBounds(L.featureGroup(markerInstances).getBounds().pad(0.15)); |
| } |
| }; |
| })(); |
| </script> |
| </body> |
| </html>`; |
| } |
|
|