diff --git a/src/components/FeaturePanel/Climbing/CameraMarker.tsx b/src/components/FeaturePanel/Climbing/CameraMarker.tsx
new file mode 100644
index 00000000..e2e11ed6
--- /dev/null
+++ b/src/components/FeaturePanel/Climbing/CameraMarker.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import styled from '@emotion/styled';
+
+type CameraTopDownMarkerProps = {
+ width?: number;
+ height?: number;
+ index?: number;
+ azimuth?: number; // Směr v úhlech, 0 = nahoru
+ onClick?: () => void;
+};
+
+const ARROW_SCALE = 2;
+
+export const CameraMarker = ({
+ width,
+ height,
+ azimuth = 0,
+ index,
+ onClick,
+}: CameraTopDownMarkerProps) => (
+
+);
diff --git a/src/components/FeaturePanel/Climbing/CragMap.tsx b/src/components/FeaturePanel/Climbing/CragMap.tsx
index 82f456e0..0725e261 100644
--- a/src/components/FeaturePanel/Climbing/CragMap.tsx
+++ b/src/components/FeaturePanel/Climbing/CragMap.tsx
@@ -1,11 +1,27 @@
-import React, { useCallback, useState } from 'react';
-import maplibregl, { GeoJSONSource } from 'maplibre-gl';
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+import maplibregl, { GeoJSONSource, LngLatLike, PointLike } from 'maplibre-gl';
import { outdoorStyle } from '../../Map/styles/outdoorStyle';
import { COMPASS_TOOLTIP } from '../../Map/useAddTopRightControls';
import styled from '@emotion/styled';
import { useFeatureContext } from '../../utils/FeatureContext';
import type { LayerSpecification } from '@maplibre/maplibre-gl-style-spec';
import { CircularProgress } from '@mui/material';
+import { useClimbingContext } from './contexts/ClimbingContext';
+import { addFilePrefix } from './utils/photo';
+import metadata from 'next/dist/server/typescript/rules/metadata';
+import { string } from 'prop-types';
+import ReactDOMServer from 'react-dom/server';
+import { AlphabeticalMarker } from '../../Directions/TextMarker';
+import { getGlobalMap } from '../../../services/mapStorage';
+import { CameraMarker } from './CameraMarker';
+import Router from 'next/router';
+import { getOsmappLink } from '../../../services/helpers';
const Map = styled.div<{ $isVisible: boolean }>`
visibility: ${({ $isVisible }) => ($isVisible ? 'visible' : 'hidden')};
@@ -75,11 +91,117 @@ export const routes: LayerSpecification[] = [
},
];
+const useGetPhotoExifs = (photoPaths) => {
+ const [photoExifs, setPhotoExifs] = useState<
+ Record>
+ >({});
+ useEffect(() => {
+ async function fetchExifData(photos) {
+ const encodedTitles = photos.map((t) => addFilePrefix(t)).join('|');
+ const url = `https://commons.wikimedia.org/w/api.php?action=query&prop=imageinfo&iiprop=metadata&titles=${encodedTitles}&format=json&origin=*`;
+ const response = await fetch(url);
+ const data = await response.json();
+ return data.query.pages;
+ }
+
+ fetchExifData(photoPaths).then((pages) => {
+ const data = Object.values(pages).reduce>(
+ (acc, item: any) => {
+ const metadata = item?.imageinfo?.[0]?.metadata.reduce(
+ (acc2, { name, value }) => ({ ...acc2, [name]: value }),
+ {},
+ );
+
+ return {
+ ...acc,
+ [item.title]: metadata,
+ };
+ },
+ {},
+ );
+ setPhotoExifs(data);
+ });
+ }, [photoPaths]);
+ return photoExifs;
+};
+
+function parseFractionOrNumber(input) {
+ if (input.includes('/')) {
+ const [numerator, denominator] = input.split('/');
+ return parseFloat(numerator) / parseFloat(denominator);
+ } else {
+ return parseFloat(input);
+ }
+}
+
+const usePhotoMarkers = (photoExifs, mapRef) => {
+ const { feature } = useFeatureContext();
+ const getMarker = (index: number, azimuth: number | null) => {
+ let svgElement;
+ const photoPath = Object.keys(photoExifs)[index];
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
+ svgElement = document.createElement('div');
+ svgElement.innerHTML = ReactDOMServer.renderToStaticMarkup(
+ {
+ console.log('___TU');
+ Router.push(
+ `${getOsmappLink(feature)}/climbing/photo/${photoPath}${window.location.hash}`,
+ );
+ }}
+ />,
+ );
+ } else svgElement = undefined;
+
+ return {
+ color: 'salmon',
+ element: svgElement,
+ offset: [0, -10] as PointLike,
+ };
+ };
+
+ const markerRef = useRef();
+
+ useEffect(() => {
+ Object.keys(photoExifs).map((key, index) => {
+ const exifItems = photoExifs[key];
+
+ if (exifItems && exifItems.GPSLongitude && exifItems.GPSLatitude) {
+ const marker = getMarker(
+ index,
+ exifItems.GPSImgDirection
+ ? parseFractionOrNumber(exifItems.GPSImgDirection)
+ : null,
+ );
+ markerRef.current = new maplibregl.Marker(marker)
+ .setLngLat([
+ exifItems.GPSLongitude,
+ exifItems.GPSLatitude,
+ ] as LngLatLike)
+ .addTo(mapRef.current);
+ }
+ });
+
+ return () => {
+ Object.keys(photoExifs).map((key) => {
+ markerRef.current?.remove();
+ });
+ };
+ }, [mapRef, markerRef, photoExifs]);
+};
+
const useInitMap = () => {
const containerRef = React.useRef(null);
const mapRef = React.useRef(null);
const [isMapLoaded, setIsMapLoaded] = useState(false);
const { feature } = useFeatureContext();
+ const { photoPaths } = useClimbingContext();
+
+ const photoExifs = useGetPhotoExifs(photoPaths);
+ usePhotoMarkers(photoExifs, mapRef);
const getClimbingSource = useCallback(
() => mapRef.current.getSource('climbing') as GeoJSONSource | undefined,
diff --git a/src/components/FeaturePanel/Climbing/utils/photo.ts b/src/components/FeaturePanel/Climbing/utils/photo.ts
index 85f392cd..470d6aef 100644
--- a/src/components/FeaturePanel/Climbing/utils/photo.ts
+++ b/src/components/FeaturePanel/Climbing/utils/photo.ts
@@ -7,6 +7,8 @@ import { naturalSort } from './array';
export const getWikimediaCommonsKey = (index: number) =>
`wikimedia_commons${index === 0 ? '' : `:${index + 1}`}`;
+export const addFilePrefix = (name: string) => `File:${name}`;
+
export const removeFilePrefix = (name: string) => name?.replace(/^File:/, '');
export const isWikimediaCommons = (tag: string) =>