2025-09-03 23:17:35 +03:00
|
|
|
import Map from "./map";
|
|
|
|
|
import "./index.css";
|
2025-09-03 23:35:29 +03:00
|
|
|
import { ViewModeProps } from "../interface";
|
2025-09-04 16:47:38 +03:00
|
|
|
import { useNoteLabel, useNoteLabelBoolean, useNoteProperty, useSpacedUpdate } from "../../react/hooks";
|
2025-09-03 23:35:29 +03:00
|
|
|
import { DEFAULT_MAP_LAYER_NAME } from "./map_layer";
|
2025-09-04 16:17:27 +03:00
|
|
|
import { divIcon, LatLng } from "leaflet";
|
2025-09-04 16:24:01 +03:00
|
|
|
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
|
2025-09-04 15:58:50 +03:00
|
|
|
import Marker from "./marker";
|
2025-09-04 15:47:56 +03:00
|
|
|
import froca from "../../../services/froca";
|
2025-09-04 15:58:50 +03:00
|
|
|
import FNote from "../../../entities/fnote";
|
2025-09-04 16:17:27 +03:00
|
|
|
import markerIcon from "leaflet/dist/images/marker-icon.png";
|
|
|
|
|
import markerIconShadow from "leaflet/dist/images/marker-shadow.png";
|
2025-09-04 16:24:01 +03:00
|
|
|
import appContext from "../../../components/app_context";
|
2025-09-04 16:44:35 +03:00
|
|
|
import { moveMarker } from "./api";
|
2025-09-03 23:17:35 +03:00
|
|
|
|
|
|
|
|
const DEFAULT_COORDINATES: [number, number] = [3.878638227135724, 446.6630455551659];
|
|
|
|
|
const DEFAULT_ZOOM = 2;
|
2025-09-04 15:47:56 +03:00
|
|
|
export const LOCATION_ATTRIBUTE = "geolocation";
|
2025-09-03 23:17:35 +03:00
|
|
|
|
2025-09-03 23:57:38 +03:00
|
|
|
interface MapData {
|
|
|
|
|
view?: {
|
|
|
|
|
center?: LatLng | [number, number];
|
|
|
|
|
zoom?: number;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 15:47:56 +03:00
|
|
|
export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewModeProps<MapData>) {
|
2025-09-03 23:35:29 +03:00
|
|
|
const [ layerName ] = useNoteLabel(note, "map:style");
|
2025-09-04 16:47:38 +03:00
|
|
|
const [ isReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
2025-09-04 15:58:50 +03:00
|
|
|
const [ notes, setNotes ] = useState<FNote[]>([]);
|
2025-09-04 14:26:29 +03:00
|
|
|
const spacedUpdate = useSpacedUpdate(() => {
|
2025-09-04 15:13:48 +03:00
|
|
|
if (viewConfig) {
|
|
|
|
|
saveConfig(viewConfig);
|
|
|
|
|
}
|
2025-09-04 14:26:29 +03:00
|
|
|
}, 5000);
|
|
|
|
|
|
2025-09-04 15:58:50 +03:00
|
|
|
useEffect(() => { froca.getNotes(noteIds).then(setNotes) }, [ noteIds ]);
|
2025-09-04 15:47:56 +03:00
|
|
|
|
2025-09-03 23:17:35 +03:00
|
|
|
return (
|
|
|
|
|
<div className="geo-view">
|
|
|
|
|
<Map
|
2025-09-04 15:13:48 +03:00
|
|
|
coordinates={viewConfig?.view?.center ?? DEFAULT_COORDINATES}
|
|
|
|
|
zoom={viewConfig?.view?.zoom ?? DEFAULT_ZOOM}
|
2025-09-03 23:35:29 +03:00
|
|
|
layerName={layerName ?? DEFAULT_MAP_LAYER_NAME}
|
2025-09-04 14:26:29 +03:00
|
|
|
viewportChanged={(coordinates, zoom) => {
|
2025-09-04 15:13:48 +03:00
|
|
|
if (!viewConfig) viewConfig = {};
|
|
|
|
|
viewConfig.view = { center: coordinates, zoom };
|
2025-09-04 14:26:29 +03:00
|
|
|
spacedUpdate.scheduleUpdate();
|
|
|
|
|
}}
|
2025-09-04 15:47:56 +03:00
|
|
|
>
|
2025-09-04 16:44:35 +03:00
|
|
|
{notes.map(note => <NoteMarker note={note} editable={!isReadOnly} />)}
|
2025-09-04 15:47:56 +03:00
|
|
|
</Map>
|
2025-09-03 23:17:35 +03:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-09-04 15:58:50 +03:00
|
|
|
|
2025-09-04 16:44:35 +03:00
|
|
|
function NoteMarker({ note, editable }: { note: FNote, editable: boolean }) {
|
2025-09-04 15:58:50 +03:00
|
|
|
const [ location ] = useNoteLabel(note, LOCATION_ATTRIBUTE);
|
2025-09-04 16:19:56 +03:00
|
|
|
|
|
|
|
|
// React to changes
|
|
|
|
|
useNoteLabel(note, "color");
|
|
|
|
|
useNoteLabel(note, "iconClass");
|
|
|
|
|
|
2025-09-04 16:17:27 +03:00
|
|
|
const title = useNoteProperty(note, "title");
|
2025-09-04 16:19:56 +03:00
|
|
|
const colorClass = note.getColorClass();
|
2025-09-04 16:17:27 +03:00
|
|
|
const iconClass = note.getIcon();
|
2025-09-04 15:58:50 +03:00
|
|
|
const latLng = location?.split(",", 2).map((el) => parseFloat(el)) as [ number, number ] | undefined;
|
2025-09-04 16:17:27 +03:00
|
|
|
const icon = useMemo(() => buildIcon(iconClass, colorClass ?? undefined, title, note.noteId), [ iconClass, colorClass, title, note.noteId]);
|
2025-09-04 15:58:50 +03:00
|
|
|
|
2025-09-04 16:44:35 +03:00
|
|
|
// Middle click to open in new tab
|
|
|
|
|
const onMouseDown = useCallback((e: MouseEvent) => {
|
|
|
|
|
if (e.button === 1) {
|
|
|
|
|
const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId;
|
|
|
|
|
appContext.tabManager.openInNewTab(note.noteId, hoistedNoteId);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}, [ note.noteId ]);
|
|
|
|
|
|
|
|
|
|
const onDragged = useCallback((newCoordinates: LatLng) => {
|
|
|
|
|
moveMarker(note.noteId, newCoordinates);
|
|
|
|
|
}, [ note.noteId ]);
|
|
|
|
|
|
2025-09-04 16:17:27 +03:00
|
|
|
return latLng && <Marker
|
|
|
|
|
coordinates={latLng}
|
|
|
|
|
icon={icon}
|
2025-09-04 16:44:35 +03:00
|
|
|
mouseDown={onMouseDown}
|
|
|
|
|
draggable={editable}
|
2025-09-04 16:47:38 +03:00
|
|
|
dragged={editable ? onDragged : undefined}
|
2025-09-04 16:17:27 +03:00
|
|
|
/>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildIcon(bxIconClass: string, colorClass?: string, title?: string, noteIdLink?: string) {
|
|
|
|
|
let html = /*html*/`\
|
|
|
|
|
<img class="icon" src="${markerIcon}" />
|
|
|
|
|
<img class="icon-shadow" src="${markerIconShadow}" />
|
|
|
|
|
<span class="bx ${bxIconClass} ${colorClass ?? ""}"></span>
|
|
|
|
|
<span class="title-label">${title ?? ""}</span>`;
|
|
|
|
|
|
|
|
|
|
if (noteIdLink) {
|
|
|
|
|
html = `<div data-href="#root/${noteIdLink}">${html}</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return divIcon({
|
|
|
|
|
html,
|
|
|
|
|
iconSize: [25, 41],
|
|
|
|
|
iconAnchor: [12, 41]
|
|
|
|
|
});
|
2025-09-04 15:58:50 +03:00
|
|
|
}
|