feat: add measuring functionality to Map component and enhance MapMarker styles

This commit is contained in:
Marco Pedone 2026-02-09 18:55:06 +01:00
parent 6a101f7b68
commit 7643e5a430
3 changed files with 237 additions and 138 deletions

View file

@ -1,5 +1,5 @@
"use client"; "use client";
import { MapIcon } from "lucide-react"; import { MapIcon, Ruler } from "lucide-react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useState } from "react"; import { useState } from "react";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
@ -26,6 +26,7 @@ export const MappaDialogFullscreen = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [isMeasuring, setIsMeasuring] = useState(false);
return ( return (
<Dialog onOpenChange={setOpen} open={open}> <Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild> <DialogTrigger asChild>
@ -38,11 +39,20 @@ export const MappaDialogFullscreen = ({
<DialogTitle className="sr-only">{t.mappa}</DialogTitle> <DialogTitle className="sr-only">{t.mappa}</DialogTitle>
<DialogDescription className="sr-only">{t.mappa}</DialogDescription> <DialogDescription className="sr-only">{t.mappa}</DialogDescription>
</DialogHeader> </DialogHeader>
<Button
className="absolute bottom-2 left-2 z-10"
disabled={!lat || !long}
onClick={() => setIsMeasuring((m) => !m)}
variant="orange"
>
<Ruler /> {isMeasuring ? "Clicca sulla mappa" : "Misura"}
</Button>
<div className="size-full"> <div className="size-full">
{lat && long && ( {lat && long && (
<> <>
<MapComp <MapComp
height="100%" height="100%"
isMeasuring={isMeasuring}
markerType="home" markerType="home"
posix={{ posix={{
popupTxt: popupTxt:

View file

@ -4,6 +4,7 @@ import {
Circle, Circle,
MapContainer, MapContainer,
Marker, Marker,
Polyline,
Popup, Popup,
TileLayer, TileLayer,
useMapEvents, useMapEvents,
@ -13,6 +14,7 @@ import "leaflet/dist/leaflet.css";
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css"; import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
import "leaflet-defaulticon-compatibility"; import "leaflet-defaulticon-compatibility";
import { Home } from "lucide-react"; import { Home } from "lucide-react";
import { useState } from "react";
import { Marker as CustomMarker } from "~/components/map/custom_marker"; import { Marker as CustomMarker } from "~/components/map/custom_marker";
import MapMarker from "~/components/map/map_marker"; import MapMarker from "~/components/map/map_marker";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
@ -39,16 +41,19 @@ type SingleProps = {
groupData?: never; groupData?: never;
}; };
type MarkerType = "precise" | "circle" | "price" | "home";
type MapProps = { type MapProps = {
posix: posData; posix: posData;
autocenter?: boolean; autocenter?: boolean;
zoom?: number; zoom?: number;
markerType: "precise" | "circle" | "price" | "home"; markerType: MarkerType;
markerRadius?: number; markerRadius?: number;
scrollWheelZoom?: boolean; scrollWheelZoom?: boolean;
width?: string; width?: string;
height?: string; height?: string;
onMapClick?: () => void; onMapClick?: () => void;
isMeasuring?: boolean;
} & (GroupProps | SingleProps); } & (GroupProps | SingleProps);
const defaults = { const defaults = {
@ -63,31 +68,15 @@ const MedianPoint = (pos: posData[]) => {
return { lat, lng }; return { lat, lng };
}; };
const MapComp = ({ const MarkerComp = ({
posix, pos,
autocenter,
zoom = defaults.zoom,
markerType, markerType,
markerRadius, markerRadius,
scrollWheelZoom = false, }: {
width = defaults.width, pos: posData;
height = defaults.height, markerType: MarkerType;
isGroup, markerRadius?: number;
groupData, }) => {
onMapClick,
}: MapProps) => {
const APIs = () => {
useMapEvents({
click: () => {
if (onMapClick) {
onMapClick();
}
},
});
return null;
};
const MarkerComp = ({ pos }: { pos: posData }) => {
switch (markerType) { switch (markerType) {
case "price": case "price":
return ( return (
@ -163,6 +152,90 @@ const MapComp = ({
} }
}; };
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( const hashKey = JSON.stringify(
groupData ? groupData.map((p) => p.pos) : posix.pos, groupData ? groupData.map((p) => p.pos) : posix.pos,
); );
@ -190,10 +263,24 @@ const MapComp = ({
/> />
{isGroup ? ( {isGroup ? (
groupData.map((pos, i) => (
<MarkerComp
// biome-ignore lint/suspicious/noArrayIndexKey: <index is acceptable here> // biome-ignore lint/suspicious/noArrayIndexKey: <index is acceptable here>
groupData.map((pos, i) => <MarkerComp key={i} pos={pos} />) key={i}
markerRadius={markerRadius}
markerType={markerType}
pos={pos}
/>
))
) : ( ) : (
<MarkerComp pos={posix} /> <MarkerComp
markerRadius={markerRadius}
markerType={markerType}
pos={posix}
/>
)}
{isMeasuring && measuringPos && (
<DistanceMarker pos={measuringPos} posix={posix} />
)} )}
</MapContainer> </MapContainer>
); );

View file

@ -4,17 +4,10 @@ import { cn } from "~/lib/utils";
interface MapMarkerProps { interface MapMarkerProps {
label: ReactNode; label: ReactNode;
color?: "primary" | "red" | "blue" | "green" | "yellow" | "purple" | "gray"; color?: keyof typeof colorClasses;
className?: string; className?: string;
size?: "sm" | "md" | "lg"; size?: keyof typeof sizeClasses;
} }
export default function MapMarker({
label,
color = "primary",
className,
size = "md",
}: MapMarkerProps) {
const colorClasses = { const colorClasses = {
blue: { blue: {
rectangle: "bg-blue-500 text-white", rectangle: "bg-blue-500 text-white",
@ -44,33 +37,42 @@ export default function MapMarker({
rectangle: "bg-yellow-500 text-black", rectangle: "bg-yellow-500 text-black",
triangle: "border-t-yellow-500", triangle: "border-t-yellow-500",
}, },
}; orange: {
rectangle: "bg-orange-500 text-white",
triangle: "border-t-orange-500",
},
} as const;
const sizeClasses = { const sizeClasses = {
lg: { lg: {
container: "h-16", container: "h-16",
rectangle: "px-4 py-3 rounded-xl", rectangle: "px-4 py-3 rounded-xl",
text: "text-base", text: "text-base",
triangle: "border-l-[12px] border-r-[12px] border-t-[12px]", triangle: "border-l-[12px] border-r-[12px] border-t-[20px]",
}, },
md: { md: {
container: "h-12", container: "h-12",
rectangle: "px-3 py-2 rounded-lg", rectangle: "px-3 py-2 rounded-lg",
text: "text-sm", text: "text-sm",
triangle: "border-l-[10px] border-r-[10px] border-t-[10px]", triangle: "border-l-[10px] border-r-[10px] border-t-[16px]",
}, },
sm: { sm: {
container: "h-8", container: "h-8",
rectangle: "px-2 py-1 rounded-md", rectangle: "px-2 py-1 rounded-md",
text: "text-xs", text: "text-xs",
triangle: "border-l-[8px] border-r-[8px] border-t-[8px]", triangle: "border-l-[8px] border-r-[8px] border-t-[15px]",
}, },
}; } as const;
export default function MapMarker({
label,
color = "primary",
className,
size = "md",
}: MapMarkerProps) {
return ( return (
<div <div
className={cn( className={cn(
"inline-flex flex-col items-center", "inline-flex -translate-y-[60%] flex-col items-center",
sizeClasses[size].container, sizeClasses[size].container,
className, className,
)} )}
@ -89,7 +91,7 @@ export default function MapMarker({
{/* Triangle part */} {/* Triangle part */}
<div <div
className={cn( className={cn(
"h-0 w-0 border-transparent border-solid", "-mt-px h-0 w-0 border-transparent border-solid",
sizeClasses[size].triangle, sizeClasses[size].triangle,
colorClasses[color].triangle, colorClasses[color].triangle,
)} )}