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";
|
"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:
|
||||||
|
|
|
||||||
|
|
@ -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,6 +68,144 @@ const MedianPoint = (pos: posData[]) => {
|
||||||
return { lat, lng };
|
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 = ({
|
const MapComp = ({
|
||||||
posix,
|
posix,
|
||||||
autocenter,
|
autocenter,
|
||||||
|
|
@ -75,10 +218,16 @@ const MapComp = ({
|
||||||
isGroup,
|
isGroup,
|
||||||
groupData,
|
groupData,
|
||||||
onMapClick,
|
onMapClick,
|
||||||
|
isMeasuring,
|
||||||
}: MapProps) => {
|
}: MapProps) => {
|
||||||
|
const [measuringPos, setMeasuringPos] = useState<LatLon | null>(null);
|
||||||
const APIs = () => {
|
const APIs = () => {
|
||||||
useMapEvents({
|
useMapEvents({
|
||||||
click: () => {
|
click: (e) => {
|
||||||
|
if (isMeasuring) {
|
||||||
|
setMeasuringPos(e.latlng);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (onMapClick) {
|
if (onMapClick) {
|
||||||
onMapClick();
|
onMapClick();
|
||||||
}
|
}
|
||||||
|
|
@ -87,82 +236,6 @@ const MapComp = ({
|
||||||
return null;
|
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(
|
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 ? (
|
||||||
// biome-ignore lint/suspicious/noArrayIndexKey: <index is acceptable here>
|
groupData.map((pos, i) => (
|
||||||
groupData.map((pos, i) => <MarkerComp key={i} pos={pos} />)
|
<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>
|
</MapContainer>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,64 @@ 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;
|
||||||
}
|
}
|
||||||
|
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({
|
export default function MapMarker({
|
||||||
label,
|
label,
|
||||||
|
|
@ -15,62 +69,10 @@ export default function MapMarker({
|
||||||
className,
|
className,
|
||||||
size = "md",
|
size = "md",
|
||||||
}: MapMarkerProps) {
|
}: 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 (
|
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,
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue