Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/map/components/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
import Control from "react-leaflet-custom-control";

import NonPlayableStations from "@/components/NonPlayableStations";
import PassengerStations from "@/components/PassengerStations";
import RemoteStations from "@/components/RemoteStations";
import { TrainsList } from "@/components/TrainsList";

Expand Down Expand Up @@ -523,6 +524,18 @@ const LeaftletMap = ({ serverId }: MapProps) => {
</LayerGroup>
</LayersControl.Overlay>

<LayersControl.Overlay
checked={
localStorage.getItem("layer-passenger stations") === null ||
localStorage.getItem("layer-passenger stations") === "true"
}
name="Passenger stations"
>
<LayerGroup>
<PassengerStations />
</LayerGroup>
</LayersControl.Overlay>

<LayersControl.Overlay
checked={
localStorage.getItem("layer-signalling (not 100% accurate)") ===
Expand Down
108 changes: 108 additions & 0 deletions packages/map/components/Markers/HoverPopup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import L from "leaflet";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
import { useEffect, useRef } from "react";
import { Popup, type PopupProps } from "react-leaflet";

const POPUP_TIP_HEIGHT = 20;
const POPUP_EDGE_PADDING = 10;
// Keeps the tip from sliding past the rounded corners of the popup body.
const POPUP_TIP_INSET = 30;

type PopupInternals = {
_map?: L.Map;
_getAnchor: () => L.Point;
};

// Places the popup above or below its anchor (whichever fits) and shifts it
// horizontally to stay inside the map, instead of letting Leaflet pan the map.
const placePopupInsideMap = (popup: L.Popup) => {
const internals = popup as unknown as PopupInternals;
const map = internals._map;
const container = popup.getElement();
const latLng = popup.getLatLng();
if (!map || !container || !latLng) return;

const wrapper = container.querySelector<HTMLElement>(
".leaflet-popup-content-wrapper",
);
const tipContainer = container.querySelector<HTMLElement>(
".leaflet-popup-tip-container",
);
if (!wrapper) return;

const width = wrapper.offsetWidth;
const height = wrapper.offsetHeight;
const mapSize = map.getSize();
// The popup anchor (e.g. the icon's popupAnchor) is where the tip points
// when the popup sits above the marker; mirror it when placing below.
const anchor = internals._getAnchor();
const point = map.latLngToContainerPoint(latLng).add([anchor.x, 0]);

const spaceAbove = point.y + anchor.y - POPUP_TIP_HEIGHT - height;
const spaceBelow =
mapSize.y - (point.y - anchor.y + POPUP_TIP_HEIGHT + height);
const placeBelow = spaceAbove < POPUP_EDGE_PADDING && spaceBelow > spaceAbove;

const minShift = POPUP_EDGE_PADDING - (point.x - width / 2);
const maxShift = mapSize.x - POPUP_EDGE_PADDING - (point.x + width / 2);
// Prefer the left edge when the popup is wider than the map.
const fitShift =
minShift > maxShift ? minShift : Math.min(Math.max(0, minShift), maxShift);
const shiftX = Math.round(fitShift);
// The tip stays over the anchor unless that would put it past the body's
// rounded corners (anchor right at the map edge); then it stops there.
const maxTipShift = Math.max(0, width / 2 - POPUP_TIP_INSET);
const tipShift = Math.min(Math.max(shiftX, -maxTipShift), maxTipShift);
const shiftY = placeBelow ? -2 * anchor.y + 2 * POPUP_TIP_HEIGHT + height : 0;

container.classList.toggle("popup-below", placeBelow);
if (tipContainer) tipContainer.style.marginLeft = `${-20 - tipShift}px`;

const current = L.point(popup.options.offset ?? [0, 0]);
if (current.x === shiftX && current.y === shiftY) return;
popup.options.offset = L.point(shiftX, shiftY);
popup.update();
};

// A popup that repositions itself to fit inside the map instead of panning it.
const HoverPopup = (props: PopupProps) => {
const popupRef = useRef<L.Popup | null>(null);

useEffect(() => {
const popup = popupRef.current;
if (!popup) return;

let observer: ResizeObserver | null = null;
let map: L.Map | null = null;
const place = () => placePopupInsideMap(popup);
const onAdd = () => {
const wrapper = popup
.getElement()
?.querySelector(".leaflet-popup-content-wrapper");
if (!wrapper) return;
// Also fires once the content renders or changes size (e.g. images).
observer = new ResizeObserver(place);
observer.observe(wrapper);
// The anchor moves within the map when it's panned, zoomed or resized.
map = (popup as unknown as PopupInternals)._map ?? null;
map?.on("resize moveend zoomend", place);
};
const onRemove = () => {
observer?.disconnect();
observer = null;
map?.off("resize moveend zoomend", place);
map = null;
};

popup.on("add", onAdd);
popup.on("remove", onRemove);
return () => {
popup.off("add", onAdd);
popup.off("remove", onRemove);
onRemove();
};
}, []);

return <Popup ref={popupRef} autoPan={false} {...props} />;
};

export default HoverPopup;
8 changes: 5 additions & 3 deletions packages/map/components/Markers/NonPlayableStationMarker.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Station } from "@simrail/types";
import L from "leaflet";
import { Marker, Popup } from "react-leaflet";
import { Marker } from "react-leaflet";

import HoverPopup from "./HoverPopup";

import styles from "../../styles/MarkerPopup.module.css";

Expand All @@ -26,12 +28,12 @@ export const NonPlayableStationMarker = ({ station }: StationMarkerProps) => {
mouseout: (event) => event.target.closePopup(),
}}
>
<Popup className="simple-map-popup">
<HoverPopup className="simple-map-popup">
<div className={styles.simpleCard}>
<small>Unplayable station</small>
<strong>{station.Name}</strong>
</div>
</Popup>
</HoverPopup>
</Marker>
);
};
39 changes: 39 additions & 0 deletions packages/map/components/Markers/PassengerStationMarker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Station } from "@simrail/types";
import L from "leaflet";
import { Marker } from "react-leaflet";

