UNPKG

@maptiler/geocoding-control

Version:

The Javascript & TypeScript Map Control component for MapTiler Geocoding service. Easy to be integrated into any JavaScript mapping application.

275 lines (274 loc) 10.9 kB
import bbox from "@turf/bbox"; import clone from "@turf/clone"; import { feature, featureCollection } from "@turf/helpers"; import union from "@turf/union"; import * as L from "leaflet"; import { unwrapBbox } from "./geoUtils"; import MarkerIcon from "./MarkerIcon.svelte"; import { setMask } from "./mask"; const defaultFullGeometryStyle = (feature) => { const type = feature?.geometry?.type; const weight = feature?.properties?.isMask ? 0 : type === "LineString" || type === "MultiLineString" ? 3 : 2; return { color: "#3170fe", fillColor: "#000", fillOpacity: feature?.properties?.isMask ? 0.1 : 0, weight, dashArray: [weight, weight], lineCap: "butt", }; }; export function createLeafletMapController(map, marker = true, showResultMarkers = true, flyToOptions = {}, flyToBounds = {}, fullGeometryStyle = defaultFullGeometryStyle) { let eventHandler; const markers = []; let selectedMarker; let reverseMarker; const resultLayer = L.geoJSON(undefined, { style: fullGeometryStyle === true ? defaultFullGeometryStyle : fullGeometryStyle === false ? undefined : (fullGeometryStyle ?? undefined), interactive: false, }).addTo(map); const handleMapClick = (e) => { eventHandler?.({ type: "mapClick", coordinates: [e.latlng.lng, e.latlng.lat], }); }; function createMarker(pos, interactive = false) { const element = document.createElement("div"); new MarkerIcon({ props: { displayIn: "leaflet" }, target: element }); return new L.Marker(pos, { interactive, icon: new L.DivIcon({ html: element, className: "", iconAnchor: [12, 26], iconSize: [25, 30], tooltipAnchor: [1, -24], }), }); } return { setEventHandler(handler) { if (handler) { eventHandler = handler; map.on("click", handleMapClick); } else { eventHandler = undefined; map.off("click", handleMapClick); } }, flyTo(center, zoom) { map.flyTo([center[1], center[0]], zoom, { duration: 2, ...flyToOptions }); }, fitBounds(bbox, padding, maxZoom) { map.flyToBounds([ [bbox[1], bbox[0]], [bbox[3], bbox[2]], ], { padding: [padding, padding], duration: 2, ...(maxZoom ? { maxZoom } : {}), ...flyToBounds, }); }, indicateReverse(reverse) { map.getContainer().style.cursor = reverse ? "crosshair" : ""; }, setReverseMarker(coordinates) { if (!marker) { return; } const latLng = coordinates && [coordinates[1], coordinates[0]]; if (reverseMarker) { if (!latLng) { reverseMarker.remove(); reverseMarker = undefined; } else { reverseMarker.setLatLng(latLng); } } else if (latLng) { if (marker instanceof Function) { reverseMarker = marker(map) ?? undefined; } else { reverseMarker = (typeof marker === "object" ? new L.Marker(latLng, marker) : createMarker(latLng)).addTo(map); reverseMarker.getElement()?.classList.add("marker-reverse"); } } }, setFeatures(markedFeatures, picked, showPolygonMarker) { function setData(data) { resultLayer.clearLayers(); if (data) { resultLayer.addData(data); } } for (const marker of markers) { marker.remove(); } markers.length = 0; setData(); block: if (picked) { let handled = false; if (picked.geometry.type === "GeometryCollection") { const geoms = picked.geometry.geometries.filter((geometry) => geometry.type === "Polygon" || geometry.type === "MultiPolygon"); ok: if (geoms.length > 0) { const unioned = union(featureCollection(geoms.map((geom) => feature(geom)))); if (!unioned) { break ok; } setMask({ ...picked, geometry: unioned.geometry, }, setData); handled = true; } else { const geometries = picked.geometry.geometries.filter((geometry) => geometry.type === "LineString" || geometry.type === "MultiLineString"); if (geometries.length > 0) { setData({ ...picked, geometry: { type: "GeometryCollection", geometries }, }); handled = true; } } } if (handled) { // nothing } else if (picked.geometry.type === "Polygon" || picked.geometry.type === "MultiPolygon") { setMask(picked, (fc) => { if (!fc) { return; } // leaflet doesn't repeat features every 360 degrees along longitude // so we clone it manually to the direction(s) // which could be displayed when auto-zoomed on the feature const features = [...fc.features]; const bb = unwrapBbox(bbox(picked)); const span = bb[2] - bb[0]; if (bb[0] - span / 4 < -180) { features.push(...shiftPolyCollection(fc, -360).features); } if (bb[2] + span / 4 > 180) { features.push(...shiftPolyCollection(fc, 360).features); } setData(featureCollection(features)); }); } else if (picked.geometry.type === "LineString" || picked.geometry.type === "MultiLineString") { setData(picked); break block; // no pin for (multi)linestrings } if (!showPolygonMarker && !picked.geometry.type.endsWith("Point")) { break block; } if (marker instanceof Function) { const m = marker(map, picked); if (m) { markers.push(m.addTo(map)); } } else if (marker) { const pos = [picked.center[1], picked.center[0]]; markers.push(typeof marker === "object" ? new L.Marker(pos, marker) : createMarker(pos).addTo(map)); } } if (showResultMarkers) { for (const feature of markedFeatures ?? []) { if (feature === picked) { continue; } const pos = [ feature.center[1], feature.center[0], ]; let marker; if (showResultMarkers instanceof Function) { marker = showResultMarkers(map, feature); if (!marker) { continue; } } else { marker = (typeof showResultMarkers === "object" ? new L.Marker(pos, showResultMarkers) : createMarker(pos, true)) .addTo(map) .bindTooltip(feature.place_type[0] === "reverse" ? feature.place_name : feature.place_name.replace(/,.*/, ""), { direction: "top", }); } const element = marker.getElement(); if (element) { element.addEventListener("click", (e) => { e.stopPropagation(); eventHandler?.({ type: "markerClick", id: feature.id }); }); element.addEventListener("mouseenter", () => { eventHandler?.({ type: "markerMouseEnter", id: feature.id }); }); element.addEventListener("mouseleave", () => { eventHandler?.({ type: "markerMouseLeave", id: feature.id }); }); element.classList.toggle("marker-fuzzy", !!feature.matching_text); } markers.push(marker); } } }, setSelectedMarker(index) { if (selectedMarker) { selectedMarker.getElement()?.classList.toggle("marker-selected", false); } selectedMarker = index > -1 ? markers[index] : undefined; selectedMarker?.getElement()?.classList.toggle("marker-selected", true); }, getCenterAndZoom() { const c = map.getCenter(); return [map.getZoom(), c.lng, c.lat]; }, }; } function shiftPolyCollection(featureCollection, distance) { const cloned = clone(featureCollection); for (const feature of cloned.features) { if (feature.geometry.type == "MultiPolygon") { for (const poly of feature.geometry.coordinates) { shiftPolyCoords(poly, distance); } } else { shiftPolyCoords(feature.geometry.coordinates, distance); } } return cloned; } function shiftPolyCoords(coordinates, distance) { for (const ring of coordinates) { for (const position of ring) { position[0] += distance; } } }