diff --git a/apps/client/locales/en/cad-settings.json b/apps/client/locales/en/cad-settings.json index 2c701824c..0318ff111 100644 --- a/apps/client/locales/en/cad-settings.json +++ b/apps/client/locales/en/cad-settings.json @@ -459,6 +459,7 @@ "SetUserDefinedCallsignOnOfficer": "Set User Defined Callsign On Officer", "SetUserDefinedCallsignOnEmsFd": "Set User Defined Callsign On EMS/FD Deputy", "LeoManageCitizenProfile": "Manage Citizen Profile (non-admin)", - "ManageSmartSigns": "Manage Smart Signs" + "ManageSmartSigns": "Manage Smart Signs", + "ManageSmartMotorwaySigns": "Manage Smart Motorway Signs" } } diff --git a/apps/client/locales/en/leo.json b/apps/client/locales/en/leo.json index 378b71b43..2d480a2fa 100644 --- a/apps/client/locales/en/leo.json +++ b/apps/client/locales/en/leo.json @@ -333,6 +333,10 @@ "showSmartSigns": "Show Smart Signs", "smartSignUpdated": "SmartSign Updated", "smartSignUpdatedMessage": "We sent a request to the FXServer to update the SmartSign.", + "hideSmartMotorwaySigns": "Hide Smart Motorway Signs", + "showSmartMotorwaySigns": "Show Smart Motorway Signs", + "smartMotorwaySignUpdated": "Smart Motorway Sign Updated", + "smartMotorwaySignUpdatedMessage": "We sent a request to the FXServer to update the Smart Motorway Sign.", "departmentInformation": "Department Info", "departmentInformationDesc": "Here you can view further external links and information for your department.", "noDepartmentLinks": "This department doesn't have any extra information yet.", @@ -360,7 +364,24 @@ "assignToIncident": "Assign to incident", "myRecordReports": "My Record Reports", "myRecordReportsDescription": "Here you can view all the tickets, arrest reports, written warnings and warrants that officers associated to your account have created.", - "noReportsCreated": "You have not created any reports yet." + "noReportsCreated": "You have not created any reports yet.", + "motorway_sign_1": "Arrow Left", + "motorway_sign_2": "Arrow Right", + "motorway_sign_3": "Red X", + "motorway_sign_20": "Speed 20", + "motorway_sign_40": "Speed 30", + "motorway_sign_30": "Speed 30", + "motorway_sign_50": "Speed 50", + "motorway_sign_60": "Speed 60", + "motorway_sign_70": "Speed 70", + "motorway_sign_80": "Speed 80", + "motorway_sign_90": "Speed 90", + "motorway_sign_100": "Speed 100", + "motorway_sign_110": "Speed 110", + "motorway_sign_120": "Speed 120", + "motorway_sign_130": "Speed 130", + "motorway_sign_140": "Speed 140", + "motorway_sign_150": "Speed 150" }, "Bolos": { "activeBolos": "Active Bolos", diff --git a/apps/client/public/map/smart-motorways/1.png b/apps/client/public/map/smart-motorways/1.png new file mode 100644 index 000000000..b7427fb44 Binary files /dev/null and b/apps/client/public/map/smart-motorways/1.png differ diff --git a/apps/client/public/map/smart-motorways/100.png b/apps/client/public/map/smart-motorways/100.png new file mode 100644 index 000000000..6fa8edec3 Binary files /dev/null and b/apps/client/public/map/smart-motorways/100.png differ diff --git a/apps/client/public/map/smart-motorways/110.png b/apps/client/public/map/smart-motorways/110.png new file mode 100644 index 000000000..711e26ee9 Binary files /dev/null and b/apps/client/public/map/smart-motorways/110.png differ diff --git a/apps/client/public/map/smart-motorways/120.png b/apps/client/public/map/smart-motorways/120.png new file mode 100644 index 000000000..1505c76d7 Binary files /dev/null and b/apps/client/public/map/smart-motorways/120.png differ diff --git a/apps/client/public/map/smart-motorways/130.png b/apps/client/public/map/smart-motorways/130.png new file mode 100644 index 000000000..64bcf5eb5 Binary files /dev/null and b/apps/client/public/map/smart-motorways/130.png differ diff --git a/apps/client/public/map/smart-motorways/140.png b/apps/client/public/map/smart-motorways/140.png new file mode 100644 index 000000000..a65eed341 Binary files /dev/null and b/apps/client/public/map/smart-motorways/140.png differ diff --git a/apps/client/public/map/smart-motorways/150.png b/apps/client/public/map/smart-motorways/150.png new file mode 100644 index 000000000..55d35cbcb Binary files /dev/null and b/apps/client/public/map/smart-motorways/150.png differ diff --git a/apps/client/public/map/smart-motorways/2.png b/apps/client/public/map/smart-motorways/2.png new file mode 100644 index 000000000..0514f7c80 Binary files /dev/null and b/apps/client/public/map/smart-motorways/2.png differ diff --git a/apps/client/public/map/smart-motorways/20.png b/apps/client/public/map/smart-motorways/20.png new file mode 100644 index 000000000..6fd407af2 Binary files /dev/null and b/apps/client/public/map/smart-motorways/20.png differ diff --git a/apps/client/public/map/smart-motorways/3.png b/apps/client/public/map/smart-motorways/3.png new file mode 100644 index 000000000..e99a8f517 Binary files /dev/null and b/apps/client/public/map/smart-motorways/3.png differ diff --git a/apps/client/public/map/smart-motorways/30.png b/apps/client/public/map/smart-motorways/30.png new file mode 100644 index 000000000..ede01aea9 Binary files /dev/null and b/apps/client/public/map/smart-motorways/30.png differ diff --git a/apps/client/public/map/smart-motorways/40.png b/apps/client/public/map/smart-motorways/40.png new file mode 100644 index 000000000..0d3429d66 Binary files /dev/null and b/apps/client/public/map/smart-motorways/40.png differ diff --git a/apps/client/public/map/smart-motorways/50.png b/apps/client/public/map/smart-motorways/50.png new file mode 100644 index 000000000..9d0aafb27 Binary files /dev/null and b/apps/client/public/map/smart-motorways/50.png differ diff --git a/apps/client/public/map/smart-motorways/60.png b/apps/client/public/map/smart-motorways/60.png new file mode 100644 index 000000000..d1a626358 Binary files /dev/null and b/apps/client/public/map/smart-motorways/60.png differ diff --git a/apps/client/public/map/smart-motorways/70.png b/apps/client/public/map/smart-motorways/70.png new file mode 100644 index 000000000..458fa70e2 Binary files /dev/null and b/apps/client/public/map/smart-motorways/70.png differ diff --git a/apps/client/public/map/smart-motorways/80.png b/apps/client/public/map/smart-motorways/80.png new file mode 100644 index 000000000..4b8d7b8c3 Binary files /dev/null and b/apps/client/public/map/smart-motorways/80.png differ diff --git a/apps/client/public/map/smart-motorways/90.png b/apps/client/public/map/smart-motorways/90.png new file mode 100644 index 000000000..4f92af3a1 Binary files /dev/null and b/apps/client/public/map/smart-motorways/90.png differ diff --git a/apps/client/public/map/smart-motorways/CREDITS.txt b/apps/client/public/map/smart-motorways/CREDITS.txt new file mode 100644 index 000000000..51abdd387 --- /dev/null +++ b/apps/client/public/map/smart-motorways/CREDITS.txt @@ -0,0 +1,3 @@ +# London Studios + +All credits to London Studios (https://londonstudios.net) for the default SmartMotorways images/signs. \ No newline at end of file diff --git a/apps/client/public/map/smart-motorways/national_speed.png b/apps/client/public/map/smart-motorways/national_speed.png new file mode 100644 index 000000000..dc906f28a Binary files /dev/null and b/apps/client/public/map/smart-motorways/national_speed.png differ diff --git a/apps/client/src/components/dispatch/map/map-actions.tsx b/apps/client/src/components/dispatch/map/map-actions.tsx index 0b1a4c407..13297b0d3 100644 --- a/apps/client/src/components/dispatch/map/map-actions.tsx +++ b/apps/client/src/components/dispatch/map/map-actions.tsx @@ -27,6 +27,9 @@ export function MapActions() { const { hasPermissions } = usePermission(); const hasManageUsersPermissions = hasPermissions([Permissions.ManageUsers]); const hasManageSmartSignsPermissions = hasPermissions([Permissions.ManageSmartSigns]); + const hasManageSmartMotorwaySignsPermissions = hasPermissions([ + Permissions.ManageSmartMotorwaySigns, + ]); return ( portalRef && @@ -49,6 +52,13 @@ export function MapActions() { : t("Leo.hideSmartSigns")} ) : null} + {hasManageSmartMotorwaySignsPermissions ? ( + mapState.setItem(MapItem.SMART_MOTORWAY_SIGNS)}> + {mapState.hiddenItems[MapItem.SMART_MOTORWAY_SIGNS] + ? t("Leo.showSmartMotorwaySigns") + : t("Leo.hideSmartMotorwaySigns")} + + ) : null} mapState.setItem(MapItem.BLIPS)}> {mapState.hiddenItems[MapItem.BLIPS] ? t("Leo.showBlips") : t("Leo.hideBlips")} diff --git a/apps/client/src/components/dispatch/map/map.tsx b/apps/client/src/components/dispatch/map/map.tsx index e673f27c7..570dd4ffa 100644 --- a/apps/client/src/components/dispatch/map/map.tsx +++ b/apps/client/src/components/dispatch/map/map.tsx @@ -8,6 +8,7 @@ import { MapActions } from "./map-actions"; import { RenderMapPlayers } from "./units/render-map-players"; import { SelectMapServerModal } from "./modals/select-map-server-modal"; import { RenderMapSmartSigns } from "./smart-signs/render-map-smart-signs"; +import { RenderMapSmartMotorwaySigns } from "./smart-motorway-signs/render-map-smart-motorway-signs"; const TILES_URL = "/tiles/minimap_sea_{y}_{x}.webp" as const; @@ -50,6 +51,7 @@ export function Map() { + diff --git a/apps/client/src/components/dispatch/map/smart-motorway-signs/render-map-smart-motorway-signs.tsx b/apps/client/src/components/dispatch/map/smart-motorway-signs/render-map-smart-motorway-signs.tsx new file mode 100644 index 000000000..b10bb350a --- /dev/null +++ b/apps/client/src/components/dispatch/map/smart-motorway-signs/render-map-smart-motorway-signs.tsx @@ -0,0 +1,9 @@ +import { SmartMotorwaySignsMarker } from "./smart-motorway-sign-marker"; +import { useSmartMotorwaySigns } from "./use-smart-motorway-signs"; + +export function RenderMapSmartMotorwaySigns() { + const smartMotorwaySigns = useSmartMotorwaySigns(); + return smartMotorwaySigns?.map((marker, idx) => ( + + )); +} diff --git a/apps/client/src/components/dispatch/map/smart-motorway-signs/smart-motorway-sign-marker.tsx b/apps/client/src/components/dispatch/map/smart-motorway-signs/smart-motorway-sign-marker.tsx new file mode 100644 index 000000000..f84108736 --- /dev/null +++ b/apps/client/src/components/dispatch/map/smart-motorway-signs/smart-motorway-sign-marker.tsx @@ -0,0 +1,170 @@ +import * as React from "react"; +import { convertToMap } from "lib/map/utils"; +import { Marker, Popup, Tooltip, useMap } from "react-leaflet"; +import { SmartMotorwaySignSpeedType, type SmartMotorwaySignMarker } from "types/map"; +import { icon as leafletIcon } from "leaflet"; +import { useTranslations } from "next-intl"; +import { MapItem, useDispatchMapState, useSocketStore } from "state/mapState"; +import { generateMarkerTypes } from "../render-map-blips"; +import { Button, SelectField } from "@snailycad/ui"; +import { Permissions, usePermission } from "hooks/usePermission"; +import { toastMessage } from "lib/toastMessage"; +import Image from "next/image"; + +interface Props { + marker: SmartMotorwaySignMarker & { id: number }; +} + +const SPEED_INDICATORS = Object.values(SmartMotorwaySignSpeedType).filter( + (v) => !isNaN(Number(v)), +) as number[]; + +export function SmartMotorwaySignsMarker({ marker }: Props) { + const map = useMap(); + const socket = useSocketStore((state) => state.socket); + const [markerConfiguration, setMarkerConfiguration] = React.useState([]); + + const t = useTranslations("Leo"); + const hiddenItems = useDispatchMapState((state) => state.hiddenItems); + const markerTypes = React.useMemo(generateMarkerTypes, []); + + const { hasPermissions } = usePermission(); + const hasManageSmartSignsPermissions = hasPermissions([Permissions.ManageSmartSigns]); + + const pos = React.useMemo( + () => convertToMap(marker.position.x, marker.position.y, map), + [marker.position], // eslint-disable-line react-hooks/exhaustive-deps + ); + const markerIcon = React.useMemo(() => { + // eslint-disable-next-line prefer-destructuring + const icon = markerTypes[781]; + + if (icon) { + return leafletIcon(icon); + } + + return undefined; + }, [markerTypes]); + + React.useEffect(() => { + const speeds = marker.speeds ?? marker.defaultSpeeds; + if (speeds) { + setMarkerConfiguration(speeds.map((speed) => String(speed))); + } + }, [marker.speeds, marker.defaultSpeeds]); + + function handleSubmit(event: React.FormEvent) { + event.preventDefault(); + if (!socket?.connected) return; + + socket.emit("sna-live-map:update-smart-motorway-sign", { + ...marker, + speeds: markerConfiguration.map((v) => Number(v)), + id: marker.id + 1, + }); + + toastMessage({ + icon: "success", + title: t("smartSignUpdated"), + message: t("smartSignUpdatedMessage"), + }); + } + + if (hiddenItems[MapItem.SMART_MOTORWAY_SIGNS]) { + return null; + } + + return ( + + Smart Motorway Sign + + +

+ Direction: {marker.direction} +

+ +

+ Lanes: {marker.lanes} +

+ +
+ {new Array(marker.lanes).fill(null).map((_, idx) => { + return ( + + setMarkerConfiguration((prev) => { + const newConfig = [...prev]; + newConfig[idx] = String(key); + return newConfig; + }) + } + key={idx} + label={`Lane ${idx + 1}`} + options={SPEED_INDICATORS.map((key) => ({ + textValue: t(`motorway_sign_${key}`), + label: ( +

+ {t(`motorway_sign_${key}`)} + {t(`motorway_sign_${key}`)} +

+ ), + value: String(key), + }))} + /> + ); + })} + +
+

