import { safePromiseAll } from './fetch.mjs'; import { loadData } from './data-loader.mjs'; import { getSmallIconFromWmoCode } from '../icons.mjs'; import { getOpenMeteoObservationSnapshot } from './weather.mjs'; const BASE_MAP_URL = 'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}'; const BOUNDARY_MAP_URL = 'https://services.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}'; const DEFAULT_MAX_NEARBY_MARKERS = 7; const MIN_CITY_DISTANCE_METERS = 25000; const MIN_MARKER_PIXEL_DISTANCE = 85; let radarCitiesCache = null; const createMap = (mapElement) => window.L.map(mapElement, { zoomControl: false, dragging: false, touchZoom: false, scrollWheelZoom: false, doubleClickZoom: false, boxZoom: false, keyboard: false, tap: false, attributionControl: false, preferCanvas: true, }); const addBaseLayers = (map) => { const baseLayer = window.L.tileLayer(BASE_MAP_URL, { maxZoom: 10, minZoom: 1, crossOrigin: true, className: 'radar-base-layer', }).addTo(map); const boundaryLayer = window.L.tileLayer(BOUNDARY_MAP_URL, { maxZoom: 10, minZoom: 1, opacity: 0.6, crossOrigin: true, className: 'radar-boundary-layer', }).addTo(map); return { baseLayer, boundaryLayer }; }; const setPrimaryLocationMarker = (map, existingMarker, latitude, longitude) => { if (existingMarker && map.hasLayer(existingMarker)) { map.removeLayer(existingMarker); } return window.L.circleMarker([latitude, longitude], { radius: 5, color: '#000', weight: 2, fillColor: '#ff0', fillOpacity: 1, interactive: false, className: 'location-marker', }).addTo(map); }; const loadRadarCities = async () => { if (!radarCitiesCache) { radarCitiesCache = await loadData('radarcities'); } return radarCitiesCache ?? []; }; const selectNearbyCities = (map, sourceLocation, cities, options = {}) => { const { maxMarkers = DEFAULT_MAX_NEARBY_MARKERS, minCityDistanceMeters = MIN_CITY_DISTANCE_METERS, minMarkerPixelDistance = MIN_MARKER_PIXEL_DISTANCE, } = options; const bounds = map.getBounds(); const currentLatLng = window.L.latLng(sourceLocation.latitude, sourceLocation.longitude); const visibleCities = cities .filter((city) => bounds.contains([city.lat, city.lon])) .filter((city) => currentLatLng.distanceTo([city.lat, city.lon]) > minCityDistanceMeters) .map((city) => ({ ...city, distance: currentLatLng.distanceTo([city.lat, city.lon]), point: map.latLngToContainerPoint([city.lat, city.lon]), })) .sort((a, b) => a.distance - b.distance); const selected = []; visibleCities.forEach((city) => { if (selected.length >= maxMarkers) return; const overlaps = selected.some((existingCity) => existingCity.point.distanceTo(city.point) < minMarkerPixelDistance); if (!overlaps) selected.push(city); }); if (selected.length === 0 && visibleCities.length > 0) { selected.push(visibleCities[0]); } return selected; }; const buildNearbyWeatherMarker = (city, observation) => { const icon = getSmallIconFromWmoCode(observation.weatherCode, observation.isDay); const markerHtml = `
${city.name}
${Math.round(observation.temperature)}
${city.name} weather
`; return window.L.marker([city.lat, city.lon], { icon: window.L.divIcon({ html: markerHtml, className: 'nearby-weather-marker', iconSize: [108, 52], iconAnchor: [54, 26], }), interactive: false, zIndexOffset: 500, }); }; const clearMarkers = (map, markers) => { if (!map || !markers?.length) return []; markers.forEach((marker) => { if (map.hasLayer(marker)) map.removeLayer(marker); }); return []; }; const loadNearbyObservationMarkers = async (map, sourceLocation, options = {}) => { const radarCities = await loadRadarCities(); const nearbyCities = selectNearbyCities(map, sourceLocation, radarCities, options); if (!nearbyCities.length) return []; const nearbyObservations = await safePromiseAll(nearbyCities.map(async (city) => { const observation = await getOpenMeteoObservationSnapshot(city.lat, city.lon); if (!observation || observation.temperature === null) return null; return { city, observation }; })); return nearbyObservations .filter((entry) => entry) .map(({ city, observation }) => buildNearbyWeatherMarker(city, observation)); }; export { createMap, addBaseLayers, setPrimaryLocationMarker, loadNearbyObservationMarkers, clearMarkers, };