2025-08-04 17:45:44 +02:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import {
|
2025-08-28 18:27:07 +02:00
|
|
|
|
Circle,
|
|
|
|
|
|
MapContainer,
|
|
|
|
|
|
Marker,
|
2026-02-09 18:55:06 +01:00
|
|
|
|
Polyline,
|
2025-08-28 18:27:07 +02:00
|
|
|
|
Popup,
|
|
|
|
|
|
TileLayer,
|
|
|
|
|
|
useMapEvents,
|
2025-08-04 17:45:44 +02:00
|
|
|
|
} from "react-leaflet";
|
|
|
|
|
|
|
|
|
|
|
|
import "leaflet/dist/leaflet.css";
|
|
|
|
|
|
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
|
|
|
|
|
|
import "leaflet-defaulticon-compatibility";
|
|
|
|
|
|
import { Home } from "lucide-react";
|
2026-02-09 18:55:06 +01:00
|
|
|
|
import { useState } from "react";
|
2025-08-28 18:27:07 +02:00
|
|
|
|
import { Marker as CustomMarker } from "~/components/map/custom_marker";
|
|
|
|
|
|
import MapMarker from "~/components/map/map_marker";
|
|
|
|
|
|
import { cn } from "~/lib/utils";
|
2025-08-04 17:45:44 +02:00
|
|
|
|
|
|
|
|
|
|
type LatLon = {
|
2025-08-28 18:27:07 +02:00
|
|
|
|
lat: number;
|
|
|
|
|
|
lng: number;
|
|
|
|
|
|
alt?: number;
|
2025-08-04 17:45:44 +02:00
|
|
|
|
};
|
|
|
|
|
|
type posData = {
|
2025-08-28 18:27:07 +02:00
|
|
|
|
pos: LatLon;
|
|
|
|
|
|
popupTxt?: string;
|
|
|
|
|
|
onClick?: () => void;
|
|
|
|
|
|
markerTxt?: string;
|
2025-08-04 17:45:44 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
type GroupProps = {
|
2025-08-28 18:27:07 +02:00
|
|
|
|
isGroup: true;
|
|
|
|
|
|
groupData: posData[];
|
2025-08-04 17:45:44 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
type SingleProps = {
|
2025-08-28 18:27:07 +02:00
|
|
|
|
isGroup?: false;
|
|
|
|
|
|
groupData?: never;
|
2025-08-04 17:45:44 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-09 18:55:06 +01:00
|
|
|
|
type MarkerType = "precise" | "circle" | "price" | "home";
|
|
|
|
|
|
|
2025-08-04 17:45:44 +02:00
|
|
|
|
type MapProps = {
|
2025-08-28 18:27:07 +02:00
|
|
|
|
posix: posData;
|
|
|
|
|
|
autocenter?: boolean;
|
|
|
|
|
|
zoom?: number;
|
2026-02-09 18:55:06 +01:00
|
|
|
|
markerType: MarkerType;
|
2025-08-28 18:27:07 +02:00
|
|
|
|
markerRadius?: number;
|
|
|
|
|
|
scrollWheelZoom?: boolean;
|
|
|
|
|
|
width?: string;
|
|
|
|
|
|
height?: string;
|
|
|
|
|
|
onMapClick?: () => void;
|
2026-02-09 18:55:06 +01:00
|
|
|
|
isMeasuring?: boolean;
|
2025-08-04 17:45:44 +02:00
|
|
|
|
} & (GroupProps | SingleProps);
|
|
|
|
|
|
|
|
|
|
|
|
const defaults = {
|
2025-08-28 18:27:07 +02:00
|
|
|
|
height: "500px",
|
2025-08-29 16:18:32 +02:00
|
|
|
|
width: "500px",
|
|
|
|
|
|
zoom: 15,
|
2025-08-04 17:45:44 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const MedianPoint = (pos: posData[]) => {
|
2025-08-28 18:27:07 +02:00
|
|
|
|
const lat = pos.map((p) => p.pos.lat).reduce((a, b) => a + b) / pos.length;
|
|
|
|
|
|
const lng = pos.map((p) => p.pos.lng).reduce((a, b) => a + b) / pos.length;
|
|
|
|
|
|
return { lat, lng };
|
2025-08-04 17:45:44 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-09 18:55:06 +01:00
|
|
|
|
const MarkerComp = ({
|
|
|
|
|
|
pos,
|
|
|
|
|
|
markerType,
|
|
|
|
|
|
markerRadius,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
pos: posData;
|
|
|
|
|
|
markerType: MarkerType;
|
|
|
|
|
|
markerRadius?: number;
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
switch (markerType) {
|
|
|
|
|
|
case "price":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<CustomMarker
|
|
|
|
|
|
eventHandlers={pos.onClick && { click: pos.onClick }}
|
|
|
|
|
|
icon={
|
|
|
|
|
|
<MapMarker
|
|
|
|
|
|
color="primary"
|
|
|
|
|
|
label={
|
|
|
|
|
|
<span className="inline-block whitespace-nowrap">
|
|
|
|
|
|
{pos.markerTxt}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
/>
|
|
|
|
|
|
}
|
|
|
|
|
|
position={pos.pos}
|
|
|
|
|
|
title={pos.markerTxt}
|
|
|
|
|
|
>
|
|
|
|
|
|
{pos.popupTxt && <Popup>{pos.popupTxt}</Popup>}
|
|
|
|
|
|
</CustomMarker>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
case "precise":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Marker
|
|
|
|
|
|
eventHandlers={pos.onClick && { click: pos.onClick }}
|
|
|
|
|
|
position={pos.pos}
|
|
|
|
|
|
title={pos.markerTxt}
|
|
|
|
|
|
>
|
|
|
|
|
|
{pos.popupTxt && <Popup>{pos.popupTxt}</Popup>}
|
|
|
|
|
|
</Marker>
|
|
|
|
|
|
);
|
|
|
|
|
|
case "circle":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Circle
|
|
|
|
|
|
center={pos.pos}
|
|
|
|
|
|
eventHandlers={pos.onClick && { click: pos.onClick }}
|
|
|
|
|
|
pathOptions={{ color: "red" }}
|
|
|
|
|
|
radius={markerRadius || 100}
|
|
|
|
|
|
>
|
|
|
|
|
|
{pos.popupTxt && <Popup>{pos.popupTxt}</Popup>}
|
|
|
|
|
|
</Circle>
|
|
|
|
|
|
);
|
|
|
|
|
|
case "home":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Circle
|
|
|
|
|
|
center={pos.pos}
|
|
|
|
|
|
eventHandlers={pos.onClick && { click: pos.onClick }}
|
|
|
|
|
|
fillOpacity={1}
|
|
|
|
|
|
pathOptions={{ color: "#ec4899" }}
|
|
|
|
|
|
radius={markerRadius || 100}
|
|
|
|
|
|
>
|
|
|
|
|
|
{pos.popupTxt && <Popup>{pos.popupTxt}</Popup>}
|
|
|
|
|
|
</Circle>
|
|
|
|
|
|
<CustomMarker
|
|
|
|
|
|
eventHandlers={pos.onClick && { click: pos.onClick }}
|
|
|
|
|
|
icon={
|
|
|
|
|
|
<div className="relative flex size-10 items-center justify-center rounded-full bg-pink-500 p-2">
|
|
|
|
|
|
<Home className="absolute size-3/4 stroke-white" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
position={pos.pos}
|
|
|
|
|
|
title={pos.markerTxt}
|
|
|
|
|
|
>
|
|
|
|
|
|
{pos.popupTxt && <Popup>{pos.popupTxt}</Popup>}
|
|
|
|
|
|
</CustomMarker>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
default:
|
|
|
|
|
|
throw new Error(`Invalid marker type: ${markerType satisfies never}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function getDistanceInKm(pos1: LatLon, pos2: LatLon) {
|
|
|
|
|
|
const R = 6371; // Radius of the earth in km
|
|
|
|
|
|
const dLat = deg2rad(pos2.lat - pos1.lat);
|
|
|
|
|
|
const dLon = deg2rad(pos2.lng - pos1.lng);
|
|
|
|
|
|
|
|
|
|
|
|
const a =
|
|
|
|
|
|
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
|
|
|
|
Math.cos(deg2rad(pos1.lat)) *
|
|
|
|
|
|
Math.cos(deg2rad(pos2.lat)) *
|
|
|
|
|
|
Math.sin(dLon / 2) *
|
|
|
|
|
|
Math.sin(dLon / 2);
|
|
|
|
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
|
|
|
|
const distance = R * c; // Distance in km
|
|
|
|
|
|
return distance;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function deg2rad(deg: number) {
|
|
|
|
|
|
return deg * (Math.PI / 180);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const DistanceMarker = ({ pos, posix }: { pos: LatLon; posix: posData }) => {
|
|
|
|
|
|
const distanceKm = getDistanceInKm(pos, posix.pos);
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<CustomMarker
|
|
|
|
|
|
icon={
|
|
|
|
|
|
<MapMarker
|
|
|
|
|
|
color="orange"
|
|
|
|
|
|
label={
|
|
|
|
|
|
<span className="inline-block whitespace-nowrap">
|
|
|
|
|
|
~{distanceKm.toFixed(2)} km
|
|
|
|
|
|
</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
/>
|
|
|
|
|
|
}
|
|
|
|
|
|
position={pos}
|
|
|
|
|
|
title={"Distanza"}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<Polyline
|
|
|
|
|
|
pathOptions={{
|
|
|
|
|
|
color: "#2563eb", // Primary Blue (Tailwind blue-600)
|
|
|
|
|
|
weight: 4,
|
|
|
|
|
|
dashArray: "15, 10", // Creates the "Ruler" dashed effect (15px line, 10px gap)
|
|
|
|
|
|
opacity: 0.7,
|
|
|
|
|
|
lineCap: "round", // Makes the ends of the dashes rounded
|
|
|
|
|
|
}}
|
|
|
|
|
|
positions={[posix.pos, pos]}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
|
const MapComp = ({
|
|
|
|
|
|
posix,
|
|
|
|
|
|
autocenter,
|
|
|
|
|
|
zoom = defaults.zoom,
|
|
|
|
|
|
markerType,
|
|
|
|
|
|
markerRadius,
|
|
|
|
|
|
scrollWheelZoom = false,
|
|
|
|
|
|
width = defaults.width,
|
|
|
|
|
|
height = defaults.height,
|
|
|
|
|
|
isGroup,
|
|
|
|
|
|
groupData,
|
|
|
|
|
|
onMapClick,
|
2026-02-09 18:55:06 +01:00
|
|
|
|
isMeasuring,
|
2025-08-04 17:45:44 +02:00
|
|
|
|
}: MapProps) => {
|
2026-02-09 18:55:06 +01:00
|
|
|
|
const [measuringPos, setMeasuringPos] = useState<LatLon | null>(null);
|
2025-08-28 18:27:07 +02:00
|
|
|
|
const APIs = () => {
|
|
|
|
|
|
useMapEvents({
|
2026-02-09 18:55:06 +01:00
|
|
|
|
click: (e) => {
|
|
|
|
|
|
if (isMeasuring) {
|
|
|
|
|
|
setMeasuringPos(e.latlng);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-08-28 18:27:07 +02:00
|
|
|
|
if (onMapClick) {
|
|
|
|
|
|
onMapClick();
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
return null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const hashKey = JSON.stringify(
|
|
|
|
|
|
groupData ? groupData.map((p) => p.pos) : posix.pos,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<MapContainer
|
|
|
|
|
|
center={
|
|
|
|
|
|
autocenter ? MedianPoint(isGroup ? groupData : [posix]) : posix.pos
|
|
|
|
|
|
}
|
2025-08-29 16:18:32 +02:00
|
|
|
|
className={cn("")}
|
|
|
|
|
|
key={hashKey}
|
2025-08-28 18:27:07 +02:00
|
|
|
|
scrollWheelZoom={scrollWheelZoom}
|
|
|
|
|
|
style={{
|
2025-08-29 16:18:32 +02:00
|
|
|
|
borderRadius: "8px",
|
2025-08-28 18:27:07 +02:00
|
|
|
|
height: height,
|
|
|
|
|
|
width: width,
|
|
|
|
|
|
}}
|
2025-08-29 16:18:32 +02:00
|
|
|
|
zoom={zoom}
|
2025-08-28 18:27:07 +02:00
|
|
|
|
>
|
|
|
|
|
|
<APIs />
|
|
|
|
|
|
<TileLayer
|
|
|
|
|
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
2025-08-29 16:18:32 +02:00
|
|
|
|
//url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
|
|
|
|
url="/api/tiles/{s}/{z}/{x}/{y}"
|
2025-08-28 18:27:07 +02:00
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{isGroup ? (
|
2026-02-09 18:55:06 +01:00
|
|
|
|
groupData.map((pos, i) => (
|
|
|
|
|
|
<MarkerComp
|
|
|
|
|
|
// biome-ignore lint/suspicious/noArrayIndexKey: <index is acceptable here>
|
|
|
|
|
|
key={i}
|
|
|
|
|
|
markerRadius={markerRadius}
|
|
|
|
|
|
markerType={markerType}
|
|
|
|
|
|
pos={pos}
|
|
|
|
|
|
/>
|
|
|
|
|
|
))
|
2025-08-28 18:27:07 +02:00
|
|
|
|
) : (
|
2026-02-09 18:55:06 +01:00
|
|
|
|
<MarkerComp
|
|
|
|
|
|
markerRadius={markerRadius}
|
|
|
|
|
|
markerType={markerType}
|
|
|
|
|
|
pos={posix}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{isMeasuring && measuringPos && (
|
|
|
|
|
|
<DistanceMarker pos={measuringPos} posix={posix} />
|
2025-08-28 18:27:07 +02:00
|
|
|
|
)}
|
|
|
|
|
|
</MapContainer>
|
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
|
export default MapComp;
|
2025-08-04 17:45:44 +02:00
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
const PuntiInteresse: {
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
lat: number;
|
|
|
|
|
|
lng: number;
|
|
|
|
|
|
icon: IconType;
|
|
|
|
|
|
}[] = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "PIAZZA LIBERTA’",
|
|
|
|
|
|
lat: 45.766706,
|
|
|
|
|
|
lng: 11.733919,
|
|
|
|
|
|
icon: "milestone",
|
|
|
|
|
|
},
|
|
|
|
|
|
{ title: "PONTE VECCHIO", lat: 45.76752, lng: 11.731148, icon: "milestone" },
|
|
|
|
|
|
{ title: "INFOALLOGGI", lat: 45.764886, lng: 11.736019, icon: "info" },
|
|
|
|
|
|
{ title: "PRATO 1", lat: 45.770066, lng: 11.736306, icon: "parking" },
|
|
|
|
|
|
{ title: "PRATO 2", lat: 45.771495, lng: 11.734107, icon: "parking" },
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "STAZIONE FERROVIARIA/BUS",
|
|
|
|
|
|
lat: 45.766709,
|
|
|
|
|
|
lng: 11.741204,
|
|
|
|
|
|
icon: "milestone",
|
|
|
|
|
|
},
|
|
|
|
|
|
{ title: "GIARDINI PAROLINI", lat: 45.764804, lng: 11.738355, icon: "park" },
|
|
|
|
|
|
{ title: "BIBLIOTECA", lat: 45.765801, lng: 11.735987, icon: "school" },
|
|
|
|
|
|
{ title: "CENTRO STUDIO", lat: 45.754266, lng: 11.733066, icon: "school" },
|
|
|
|
|
|
{ title: "OSPEDALE", lat: 45.747444, lng: 11.744371, icon: "cross" },
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "PALARENA CMP",
|
|
|
|
|
|
lat: 45.747374,
|
|
|
|
|
|
lng: 11.740302,
|
|
|
|
|
|
icon: "entertainment",
|
|
|
|
|
|
},
|
|
|
|
|
|
{ title: "PONTE NUOVO", lat: 45.764594, lng: 11.729995, icon: "milestone" },
|
|
|
|
|
|
{ title: "OSSARIO ", lat: 45.763568, lng: 11.733449, icon: "milestone" },
|
|
|
|
|
|
{ title: "CINEMA METROPOLIS", lat: 45.742997, lng: 11.735622, icon: "film" },
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "CENTRO COMMERCIALE GRIFONE",
|
|
|
|
|
|
lat: 45.744988,
|
|
|
|
|
|
lng: 11.751847,
|
|
|
|
|
|
icon: "cart",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "CENTRO COMMERCIALE EMISFERO",
|
|
|
|
|
|
lat: 45.742368,
|
|
|
|
|
|
lng: 11.728874,
|
|
|
|
|
|
icon: "cart",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "SUPERMERCATO Alì s. vito",
|
|
|
|
|
|
lat: 45.775167,
|
|
|
|
|
|
lng: 11.750929,
|
|
|
|
|
|
icon: "cart",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "SUPERMERCATO Alì cà baroncello ",
|
|
|
|
|
|
lat: 45.758659,
|
|
|
|
|
|
lng: 11.751037,
|
|
|
|
|
|
icon: "cart",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "CONAD via passalacqua",
|
|
|
|
|
|
lat: 45.771366,
|
|
|
|
|
|
lng: 11.742604,
|
|
|
|
|
|
icon: "cart",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "CONAD VIALE PECORI GIRARDI",
|
|
|
|
|
|
lat: 45.764649,
|
|
|
|
|
|
lng: 11.720416,
|
|
|
|
|
|
icon: "cart",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "BIOSAPORI VIALE DE GASPERI",
|
|
|
|
|
|
lat: 45.761377,
|
|
|
|
|
|
lng: 11.732186,
|
|
|
|
|
|
icon: "cart",
|
|
|
|
|
|
},
|
|
|
|
|
|
{ title: "SUPERMERCATO ALDI ", lat: 45.743694, lng: 11.755435, icon: "cart" },
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "SUPERMERCATO TOSANO",
|
|
|
|
|
|
lat: 45.764982,
|
|
|
|
|
|
lng: 11.766572,
|
|
|
|
|
|
icon: "cart",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "POSTE CENTRALI BASSANO",
|
|
|
|
|
|
lat: 45.762758,
|
|
|
|
|
|
lng: 11.735091,
|
|
|
|
|
|
icon: "mail",
|
|
|
|
|
|
},
|
|
|
|
|
|
{ title: "POSTE VIA ANGARANO", lat: 45.767573, lng: 11.726599, icon: "mail" },
|
|
|
|
|
|
];
|
|
|
|
|
|
*/
|