infoalloggi-monorepo/apps/infoalloggi/src/components/map/Map.tsx

384 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import {
Circle,
MapContainer,
Marker,
Polyline,
Popup,
TileLayer,
useMapEvents,
} 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";
import { useState } from "react";
import { Marker as CustomMarker } from "~/components/map/custom_marker";
import MapMarker from "~/components/map/map_marker";
import { cn } from "~/lib/utils";
type LatLon = {
lat: number;
lng: number;
alt?: number;
};
type posData = {
pos: LatLon;
popupTxt?: string;
onClick?: () => void;
markerTxt?: string;
};
type GroupProps = {
isGroup: true;
groupData: posData[];
};
type SingleProps = {
isGroup?: false;
groupData?: never;
};
type MarkerType = "precise" | "circle" | "price" | "home";
type MapProps = {
posix: posData;
autocenter?: boolean;
zoom?: number;
markerType: MarkerType;
markerRadius?: number;
scrollWheelZoom?: boolean;
width?: string;
height?: string;
onMapClick?: () => void;
isMeasuring?: boolean;
} & (GroupProps | SingleProps);
const defaults = {
height: "500px",
width: "500px",
zoom: 15,
};
const MedianPoint = (pos: posData[]) => {
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 };
};
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]}
/>
</>
);
};
const MapComp = ({
posix,
autocenter,
zoom = defaults.zoom,
markerType,
markerRadius,
scrollWheelZoom = false,
width = defaults.width,
height = defaults.height,
isGroup,
groupData,
onMapClick,
isMeasuring,
}: MapProps) => {
const [measuringPos, setMeasuringPos] = useState<LatLon | null>(null);
const APIs = () => {
useMapEvents({
click: (e) => {
if (isMeasuring) {
setMeasuringPos(e.latlng);
return;
}
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
}
className={cn("")}
key={hashKey}
scrollWheelZoom={scrollWheelZoom}
style={{
borderRadius: "8px",
height: height,
width: width,
}}
zoom={zoom}
>
<APIs />
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
//url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
url="/api/tiles/{s}/{z}/{x}/{y}"
/>
{isGroup ? (
groupData.map((pos, i) => (
<MarkerComp
// biome-ignore lint/suspicious/noArrayIndexKey: <index is acceptable here>
key={i}
markerRadius={markerRadius}
markerType={markerType}
pos={pos}
/>
))
) : (
<MarkerComp
markerRadius={markerRadius}
markerType={markerType}
pos={posix}
/>
)}
{isMeasuring && measuringPos && (
<DistanceMarker pos={measuringPos} posix={posix} />
)}
</MapContainer>
);
};
export default MapComp;
/*
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" },
];
*/