mobileapp / src /components /map /LeafletMapHtml.ts
Antaram Dev Bot
feat: complete ANTARAM.ORG ride-sharing app frontend
5c876be
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;
}
/**
* Returns a self-contained HTML string that renders a Leaflet.js map
* inside a WebView. Includes CDN links for Leaflet CSS / JS and
* communicates with the parent via `window.ReactNativeWebView.postMessage`.
*/
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: '&copy; <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>`;
}