feat(address-map): MapLibre GL + Map.ir vector map & Persian reverse geocoding
Replace the broken-looking Leaflet/OSM address picker with a MapLibre GL map using Map.ir vector tiles, plus Map.ir reverse geocoding for fast coordinates -> Persian address. - InteractiveMap: drop react-leaflet/leaflet; init a MapLibre map (Map.ir style via transformRequest x-api-key), a NavigationControl, and a fixed center pin — the map moves under the pin and the centre is reverse- geocoded on moveend (debounced). Wrapped dir=ltr so the map renders correctly inside the RTL page (fixes the scattered-tiles bug). Selected address shown in an overlay chip; city select / geolocation fly the map. - reverse geocoding: Nominatim -> Map.ir (https://map.ir/reverse), which returns proper Persian addresses (Nominatim was weak for Iran and its TOS forbids production use). - default map centre fixed to Tehran (matched the "تهران" selector). - key via VITE_MAPIR_KEY (documented in .env.example; real key in env). - deps: add maplibre-gl, remove leaflet/react-leaflet/@types/leaflet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
668649d540
commit
3c7ee5b24c
@ -21,3 +21,6 @@ VITE_BUILD_TARGET=development
|
||||
|
||||
# Support Configuration
|
||||
VITE_SUPPORT_PHONE=+989223740993
|
||||
|
||||
# Map.ir key for the address map (MapLibre vector tiles + reverse geocoding)
|
||||
VITE_MAPIR_KEY=your_mapir_api_key
|
||||
|
||||
@ -1,15 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import {
|
||||
MapContainer,
|
||||
TileLayer,
|
||||
Marker,
|
||||
Popup,
|
||||
useMapEvents,
|
||||
useMap,
|
||||
} from "react-leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import L from "leaflet";
|
||||
import { Search, X, ChevronLeft } from "lucide-react";
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
import type { Map as MaplibreMap } from "maplibre-gl";
|
||||
import { Search, X, ChevronLeft, MapPin } from "lucide-react";
|
||||
import AppInput from "./AppInput";
|
||||
import AppTextArea from "./AppTextArea";
|
||||
import {
|
||||
@ -32,6 +24,25 @@ import {
|
||||
type IranianCity,
|
||||
} from "../data/iranian-cities";
|
||||
|
||||
// Map.ir config (vector tiles + reverse geocoding). The key is a client-side
|
||||
// map key (sent with every tile request), provided via env.
|
||||
const MAPIR_KEY = import.meta.env.VITE_MAPIR_KEY || "";
|
||||
const MAPIR_STYLE = "https://map.ir/vector/styles/main/mapir-style.json";
|
||||
|
||||
// Reverse geocode (lat/lng -> Persian address) via Map.ir.
|
||||
async function reverseGeocode(lat: number, lon: number): Promise<string> {
|
||||
try {
|
||||
const res = await fetch(`https://map.ir/reverse?lat=${lat}&lon=${lon}`, {
|
||||
headers: { "x-api-key": MAPIR_KEY },
|
||||
});
|
||||
const data = await res.json();
|
||||
return data?.address || data?.address_compact || "آدرس نامشخص";
|
||||
} catch (error) {
|
||||
console.error("Error fetching address:", error);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Form data interface
|
||||
interface FormData {
|
||||
title: string;
|
||||
@ -71,30 +82,6 @@ function convertPersianToEnglish(str: string): string {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Component to change map view when city is selected
|
||||
function CityChangeView({ center }: { center: [number, number] }) {
|
||||
const map = useMap();
|
||||
useEffect(() => {
|
||||
map.setView(center, 13);
|
||||
}, [center, map]);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Component to handle map clicks
|
||||
function MapClickHandler({
|
||||
onMapClick,
|
||||
}: {
|
||||
onMapClick: (latlng: [number, number]) => void;
|
||||
}) {
|
||||
useMapEvents({
|
||||
click: (e) => {
|
||||
const { lat, lng } = e.latlng;
|
||||
onMapClick([lat, lng]);
|
||||
},
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function InteractiveMap({
|
||||
onLocationSelected,
|
||||
initialAddress,
|
||||
@ -112,7 +99,7 @@ export default function InteractiveMap({
|
||||
isEditing ? "" : "تهران"
|
||||
);
|
||||
const [currentCenter, setCurrentCenter] = useState<[number, number]>(
|
||||
iranianCities[0].position
|
||||
(iranianCities.find((c) => c.name === "تهران") || iranianCities[0]).position
|
||||
);
|
||||
const [mounted, setMounted] = useState<boolean>(false);
|
||||
|
||||
@ -166,34 +153,10 @@ export default function InteractiveMap({
|
||||
isDefault: formData.is_default,
|
||||
});
|
||||
|
||||
// Fix Leaflet's default icon path issues when the component mounts
|
||||
useEffect(() => {
|
||||
// Fix Leaflet icon issues
|
||||
delete (L.Icon.Default.prototype as { _getIconUrl?: string })._getIconUrl;
|
||||
L.Icon.Default.mergeOptions({
|
||||
iconUrl: "https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png",
|
||||
iconRetinaUrl:
|
||||
"https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon-2x.png",
|
||||
shadowUrl:
|
||||
"https://unpkg.com/leaflet@1.7.1/dist/images/marker-shadow.png",
|
||||
});
|
||||
|
||||
// Cleanup function to remove map instance when component unmounts
|
||||
return () => {
|
||||
// This code ensures previous maps are cleaned up
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const mapContainer = document.getElementById(mapIdRef.current);
|
||||
if (mapContainer) {
|
||||
// Clean up previous map on this element
|
||||
// In Leaflet version 1+, using map._leaflet no longer works
|
||||
// and instead removeAttribute should be used
|
||||
mapContainer.innerHTML = "";
|
||||
mapContainer.removeAttribute("data-leaflet-internal-id");
|
||||
mapContainer.removeAttribute("_leaflet_id");
|
||||
console.log("Map container cleaned up successfully");
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
// MapLibre refs
|
||||
const mapContainerRef = useRef<HTMLDivElement>(null);
|
||||
const mapRef = useRef<MaplibreMap | null>(null);
|
||||
const moveDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// Initialize with the existing address data when in editing mode
|
||||
useEffect(() => {
|
||||
@ -245,76 +208,94 @@ export default function InteractiveMap({
|
||||
setFilteredCities(cities);
|
||||
}, [citySearchTerm, selectedProvince]);
|
||||
|
||||
// Function to get address from coordinates
|
||||
const getAddressFromCoordinates = async (latlng: [number, number]) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latlng[0]}&lon=${latlng[1]}&zoom=18&addressdetails=1&accept-language=fa&countrycodes=ir`,
|
||||
{
|
||||
headers: {
|
||||
"Accept-Language": "fa",
|
||||
},
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
return data.display_name || "آدرس نامشخص";
|
||||
} catch (error) {
|
||||
console.error("Error fetching address:", error);
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
// Extract postal code from address data
|
||||
const getPostalCodeFromAddress = async (latlng: [number, number]) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latlng[0]}&lon=${latlng[1]}&zoom=18&addressdetails=1&accept-language=fa&countrycodes=ir`,
|
||||
{
|
||||
headers: {
|
||||
"Accept-Language": "fa",
|
||||
},
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (data && data.address && data.address.postcode) {
|
||||
return data.address.postcode;
|
||||
}
|
||||
return "";
|
||||
} catch (error) {
|
||||
console.error("Error fetching postal code:", error);
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const handleMapClick = async (latlng: [number, number]) => {
|
||||
const address = await getAddressFromCoordinates(latlng);
|
||||
const postalCode = await getPostalCodeFromAddress(latlng);
|
||||
|
||||
// Reverse-geocode a point and push it into the form. Used by the map's
|
||||
// center pin (on move) — replaces the old click handler.
|
||||
const applyLocation = useCallback(
|
||||
async (latlng: [number, number]) => {
|
||||
const address = await reverseGeocode(latlng[0], latlng[1]);
|
||||
setSelectedPosition(latlng);
|
||||
setSelectedAddress(address);
|
||||
|
||||
// Don't update postal_code in edit mode, preserve the original value
|
||||
setFormData({
|
||||
...formData,
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
main_address: address,
|
||||
// Don't set postal_code here in either mode, preserve the existing value in edit mode
|
||||
latitude: latlng[0],
|
||||
longitude: latlng[1],
|
||||
});
|
||||
|
||||
// If using as a location picker in another component, call the callback
|
||||
}));
|
||||
if (onLocationSelected) {
|
||||
onLocationSelected({
|
||||
position: latlng,
|
||||
address,
|
||||
postalCode: isEditing ? formData.postal_code : postalCode,
|
||||
onLocationSelected({ position: latlng, address });
|
||||
}
|
||||
},
|
||||
[onLocationSelected]
|
||||
);
|
||||
|
||||
// Keep a stable ref so the map's event listeners always call the latest.
|
||||
const applyLocationRef = useRef(applyLocation);
|
||||
useEffect(() => {
|
||||
applyLocationRef.current = applyLocation;
|
||||
}, [applyLocation]);
|
||||
|
||||
// Initialize the MapLibre map (client-only, once) with the Map.ir style.
|
||||
useEffect(() => {
|
||||
if (!mounted || formOnly || !mapContainerRef.current || mapRef.current) {
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const maplibregl = (await import("maplibre-gl")).default;
|
||||
if (cancelled || !mapContainerRef.current) return;
|
||||
const start = selectedPosition || currentCenter; // [lat, lng]
|
||||
const map = new maplibregl.Map({
|
||||
container: mapContainerRef.current,
|
||||
style: MAPIR_STYLE,
|
||||
center: [start[1], start[0]], // MapLibre uses [lng, lat]
|
||||
zoom: 14,
|
||||
attributionControl: false,
|
||||
transformRequest: (url: string) =>
|
||||
url.startsWith("https://map.ir")
|
||||
? { url, headers: { "x-api-key": MAPIR_KEY } }
|
||||
: { url },
|
||||
});
|
||||
mapRef.current = map;
|
||||
map.addControl(
|
||||
new maplibregl.NavigationControl({ showCompass: false }),
|
||||
"top-left"
|
||||
);
|
||||
// Reverse-geocode the centre whenever the map settles.
|
||||
map.on("moveend", () => {
|
||||
if (moveDebounceRef.current) clearTimeout(moveDebounceRef.current);
|
||||
moveDebounceRef.current = setTimeout(() => {
|
||||
const c = map.getCenter();
|
||||
applyLocationRef.current([c.lat, c.lng]);
|
||||
}, 350);
|
||||
});
|
||||
map.on("load", () => {
|
||||
const c = map.getCenter();
|
||||
applyLocationRef.current([c.lat, c.lng]);
|
||||
});
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (moveDebounceRef.current) clearTimeout(moveDebounceRef.current);
|
||||
if (mapRef.current) {
|
||||
mapRef.current.remove();
|
||||
mapRef.current = null;
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mounted, formOnly]);
|
||||
|
||||
// Recenter the map when the chosen city changes.
|
||||
useEffect(() => {
|
||||
if (mapRef.current) {
|
||||
mapRef.current.flyTo({
|
||||
center: [currentCenter[1], currentCenter[0]],
|
||||
zoom: 14,
|
||||
});
|
||||
}
|
||||
}, [currentCenter]);
|
||||
|
||||
const handleCitySelect = (cityName: string) => {
|
||||
setSelectedCity(cityName);
|
||||
@ -499,7 +480,11 @@ export default function InteractiveMap({
|
||||
const handleLocationFound = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<{ lat: number; lng: number }>;
|
||||
const { lat, lng } = customEvent.detail;
|
||||
handleMapClick([lat, lng]);
|
||||
if (mapRef.current) {
|
||||
mapRef.current.flyTo({ center: [lng, lat], zoom: 15 });
|
||||
} else {
|
||||
applyLocationRef.current([lat, lng]);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener(
|
||||
@ -832,44 +817,30 @@ export default function InteractiveMap({
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
{/* Map Container */}
|
||||
<div className="dd z-0 h-[500px] w-full relative">
|
||||
<div id={mapIdRef.current} className="h-full w-full">
|
||||
{mounted && (
|
||||
<MapContainer
|
||||
key={mapIdRef.current}
|
||||
center={currentCenter}
|
||||
zoom={13}
|
||||
scrollWheelZoom={true}
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
>
|
||||
<CityChangeView center={currentCenter} />
|
||||
<TileLayer
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
{/* Map Container — MapLibre + Map.ir vector tiles. dir=ltr keeps the
|
||||
map's internal positioning correct inside the RTL page. */}
|
||||
<div className="dd z-0 h-[500px] w-full relative" dir="ltr">
|
||||
<div
|
||||
ref={mapContainerRef}
|
||||
id={mapIdRef.current}
|
||||
className="h-full w-full"
|
||||
/>
|
||||
|
||||
<MapClickHandler onMapClick={handleMapClick} />
|
||||
{/* <LocationButton /> */}
|
||||
{/* Fixed center pin — the map moves under it; its tip marks the point */}
|
||||
<div className="pointer-events-none absolute left-1/2 top-1/2 z-[5] -translate-x-1/2 -translate-y-full drop-shadow-md">
|
||||
<MapPin size={40} className="fill-RED text-RED" strokeWidth={1.5} />
|
||||
</div>
|
||||
|
||||
{selectedPosition && (
|
||||
<Marker position={selectedPosition}>
|
||||
<Popup>
|
||||
<div dir="rtl" className="text-right">
|
||||
<p className="font-bold mb-1">آدرس:</p>
|
||||
<p className="mb-2">{selectedAddress}</p>
|
||||
<p className="mb-1">
|
||||
<span className="font-bold">مختصات: </span>
|
||||
{selectedPosition[0].toFixed(6)},{" "}
|
||||
{selectedPosition[1].toFixed(6)}
|
||||
</p>
|
||||
{/* Selected-address preview */}
|
||||
{selectedAddress && (
|
||||
<div
|
||||
dir="rtl"
|
||||
className="absolute left-3 right-3 bottom-3 z-[5] bg-WHITE/95 backdrop-blur rounded-xl shadow-md px-3.5 py-2.5"
|
||||
>
|
||||
<p className="text-R10 text-GRAY mb-0.5">موقعیت انتخابشده</p>
|
||||
<p className="text-R12 font-bold line-clamp-2">{selectedAddress}</p>
|
||||
</div>
|
||||
</Popup>
|
||||
</Marker>
|
||||
)}
|
||||
</MapContainer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Select Location Button */}
|
||||
|
||||
269
package-lock.json
generated
269
package-lock.json
generated
@ -35,13 +35,12 @@
|
||||
"input-otp": "^1.4.2",
|
||||
"isbot": "^4.1.0",
|
||||
"jalali-moment": "^3.3.11",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^0.477.0",
|
||||
"maplibre-gl": "^5.24.0",
|
||||
"moment-jalaali": "^0.10.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"remix-auth": "^3.6.0",
|
||||
"remix-auth-form": "^1.4.0",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
@ -52,7 +51,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^2.16.0",
|
||||
"@types/leaflet": "^1.9.17",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
@ -2002,6 +2000,119 @@
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@mapbox/jsonlint-lines-primitives": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.3.tgz",
|
||||
"integrity": "sha512-0SElaV0uMxEnxzBhhX9WTuPyUeMsAN/SS0i16tjuba4/mio63MG9khjC1a0JAiPGXAwvwm4UfHJURCN7nyudQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 22"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/point-geometry": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz",
|
||||
"integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@mapbox/tiny-sdf": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.2.0.tgz",
|
||||
"integrity": "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@mapbox/unitbezier": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
|
||||
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@mapbox/vector-tile": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.5.tgz",
|
||||
"integrity": "sha512-pXj8m7KTsqZt+1jsE0xIpGvqTSbblfkuEJL/NJmNePMtEwxO8V3XMDo9WMSfDeqHvCtBI9Lmt4mGcGR10zecmw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "~1.1.0",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"pbf": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/whoots-js": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
|
||||
"integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/geojson-vt": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.1.0.tgz",
|
||||
"integrity": "sha512-2eIY4gZxeKIVOZVNkAMb+5NgXhgsMQpOveTQAvnp53LYqHGJZDidk7Ew0Tged9PThidpbS+NFTh0g4zivhPDzQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"kdbush": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/maplibre-gl-style-spec": {
|
||||
"version": "24.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.10.0.tgz",
|
||||
"integrity": "sha512-lichxSiagMEBBrqHF0trtMQH9RKh+9jUlIJl0qW0QHvt2H/tbvUWdE+ZzI2Jd0/pT7j/iavLonlPu7EQ/ixTOw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
|
||||
"@mapbox/unitbezier": "^1.0.0",
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
"minimist": "^1.2.8",
|
||||
"quickselect": "^3.0.0",
|
||||
"tinyqueue": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"gl-style-format": "dist/gl-style-format.mjs",
|
||||
"gl-style-migrate": "dist/gl-style-migrate.mjs",
|
||||
"gl-style-validate": "dist/gl-style-validate.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/maplibre-gl-style-spec/node_modules/@mapbox/unitbezier": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-1.0.0.tgz",
|
||||
"integrity": "sha512-fqd515fjBmANKGGsQ286E2Wvj/XvDFpGzwJxq4CI6jMQue6Oy04uCKp+JWKF00xRTmk6cEu1jPJ9p3xqH8YWqQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@maplibre/mlt": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.12.tgz",
|
||||
"integrity": "sha512-ZeK5w2TTeHOajcLaEQs1KZXw2V9wIKo1PmThlxlsHoXsQsYlBqLJzPOd6tJHRtGTChUY3DPPmjXRArYVvAbmZw==",
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/vt-pbf": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.3.2.tgz",
|
||||
"integrity": "sha512-j6p0AdjvAR19Z3XaCysle7A4ZSo08tYOzxD0Y9NQylwPAkwJJeYub5b2eVucdeDh7erhv69DahoLOevDRERRUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "^1.1.0",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"pbf": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/vt-pbf/node_modules/pbf": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pbf/-/pbf-5.1.0.tgz",
|
||||
"integrity": "sha512-Wv0yo0+uZepnoNEKsquhar1F18LogB8oeEikIhUXG16udbiXG7JecHGySwoo6kuMgjmbQYzdrTZlO+/K9t8eZg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"resolve-protobuf-schema": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"pbf": "bin/pbf"
|
||||
}
|
||||
},
|
||||
"node_modules/@mdx-js/mdx": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-2.3.0.tgz",
|
||||
@ -3211,17 +3322,6 @@
|
||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@react-leaflet/core": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz",
|
||||
"integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==",
|
||||
"license": "Hippocratic-2.1",
|
||||
"peerDependencies": {
|
||||
"leaflet": "^1.9.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@remix-run/dev": {
|
||||
"version": "2.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/dev/-/dev-2.17.2.tgz",
|
||||
@ -3948,7 +4048,6 @@
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
@ -3975,16 +4074,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/leaflet": {
|
||||
"version": "1.9.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
|
||||
"integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdast": {
|
||||
"version": "3.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz",
|
||||
@ -6859,6 +6948,12 @@
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/earcut": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz",
|
||||
"integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
@ -8555,6 +8650,12 @@
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/gl-matrix": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
|
||||
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
|
||||
@ -9919,6 +10020,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stringify-pretty-compact": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
|
||||
"integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
@ -9961,6 +10068,12 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/kdbush": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz",
|
||||
"integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@ -10001,12 +10114,6 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/leaflet": {
|
||||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/levn": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
@ -10168,6 +10275,40 @@
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/maplibre-gl": {
|
||||
"version": "5.24.0",
|
||||
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.24.0.tgz",
|
||||
"integrity": "sha512-ALyFxgtd5R+65UqZ/++lOqwWcC0SNho9c27fYSyLmG7AfnAul2o46F05aDJGPbFU57wos9dgcIySHs0Xe6ia3A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
||||
"@mapbox/point-geometry": "^1.1.0",
|
||||
"@mapbox/tiny-sdf": "^2.1.0",
|
||||
"@mapbox/unitbezier": "^0.0.1",
|
||||
"@mapbox/vector-tile": "^2.0.4",
|
||||
"@mapbox/whoots-js": "^3.1.0",
|
||||
"@maplibre/geojson-vt": "^6.1.0",
|
||||
"@maplibre/maplibre-gl-style-spec": "^24.8.1",
|
||||
"@maplibre/mlt": "^1.1.8",
|
||||
"@maplibre/vt-pbf": "^4.3.0",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"earcut": "^3.0.2",
|
||||
"gl-matrix": "^3.4.4",
|
||||
"kdbush": "^4.0.2",
|
||||
"murmurhash-js": "^1.0.0",
|
||||
"pbf": "^4.0.1",
|
||||
"potpack": "^2.1.0",
|
||||
"quickselect": "^3.0.0",
|
||||
"tinyqueue": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14.0",
|
||||
"npm": ">=8.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-extensions": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz",
|
||||
@ -11171,7 +11312,6 @@
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@ -11500,6 +11640,12 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/murmurhash-js": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
|
||||
"integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mute-stream": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
|
||||
@ -12159,6 +12305,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pbf": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.2.tgz",
|
||||
"integrity": "sha512-J0ajxARhZfpUEebxYs1vhMGMuLSXtBe1e+fFPDrf2uA2hgo+UshKfNUWOz92HJNz6/NFEXseQPddnHkTreWRqg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"resolve-protobuf-schema": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"pbf": "bin/pbf"
|
||||
}
|
||||
},
|
||||
"node_modules/peek-stream": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz",
|
||||
@ -12519,6 +12677,12 @@
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/potpack": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz",
|
||||
"integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@ -12622,6 +12786,12 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/protocol-buffers-schema": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz",
|
||||
"integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@ -12720,6 +12890,12 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/quickselect": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
|
||||
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@ -12801,20 +12977,6 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-leaflet": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz",
|
||||
"integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==",
|
||||
"license": "Hippocratic-2.1",
|
||||
"dependencies": {
|
||||
"@react-leaflet/core": "^2.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"leaflet": "^1.9.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||
@ -13168,6 +13330,15 @@
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-protobuf-schema": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
|
||||
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"protocol-buffers-schema": "^3.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve.exports": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz",
|
||||
@ -14631,6 +14802,12 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyqueue": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
|
||||
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
|
||||
@ -44,13 +44,12 @@
|
||||
"input-otp": "^1.4.2",
|
||||
"isbot": "^4.1.0",
|
||||
"jalali-moment": "^3.3.11",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^0.477.0",
|
||||
"maplibre-gl": "^5.24.0",
|
||||
"moment-jalaali": "^0.10.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"remix-auth": "^3.6.0",
|
||||
"remix-auth-form": "^1.4.0",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
@ -61,7 +60,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^2.16.0",
|
||||
"@types/leaflet": "^1.9.17",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user