Skip to content

Commit

Permalink
CragMap: Add photo locations
Browse files Browse the repository at this point in the history
  • Loading branch information
jvaclavik committed Dec 18, 2024
1 parent cc853c2 commit d52c560
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 4 deletions.
46 changes: 46 additions & 0 deletions src/components/FeaturePanel/Climbing/CameraMarker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react';
import styled from '@emotion/styled';

type CameraTopDownMarkerProps = {
width?: number;
height?: number;
index?: number;
azimuth?: number;
onClick?: () => void;
};

const ARROW_SCALE = 2;

export const CameraMarker = ({
width,
height,
azimuth = 0,
index,
onClick,
}: CameraTopDownMarkerProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 55 55"
role="img"
height={height}
width={width}
>
<g transform="translate(11.5,11.5)" cursor="pointer" onClick={onClick}>
<g
transform={`rotate(${azimuth},16,16) translate(16,16) scale(${ARROW_SCALE}) translate(-16,-16)`}
>
<polygon points="16,2 10,14 22,14" fill="white" />
</g>

<path
fill="#000"
d="M26 7h-3.465l-1.704-2.555A1 1 0 0 0 20 4h-8a1 1 0 0 0-.831.445L9.464 7H6a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h20a3 3 0 0 0 3-3V10a3 3 0 0 0-3-3Z"
/>

<text fill="white" x={12} y={23} fontWeight="bold">
{index}
</text>
</g>
</svg>
);
17 changes: 16 additions & 1 deletion src/components/FeaturePanel/Climbing/ClimbingViewContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useClimbingContext } from './contexts/ClimbingContext';
import { invertedBoltCodeMap } from './utils/boltCodes';
import { RouteList } from './RouteList/RouteList';
import { ContentContainer } from './ContentContainer';
import { Button, ButtonGroup } from '@mui/material';
import { Button, ButtonGroup, Typography } from '@mui/material';

import { RouteDistribution } from './RouteDistribution';
import React from 'react';
Expand All @@ -17,6 +17,9 @@ import { EditButton } from '../EditButton';
import { EditDialog } from '../EditDialog/EditDialog';
import { useGetCragViewLayout } from './utils/useCragViewLayout';
import { useUserSettingsContext } from '../../utils/UserSettingsContext';
import { useFeatureContext } from '../../utils/FeatureContext';
import { PanelLabel } from './PanelLabel';
import { t } from '../../../services/intl';

const CragMapDynamic = dynamic(() => import('./CragMap'), {
ssr: false,
Expand All @@ -40,6 +43,7 @@ const ContentBelowRouteList = styled.div<{

export const ClimbingViewContent = ({ isMapVisible }) => {
const { showDebugMenu, routes } = useClimbingContext();
const { feature } = useFeatureContext();
const cragViewLayout = useGetCragViewLayout();
const { userSettings } = useUserSettingsContext();
const splitPaneSize = userSettings['climbing.splitPaneSize'];
Expand Down Expand Up @@ -90,6 +94,17 @@ export const ClimbingViewContent = ({ isMapVisible }) => {
)}
</ContentContainer>
<RouteDistribution />

{feature.tags.description ? (
<>
<PanelLabel>{t('climbingview.description')}</PanelLabel>

<Typography ml={4.5} mr={4.5}>
{feature.tags.description}
</Typography>
</>
) : null}

<EditButton />
<EditDialog />
</ContentBelowRouteList>
Expand Down
119 changes: 117 additions & 2 deletions src/components/FeaturePanel/Climbing/CragMap.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import React, { useCallback, useState } from 'react';
import maplibregl, { GeoJSONSource } from 'maplibre-gl';
import React, { useCallback, useEffect, 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 ReactDOMServer from 'react-dom/server';
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')};
Expand Down Expand Up @@ -75,11 +81,120 @@ export const routes: LayerSpecification[] = [
},
];

const useGetPhotoExifs = (photoPaths) => {
const [photoExifs, setPhotoExifs] = useState<
Record<string, Record<string, any>>
>({});
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<Record<string, any>>(
(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 = useCallback(
(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(
<CameraMarker
width={30}
index={index}
azimuth={azimuth}
onClick={() => {
Router.push(
`${getOsmappLink(feature)}/climbing/photo/${photoPath}${window.location.hash}`,
);
}}
/>,
);
} else svgElement = undefined;

return {
color: 'salmon',
element: svgElement,
offset: [0, -10] as PointLike,
};
},
[feature, photoExifs],
);

const markerRef = useRef<maplibregl.Marker>();

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();
});
};
}, [getMarker, mapRef, markerRef, photoExifs]);
};

const useInitMap = () => {
const containerRef = React.useRef(null);
const mapRef = React.useRef<maplibregl.Map>(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,
Expand Down
2 changes: 1 addition & 1 deletion src/components/FeaturePanel/Climbing/RouteDistribution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const Chart = styled.div<{ $ratio: number; $color: string }>`
const getGroupingLabel = (label: string) => String(parseFloat(label));

export const RouteDistribution = () => {
const { userSettings, setUserSetting } = useUserSettingsContext();
const { userSettings } = useUserSettingsContext();
const gradeSystem = userSettings['climbing.gradeSystem'] || 'uiaa';

const theme = useTheme();
Expand Down
2 changes: 2 additions & 0 deletions src/components/FeaturePanel/Climbing/utils/photo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
1 change: 1 addition & 0 deletions src/locales/vocabulary.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ export default {
'climbingpanel.delete_climbing_route': 'Delete route __route__ in schema',
'climbingpanel.create_first_node': 'Click on the beginning of the route and continue in the direction of the route',
'climbingpanel.create_next_node': 'Follow direction of the route and click "Done" when finished',
'climbingview.description': 'Description',

'publictransport.tourism': 'Touristic trains',
'publictransport.night': 'Night trains',
Expand Down

0 comments on commit d52c560

Please sign in to comment.