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";
import { MapIcon } from "lucide-react";
import { MapIcon, Ruler } from "lucide-react";
import dynamic from "next/dynamic";
import { useState } from "react";
import { Button } from "~/components/ui/button";
@ -26,6 +26,7 @@ export const MappaDialogFullscreen = ({
}) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [isMeasuring, setIsMeasuring] = useState(false);
return (
<Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild>
@ -38,11 +39,20 @@ export const MappaDialogFullscreen = ({
<DialogTitle className="sr-only">{t.mappa}</DialogTitle>
<DialogDescription className="sr-only">{t.mappa}</DialogDescription>
</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">
{lat && long && (
<>
<MapComp
height="100%"
isMeasuring={isMeasuring}
markerType="home"
posix={{
popupTxt:

View file

@ -4,6 +4,7 @@ import {
Circle,
MapContainer,
Marker,
Polyline,
Popup,
TileLayer,
useMapEvents,
@ -13,6 +14,7 @@ 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";
@ -39,16 +41,19 @@ type SingleProps = {
groupData?: never;
};
type MarkerType = "precise" | "circle" | "price" | "home";
type MapProps = {
posix: posData;
autocenter?: boolean;
zoom?: number;
markerType: "precise" | "circle" | "price" | "home";
markerType: MarkerType;
markerRadius?: number;
scrollWheelZoom?: boolean;
width?: string;
height?: string;
onMapClick?: () => void;
isMeasuring?: boolean;
} & (GroupProps | SingleProps);
const defaults = {
@ -63,31 +68,15 @@ const MedianPoint = (pos: posData[]) => {
return { lat, lng };
};
const MapComp = ({
posix,
autocenter,
zoom = defaults.zoom,
const MarkerComp = ({
pos,
markerType,
markerRadius,
scrollWheelZoom = false,
width = defaults.width,
height = defaults.height,
isGroup,
groupData,
onMapClick,
}: MapProps) => {
const APIs = () => {
useMapEvents({
click: () => {
if (onMapClick) {
onMapClick();
}
},
});
return null;
};
const MarkerComp = ({ pos }: { pos: posData }) => {
}: {
pos: posData;
markerType: MarkerType;
markerRadius?: number;
}) => {
switch (markerType) {
case "price":
return (
@ -161,6 +150,90 @@ const MapComp = ({
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(
@ -190,10 +263,24 @@ const MapComp = ({
/>
{isGroup ? (
groupData.map((pos, i) => (
<MarkerComp
// 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>
);

View file

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