import HoverPopup from "./HoverPopup";

import styles from "../../styles/MarkerPopup.module.css";

type StationMarkerProps = {
station: Station;
};

export const PassengerStationMarker = ({ station }: StationMarkerProps) => {
const icon = L.icon({
iconUrl: "/markers/icon-station-passenger.png",
iconSize: [16, 16],
popupAnchor: [0, -16],
});

return (
<Marker
key={station.id}
icon={icon}
position={[station.Latititude, station.Longitude]}
zIndexOffset={30}
eventHandlers={{
mouseover: (event) => event.target.openPopup(),
mouseout: (event) => event.target.closePopup(),
}}
>
<HoverPopup className="simple-map-popup">
<div className={styles.simpleCard}>
<small>Passenger station</small>
<strong>{station.Name}</strong>
</div>
</HoverPopup>
</Marker>
);
};
8 changes: 5 additions & 3 deletions packages/map/components/Markers/SignalMarker.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Signal } from "@simrail/types";
import { memo } from "react";
import { CircleMarker, Popup } from "react-leaflet";
import { CircleMarker } from "react-leaflet";

import HoverPopup from "./HoverPopup";

import styles from "../../styles/MarkerPopup.module.css";

Expand All @@ -26,12 +28,12 @@ export const SignalMarker = memo(({ signal }: SignalMarkerProps) => {
mouseout: (event) => event.target.closePopup(),
}}
>
<Popup className="simple-map-popup">
<HoverPopup className="simple-map-popup">
<div className={styles.simpleCard}>
<small>Signal</small>
<strong>{signal.Name}</strong>
</div>
</Popup>
</HoverPopup>
</CircleMarker>
);
});
Expand Down
7 changes: 4 additions & 3 deletions packages/map/components/Markers/StationMarker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import type { Station } from "@simrail/types";
import L from "leaflet";
import { usePathname, useRouter } from "next/navigation";
import { memo, useEffect, useState } from "react";
import { Marker, Popup, Tooltip } from "react-leaflet";
import { Marker, Tooltip } from "react-leaflet";

import stationsList from "../EDR_station.json";
import { getSteamProfileOrBot } from "../steam";
import HoverPopup from "./HoverPopup";

import styles from "../../styles/MarkerPopup.module.css";

Expand Down Expand Up @@ -94,7 +95,7 @@ export const StationMarker = memo(({ station }: StationMarkerProps) => {
},
}}
>
<Popup className="station-map-popup" minWidth={250}>
<HoverPopup className="station-map-popup" minWidth={250}>
<div className={styles.stationCard}>
<div className={styles.stationImage}>
<img src={station.MainImageURL} alt="" width={250} height={104} />
Expand All @@ -113,7 +114,7 @@ export const StationMarker = memo(({ station }: StationMarkerProps) => {
<span className={styles.openHint}>Open station in EDR ↗</span>
</div>
</div>
</Popup>
</HoverPopup>
<Tooltip
className="station-name-tooltip"
offset={[0, 20]}
Expand Down
8 changes: 5 additions & 3 deletions packages/map/components/Markers/StationRemoteMarker.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Station } from "@simrail/types";
import L from "leaflet";
import { Marker, Popup } from "react-leaflet";
import { Marker } from "react-leaflet";

import HoverPopup from "./HoverPopup";

import styles from "../../styles/MarkerPopup.module.css";

Expand All @@ -27,13 +29,13 @@ export const RemoteStationMarker = ({ station }: StationMarkerProps) => {
mouseout: (event) => event.target.closePopup(),
}}
>
<Popup className="simple-map-popup">
<HoverPopup className="simple-map-popup">
<div className={styles.simpleCard}>
<small>Remote station</small>
<strong>{station.Name}</strong>
<span>Controlled from {station.id}</span>
</div>
</Popup>
</HoverPopup>
</Marker>
);
};
7 changes: 4 additions & 3 deletions packages/map/components/Markers/TrainMarker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { useMantineColorScheme } from "@mantine/core";
import type { Train } from "@simrail/types";
import L from "leaflet";
import { memo, useEffect, useMemo, useRef, useState } from "react";
import { Popup, Tooltip } from "react-leaflet";
import { Tooltip } from "react-leaflet";
import ReactLeafletDriftMarker from "react-leaflet-drift-marker";

import { getSteamProfileOrBot } from "@/components/steam";

import TrainText from "../TrainText";
import HoverPopup from "./HoverPopup";

type TrainMarkerProps = {
train: Train;
Expand Down Expand Up @@ -132,7 +133,7 @@ const TrainMarker = ({
popupclose: () => setIsPopupOpen(false),
}}
>
<Popup className="train-map-popup" minWidth={280}>
<HoverPopup className="train-map-popup" minWidth={280}>
{isPopupOpen && (
<TrainText
train={train}
Expand All @@ -142,7 +143,7 @@ const TrainMarker = ({
stoppedSince={stoppedSince}
/>
)}
</Popup>
</HoverPopup>

<Tooltip
className="train-number-tooltip"
Expand Down
14 changes: 14 additions & 0 deletions packages/map/components/PassengerStations.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React, { type FC } from "react";

import { PassengerStationMarker } from "@/components/Markers/PassengerStationMarker";
import stationsJson from "@/components/stationsPassenger.json";

const PassengerStations: FC = () => (
<>
{stationsJson.map((station) => (
<PassengerStationMarker key={station.Name} station={station} />
))}
</>
);

export default React.memo(PassengerStations);
Loading
Loading