Preview

+ + {markerConfiguration.length <= 0 ? ( +

Configure the lanes to see a preview

+ ) : ( +
+ {new Array(marker.lanes).fill(null).map((_, idx) => { + const speed = markerConfiguration[idx]; + + if (!speed) { + return
; + } + + return ( + {t(`motorway_sign_${speed}`)} + ); + })} +
+ )} +
+ +
+ + +
+ +
+
+ ); +} diff --git a/apps/client/src/components/dispatch/map/smart-motorway-signs/use-smart-motorway-signs.tsx b/apps/client/src/components/dispatch/map/smart-motorway-signs/use-smart-motorway-signs.tsx new file mode 100644 index 000000000..388ff5d90 --- /dev/null +++ b/apps/client/src/components/dispatch/map/smart-motorway-signs/use-smart-motorway-signs.tsx @@ -0,0 +1,32 @@ +import * as React from "react"; +import { useDispatchMapState, useSocketStore } from "state/mapState"; +import type { SmartMotorwaySignMarker } from "types/map"; + +export function useSmartMotorwaySigns() { + const socket = useSocketStore((state) => state.socket); + const { smartMotorwaySigns, setSmartMotorwaySigns } = useDispatchMapState((state) => ({ + smartMotorwaySigns: state.smartMotorwaySigns, + setSmartMotorwaySigns: state.setSmartMotorwaySigns, + })); + + const onInitialize = React.useCallback( + (data: { smartMotorwaySigns: SmartMotorwaySignMarker[] }) => { + setSmartMotorwaySigns(data.smartMotorwaySigns); + }, + [], // eslint-disable-line react-hooks/exhaustive-deps + ); + + React.useEffect(() => { + const s = socket; + + if (s) { + s.on("sna-live-map:smart-motorways-signs", onInitialize); + } + + return () => { + s?.off("sna-live-map:smart-motorways-signs", onInitialize); + }; + }, [socket, onInitialize]); + + return smartMotorwaySigns; +} diff --git a/apps/client/src/hooks/use-permissions-modal.tsx b/apps/client/src/hooks/use-permissions-modal.tsx index ef7c4df7d..83cdf81dd 100644 --- a/apps/client/src/hooks/use-permissions-modal.tsx +++ b/apps/client/src/hooks/use-permissions-modal.tsx @@ -38,7 +38,11 @@ export function usePermissionsModal(options: UsePermissionsModalOptions) { }, { name: t("dispatch"), - permissions: [...defaultPermissions.defaultDispatchPermissions, Permissions.ManageSmartSigns], + permissions: [ + ...defaultPermissions.defaultDispatchPermissions, + Permissions.ManageSmartSigns, + Permissions.ManageSmartMotorwaySigns, + ], }, { name: t("emsFd"), diff --git a/apps/client/src/lib/map/blips.ts b/apps/client/src/lib/map/blips.ts index fe4654704..aa49392c9 100644 --- a/apps/client/src/lib/map/blips.ts +++ b/apps/client/src/lib/map/blips.ts @@ -182,4 +182,5 @@ export const blipTypes: Record = { LSCarMeet: { id: 777, x: 4, y: 29 }, LSCarMeetGarage: { id: 779, x: 2, y: 29 }, Computer: { id: 780, x: 11, y: 113.75 }, + Computer2: { id: 781, x: 4, y: 109.75 }, }; diff --git a/apps/client/src/pages/officer/my-record-reports.tsx b/apps/client/src/pages/officer/my-record-reports.tsx index 3ba8c95d5..07e504bee 100644 --- a/apps/client/src/pages/officer/my-record-reports.tsx +++ b/apps/client/src/pages/officer/my-record-reports.tsx @@ -117,8 +117,6 @@ export const getServerSideProps: GetServerSideProps = async ({ req, local ["/leo/my-record-reports", { reports: [], totalCount: 0 }], ]); - console.log(reports); - return { props: { session: user, diff --git a/apps/client/src/state/mapState.ts b/apps/client/src/state/mapState.ts index 50fd10ce9..82e488dc4 100644 --- a/apps/client/src/state/mapState.ts +++ b/apps/client/src/state/mapState.ts @@ -1,6 +1,6 @@ import { ConnectionStatus } from "@snailycad/ui"; import { Socket } from "socket.io-client"; -import { SmartSignMarker } from "types/map"; +import { SmartMotorwaySignMarker, SmartSignMarker } from "types/map"; import { persist, createJSONStorage } from "zustand/middleware"; import { shallow } from "zustand/shallow"; import { createWithEqualityFn } from "zustand/traditional"; @@ -10,12 +10,16 @@ export enum MapItem { UNITS_ONLY, BLIPS, SMART_SIGNS, + SMART_MOTORWAY_SIGNS, } interface DispatchMapState { smartSigns: SmartSignMarker[]; setSmartSigns(signs: SmartSignMarker[]): void; + smartMotorwaySigns: SmartMotorwaySignMarker[]; + setSmartMotorwaySigns(signs: SmartMotorwaySignMarker[]): void; + hiddenItems: Partial>; setItem(item: MapItem): void; @@ -39,6 +43,11 @@ export const useDispatchMapState = createWithEqualityFn()( set({ smartSigns: signs }); }, + smartMotorwaySigns: [], + setSmartMotorwaySigns(signs) { + set({ smartMotorwaySigns: signs }); + }, + currentMapServerURL: null, setCurrentMapServerURL(url) { set({ currentMapServerURL: url }); diff --git a/apps/client/src/types/map.ts b/apps/client/src/types/map.ts index 56cbb9343..a07c90e99 100644 --- a/apps/client/src/types/map.ts +++ b/apps/client/src/types/map.ts @@ -84,3 +84,34 @@ export interface SmartSignMarker { id: 5; defaultText: Record<"firstLine" | "secondLine" | "thirdLine", string>; } + +export enum SmartMotorwaySignSpeedType { + ArrowLeft = 1, + ArrowRight = 2, + RedX = 3, + Speed20 = 20, + Speed30 = 30, + Speed40 = 40, + Speed50 = 50, + Speed60 = 60, + Speed70 = 70, + Speed80 = 80, + Speed90 = 90, + Speed100 = 100, + Speed110 = 110, + Speed120 = 120, + Speed130 = 130, + Speed140 = 140, + Speed150 = 150, +} +export interface SmartMotorwaySignMarker { + defaultSpeeds?: SmartMotorwaySignSpeedType[]; + speeds?: SmartMotorwaySignSpeedType[]; + position: { + x: number; + y: number; + z: number; + }; + lanes: number; + direction: string; +} diff --git a/packages/permissions/src/permissions.ts b/packages/permissions/src/permissions.ts index dbb3b186b..a60b17ad4 100644 --- a/packages/permissions/src/permissions.ts +++ b/packages/permissions/src/permissions.ts @@ -30,6 +30,7 @@ export enum Permissions { Dispatch = "Dispatch", ManageSmartSigns = "ManageSmartSigns", + ManageSmartMotorwaySigns = "ManageSmartMotorwaySigns", // ems-fd EmsFd = "EmsFd", diff --git a/packages/ui/src/components/fields/select-field.tsx b/packages/ui/src/components/fields/select-field.tsx index 5de8e5e0c..bd6b94175 100644 --- a/packages/ui/src/components/fields/select-field.tsx +++ b/packages/ui/src/components/fields/select-field.tsx @@ -20,6 +20,7 @@ export interface SelectValue { label: React.ReactNode; isDisabled?: boolean; description?: string | null; + textValue?: string; } export type SelectFieldProps = Omit< @@ -47,7 +48,11 @@ export function SelectField(props: SelectFieldProps) { const selectionMode = props.selectionMode ?? "single"; const children = React.useMemo(() => { - return props.options.map((option) => {option.label}); + return props.options.map((option) => ( + + {option.label} + + )); }, [props.options]); const disabledKeys = React.useMemo(() => {