copied aircraft marker to mission marker
This commit is contained in:
@@ -9,6 +9,7 @@ interface MissionStore {
|
|||||||
export const useMissionsStore = create<MissionStore>((set) => ({
|
export const useMissionsStore = create<MissionStore>((set) => ({
|
||||||
missions: [
|
missions: [
|
||||||
{
|
{
|
||||||
|
state: "draft",
|
||||||
id: "01250325",
|
id: "01250325",
|
||||||
addressLat: 52.520008,
|
addressLat: 52.520008,
|
||||||
addressLng: 13.404954,
|
addressLng: 13.404954,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useMapStore } from "_store/mapStore";
|
|||||||
import { MapContainer } from "react-leaflet";
|
import { MapContainer } from "react-leaflet";
|
||||||
import { BaseMaps } from "dispatch/_components/map/BaseMaps";
|
import { BaseMaps } from "dispatch/_components/map/BaseMaps";
|
||||||
import { ContextMenu } from "dispatch/_components/map/ContextMenu";
|
import { ContextMenu } from "dispatch/_components/map/ContextMenu";
|
||||||
import { MissionMarkers } from "dispatch/_components/map/MissionMarkers";
|
import { MissionLayer } from "dispatch/_components/map/MissionMarkers";
|
||||||
import { SearchElements } from "dispatch/_components/map/SearchElements";
|
import { SearchElements } from "dispatch/_components/map/SearchElements";
|
||||||
import { AircraftLayer } from "dispatch/_components/map/AircraftMarker";
|
import { AircraftLayer } from "dispatch/_components/map/AircraftMarker";
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export default ({}) => {
|
|||||||
<BaseMaps />
|
<BaseMaps />
|
||||||
<SearchElements />
|
<SearchElements />
|
||||||
<ContextMenu />
|
<ContextMenu />
|
||||||
<MissionMarkers />
|
<MissionLayer />
|
||||||
<AircraftLayer />
|
<AircraftLayer />
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,104 +1,252 @@
|
|||||||
import { MissionOptionalDefaults } from "@repo/db/zod";
|
|
||||||
import { useMapStore } from "_store/mapStore";
|
|
||||||
import { useMissionsStore } from "_store/missionsStore";
|
import { useMissionsStore } from "_store/missionsStore";
|
||||||
import { Icon, Marker as LMarker } from "leaflet";
|
|
||||||
import { House, Route } from "lucide-react";
|
|
||||||
import { RefObject, useEffect, useRef, useState } from "react";
|
|
||||||
import { Marker, Popup, useMap } from "react-leaflet";
|
import { Marker, Popup, useMap } from "react-leaflet";
|
||||||
|
import { DivIcon, Marker as LMarker, Popup as LPopup } from "leaflet";
|
||||||
|
import { useMapStore } from "_store/mapStore";
|
||||||
|
import { Fragment, useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { cn } from "helpers/cn";
|
||||||
|
import { Cross, House, Minimize2, Route } from "lucide-react";
|
||||||
|
import { SmartPopup, useConflict, useSmartPopup } from "_components/SmartPopup";
|
||||||
|
import { Mission } from "@repo/db";
|
||||||
|
|
||||||
export const MissionMarker = ({
|
export const MISSION_STATUS_COLORS: { [key: string]: string } = {
|
||||||
mission,
|
draft: "0092b8",
|
||||||
}: {
|
running: "#155dfc",
|
||||||
mission: MissionOptionalDefaults;
|
};
|
||||||
}) => {
|
|
||||||
const { openMissionMarker, setOpenMissionMarker } = useMapStore();
|
export const MISSION_STATUS_TEXT_COLORS: { [key: string]: string } = {
|
||||||
const [zoom, setZoom] = useState(0);
|
draft: "00d3f2",
|
||||||
|
running: "#50a2ff",
|
||||||
|
};
|
||||||
|
|
||||||
|
const MissionPopupContent = ({ mission }: { mission: Mission }) => {
|
||||||
|
const setOpenMissionMarker = useMapStore(
|
||||||
|
(state) => state.setOpenMissionMarker,
|
||||||
|
);
|
||||||
|
const { anchor } = useSmartPopup();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="absolute p-1 z-99 top-0 right-0 transform -translate-y-full bg-base-100 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setOpenMissionMarker({
|
||||||
|
open: [],
|
||||||
|
close: [mission.id],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Minimize2 className="text-white " size={15} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute w-[calc(100%+2px)] h-4 z-99",
|
||||||
|
anchor.includes("left") ? "-left-[2px]" : "-right-[2px]",
|
||||||
|
anchor.includes("top") ? "-top-[2px]" : "-bottom-[2px]",
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
borderLeft: anchor.includes("left")
|
||||||
|
? `3px solid ${MISSION_STATUS_TEXT_COLORS[mission.state]}`
|
||||||
|
: "",
|
||||||
|
borderRight: anchor.includes("right")
|
||||||
|
? `3px solid ${MISSION_STATUS_TEXT_COLORS[mission.state]}`
|
||||||
|
: "",
|
||||||
|
borderTop: anchor.includes("top")
|
||||||
|
? `3px solid ${MISSION_STATUS_TEXT_COLORS[mission.state]}`
|
||||||
|
: "",
|
||||||
|
borderBottom: anchor.includes("bottom")
|
||||||
|
? `3px solid ${MISSION_STATUS_TEXT_COLORS[mission.state]}`
|
||||||
|
: "",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="flex gap-[2px] text-white pb-0.5"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${MISSION_STATUS_TEXT_COLORS[mission.state]}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="p-2 flex justify-center items-center"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${MISSION_STATUS_TEXT_COLORS[mission.state]}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<House className="text-sm " />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="p-2 flex justify-center items-center"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${MISSION_STATUS_TEXT_COLORS[mission.state]}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Route className="text-sm " />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex justify-center items-center text-2xl p-2"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${MISSION_STATUS_TEXT_COLORS[mission.state]}`,
|
||||||
|
color: `${MISSION_STATUS_TEXT_COLORS[mission.state]}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{mission.state}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="p-2 flex-1 flex justify-center items-center"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${MISSION_STATUS_TEXT_COLORS[mission.state]}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="text-sm text-white">Einsatz 250411</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MissionMarker = ({ mission }: { mission: Mission }) => {
|
||||||
|
const missions = useMissionsStore((state) => state.missions);
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
const markerRef = useRef<LMarker<any>>(null);
|
const markerRef = useRef<LMarker>(null);
|
||||||
|
const popupRef = useRef<LPopup>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const { openMissionMarker, setOpenMissionMarker } = useMapStore(
|
||||||
// markerRef.current?.openPopup();
|
(store) => store,
|
||||||
|
);
|
||||||
const handleZoom = () => {
|
|
||||||
setZoom(map.getZoom());
|
|
||||||
};
|
|
||||||
map.on("zoom", handleZoom);
|
|
||||||
return () => {
|
|
||||||
map.off("zoom", handleZoom);
|
|
||||||
};
|
|
||||||
}, [map]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (!mission.id) return;
|
const open = openMissionMarker.includes(mission.id);
|
||||||
if (!openMissionMarker.includes(mission.id)) {
|
if (open) {
|
||||||
setOpenMissionMarker({
|
|
||||||
open: [mission.id],
|
|
||||||
close: [],
|
|
||||||
});
|
|
||||||
// setSearchPopup(null);
|
|
||||||
} else {
|
|
||||||
console.log("close", mission.id);
|
|
||||||
setOpenMissionMarker({
|
setOpenMissionMarker({
|
||||||
open: [],
|
open: [],
|
||||||
close: [mission.id],
|
close: [mission.id],
|
||||||
});
|
});
|
||||||
// setSearchPopup(null);
|
} else {
|
||||||
|
setOpenMissionMarker({
|
||||||
|
open: [mission.id],
|
||||||
|
close: [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
markerRef.current?.on("click", handleClick);
|
markerRef.current?.on("click", handleClick);
|
||||||
return () => {
|
return () => {
|
||||||
markerRef.current?.off("click", handleClick);
|
markerRef.current?.off("click", handleClick);
|
||||||
};
|
};
|
||||||
}, [markerRef.current, mission.id, setOpenMissionMarker, openMissionMarker]);
|
}, [markerRef.current, mission.id, openMissionMarker]);
|
||||||
|
|
||||||
|
const [anchor, setAnchor] = useState<
|
||||||
|
"topleft" | "topright" | "bottomleft" | "bottomright"
|
||||||
|
>("topleft");
|
||||||
|
|
||||||
|
const handleConflict = () => {
|
||||||
|
const newAnchor = useConflict(mission.id, "marker");
|
||||||
|
setAnchor(newAnchor);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleConflict();
|
||||||
|
}, [missions, openMissionMarker]);
|
||||||
|
|
||||||
|
useEffect(() => {});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
handleConflict();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
map.on("zoom", handleConflict);
|
||||||
|
return () => {
|
||||||
|
map.off("zoom", handleConflict);
|
||||||
|
};
|
||||||
|
}, [map, openMissionMarker]);
|
||||||
|
|
||||||
|
const getMarkerHTML = (
|
||||||
|
mission: Mission,
|
||||||
|
anchor: "topleft" | "topright" | "bottomleft" | "bottomright",
|
||||||
|
) => {
|
||||||
|
return `<div
|
||||||
|
class="${cn(
|
||||||
|
"relative w-[140px] transform flex items-center gap-2 px-2 z-100",
|
||||||
|
anchor.includes("right") && "-translate-x-full",
|
||||||
|
anchor.includes("bottom") && "-translate-y-full",
|
||||||
|
)}"
|
||||||
|
style="
|
||||||
|
background-color: ${MISSION_STATUS_COLORS[mission.state]};
|
||||||
|
${openMissionMarker.includes(mission.id) ? "opacity: 0; pointer-events: none;" : ""}
|
||||||
|
">
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="${cn(
|
||||||
|
"absolute w-4 h-4 z-99",
|
||||||
|
anchor.includes("left") ? "-left-[2px]" : "-right-[2px]",
|
||||||
|
anchor.includes("top") ? "-top-[2px]" : "-bottom-[2px]",
|
||||||
|
)}"
|
||||||
|
style="
|
||||||
|
${anchor.includes("left") ? `border-left: 3px solid ${MISSION_STATUS_TEXT_COLORS[mission.state]};` : `border-right: 3px solid ${MISSION_STATUS_TEXT_COLORS[mission.state]};`}
|
||||||
|
${anchor.includes("top") ? `border-top: 3px solid ${MISSION_STATUS_TEXT_COLORS[mission.state]};` : `border-bottom: 3px solid ${MISSION_STATUS_TEXT_COLORS[mission.state]};`}
|
||||||
|
"
|
||||||
|
></div>
|
||||||
|
<span
|
||||||
|
class="font-semibold text-xl"
|
||||||
|
style="color: ${MISSION_STATUS_TEXT_COLORS[mission.state]};"
|
||||||
|
>
|
||||||
|
${mission.state}
|
||||||
|
</span>
|
||||||
|
<span class="text-white text-[15px]">
|
||||||
|
${mission.missionSummary}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
data-id="${mission.id}"
|
||||||
|
id="marker-${mission.id}"
|
||||||
|
class="${cn(
|
||||||
|
"map-collision absolute w-[200%] h-[200%] top-0 left-0 transform pointer-events-none",
|
||||||
|
anchor.includes("left") && "-translate-x-1/2",
|
||||||
|
anchor.includes("top") && "-translate-y-1/2",
|
||||||
|
)}"
|
||||||
|
></div>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Fragment key={mission.id}>
|
||||||
<Marker
|
<Marker
|
||||||
ref={markerRef}
|
ref={markerRef}
|
||||||
position={[mission.addressLat, mission.addressLng]}
|
position={[mission.addressLat, mission.addressLng]}
|
||||||
icon={
|
icon={
|
||||||
new Icon({
|
new DivIcon({
|
||||||
iconUrl: "/icons/MissionIcon.png",
|
iconAnchor: [0, 0],
|
||||||
iconSize: zoom < 8 ? [30, 30] : [50, 50],
|
html: getMarkerHTML(mission, anchor),
|
||||||
popupAnchor: [0, 0],
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
></Marker>
|
/>
|
||||||
{mission.id && openMissionMarker.includes(mission.id) && (
|
{openMissionMarker.includes(mission.id) && (
|
||||||
<Popup
|
<SmartPopup
|
||||||
|
id={mission.id}
|
||||||
|
ref={popupRef}
|
||||||
position={[mission.addressLat, mission.addressLng]}
|
position={[mission.addressLat, mission.addressLng]}
|
||||||
autoClose={false}
|
autoClose={false}
|
||||||
closeOnClick={false}
|
closeOnClick={false}
|
||||||
autoPan={false}
|
autoPan={false}
|
||||||
|
wrapperClassName="relative"
|
||||||
|
className="w-[200px] h-[150px]"
|
||||||
>
|
>
|
||||||
<div className="absolute z-1000 opacity-100 pointer-events-auto w-[500px] text-white">
|
<MissionPopupContent mission={mission} />
|
||||||
<div className="bg-amber-600 pt-2 flex gap-1">
|
</SmartPopup>
|
||||||
<div className="p-2 bg-amber-700">
|
|
||||||
<House className="text-sm" />
|
|
||||||
</div>
|
|
||||||
<div className="p-2">
|
|
||||||
<Route />
|
|
||||||
</div>
|
|
||||||
<div className="bg-red-600 flex justify-center items-center text-2xl p-2">
|
|
||||||
{mission.missionCategory}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Popup>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MissionMarkers = () => {
|
export const MissionLayer = () => {
|
||||||
const { missions } = useMissionsStore();
|
const missions = useMissionsStore((state) => state.missions);
|
||||||
|
|
||||||
|
// IDEA: Add Marker to Map Layer / LayerGroup
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{missions.map((mission) => (
|
{missions.map((mission) => {
|
||||||
<MissionMarker key={mission.id} mission={mission} />
|
return <MissionMarker key={mission.id} mission={mission as Mission} />;
|
||||||
))}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Binary file not shown.
@@ -1,5 +1,6 @@
|
|||||||
model Mission {
|
model Mission {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
|
state MissionState @default(draft)
|
||||||
addressLat Float
|
addressLat Float
|
||||||
addressLng Float
|
addressLng Float
|
||||||
addressStreet String
|
addressStreet String
|
||||||
@@ -10,11 +11,17 @@ model Mission {
|
|||||||
missionSummary String
|
missionSummary String
|
||||||
missionPatientInfo String
|
missionPatientInfo String
|
||||||
missionAdditionalInfo String
|
missionAdditionalInfo String
|
||||||
hpgAmbulanceState HpgState? @default(ready)
|
hpgAmbulanceState HpgState? @default(ready)
|
||||||
hpgFireEngineState HpgState? @default(ready)
|
hpgFireEngineState HpgState? @default(ready)
|
||||||
hpgPoliceState HpgState? @default(ready)
|
hpgPoliceState HpgState? @default(ready)
|
||||||
hpgLocationLat Float? @default(0)
|
hpgLocationLat Float? @default(0)
|
||||||
hpgLocationLng Float? @default(0)
|
hpgLocationLng Float? @default(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MissionState {
|
||||||
|
running
|
||||||
|
finished
|
||||||
|
draft
|
||||||
}
|
}
|
||||||
|
|
||||||
enum HpgState {
|
enum HpgState {
|
||||||
|
|||||||
Reference in New Issue
Block a user