feat: add measuring functionality to Map component and enhance MapMarker styles
This commit is contained in:
parent
6a101f7b68
commit
7643e5a430
3 changed files with 237 additions and 138 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,6 +68,144 @@ const MedianPoint = (pos: posData[]) => {
|
|||
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,
|
||||
|
|
@ -75,10 +218,16 @@ const MapComp = ({
|
|||
isGroup,
|
||||
groupData,
|
||||
onMapClick,
|
||||
isMeasuring,
|
||||
}: MapProps) => {
|
||||
const [measuringPos, setMeasuringPos] = useState<LatLon | null>(null);
|
||||
const APIs = () => {
|
||||
useMapEvents({
|
||||
click: () => {
|
||||
click: (e) => {
|
||||
if (isMeasuring) {
|
||||
setMeasuringPos(e.latlng);
|
||||
return;
|
||||
}
|
||||
if (onMapClick) {
|
||||
onMapClick();
|
||||
}
|
||||
|
|
@ -87,82 +236,6 @@ const MapComp = ({
|
|||
return null;
|
||||
};
|
||||
|
||||
const MarkerComp = ({ pos }: { pos: posData }) => {
|
||||
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}`);
|
||||
}
|
||||
};
|
||||
|
||||
const hashKey = JSON.stringify(
|
||||
groupData ? groupData.map((p) => p.pos) : posix.pos,
|
||||
);
|
||||
|
|
@ -190,10 +263,24 @@ const MapComp = ({
|
|||
/>
|
||||
|
||||
{isGroup ? (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <index is acceptable here>
|
||||
groupData.map((pos, i) => <MarkerComp key={i} pos={pos} />)
|
||||
groupData.map((pos, i) => (
|
||||
<MarkerComp
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <index is acceptable here>
|
||||
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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,10 +4,64 @@ 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;
|
||||
}
|
||||
const colorClasses = {
|
||||
blue: {
|
||||
rectangle: "bg-blue-500 text-white",
|
||||
triangle: "border-t-blue-500",
|
||||
},
|
||||
gray: {
|
||||
rectangle: "bg-gray-500 text-white",
|
||||
triangle: "border-t-gray-500",
|
||||
},
|
||||
green: {
|
||||
rectangle: "bg-green-500 text-white",
|
||||
triangle: "border-t-green-500",
|
||||
},
|
||||
primary: {
|
||||
rectangle: "bg-[#242428] text-white",
|
||||
triangle: "border-t-[#242428]",
|
||||
},
|
||||
purple: {
|
||||
rectangle: "bg-purple-500 text-white",
|
||||
triangle: "border-t-purple-500",
|
||||
},
|
||||
red: {
|
||||
rectangle: "bg-red-500 text-white",
|
||||
triangle: "border-t-red-500",
|
||||
},
|
||||
yellow: {
|
||||
rectangle: "bg-yellow-500 text-black",
|
||||
triangle: "border-t-yellow-500",
|
||||
},
|
||||
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-[20px]",
|
||||
},
|
||||
md: {
|
||||
container: "h-12",
|
||||
rectangle: "px-3 py-2 rounded-lg",
|
||||
text: "text-sm",
|
||||
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-[15px]",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export default function MapMarker({
|
||||
label,
|
||||
|
|
@ -15,62 +69,10 @@ export default function MapMarker({
|
|||
className,
|
||||
size = "md",
|
||||
}: MapMarkerProps) {
|
||||
const colorClasses = {
|
||||
blue: {
|
||||
rectangle: "bg-blue-500 text-white",
|
||||
triangle: "border-t-blue-500",
|
||||
},
|
||||
gray: {
|
||||
rectangle: "bg-gray-500 text-white",
|
||||
triangle: "border-t-gray-500",
|
||||
},
|
||||
green: {
|
||||
rectangle: "bg-green-500 text-white",
|
||||
triangle: "border-t-green-500",
|
||||
},
|
||||
primary: {
|
||||
rectangle: "bg-[#242428] text-white",
|
||||
triangle: "border-t-[#242428]",
|
||||
},
|
||||
purple: {
|
||||
rectangle: "bg-purple-500 text-white",
|
||||
triangle: "border-t-purple-500",
|
||||
},
|
||||
red: {
|
||||
rectangle: "bg-red-500 text-white",
|
||||
triangle: "border-t-red-500",
|
||||
},
|
||||
yellow: {
|
||||
rectangle: "bg-yellow-500 text-black",
|
||||
triangle: "border-t-yellow-500",
|
||||
},
|
||||
};
|
||||
|
||||
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]",
|
||||
},
|
||||
md: {
|
||||
container: "h-12",
|
||||
rectangle: "px-3 py-2 rounded-lg",
|
||||
text: "text-sm",
|
||||
triangle: "border-l-[10px] border-r-[10px] border-t-[10px]",
|
||||
},
|
||||
sm: {
|
||||
container: "h-8",
|
||||
rectangle: "px-2 py-1 rounded-md",
|
||||
text: "text-xs",
|
||||
triangle: "border-l-[8px] border-r-[8px] border-t-[8px]",
|
||||
},
|
||||
};
|
||||
|
||||
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,
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue