From fa4d465c391d5ccf92559f257881a6a6f7ee2c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Zbytovsk=C3=BD?= Date: Fri, 19 Jan 2024 09:38:45 +0100 Subject: [PATCH] MemberFeatures: show all members for some relations (#228) --- src/components/FeaturePanel/FeaturePanel.tsx | 2 + .../ImageSection/PoiDescription.tsx | 9 +-- .../FeaturePanel/MemberFeatures.tsx | 73 +++++++++++++++++++ .../Map/behaviour/useFeatureMarker.tsx | 2 +- src/helpers/featureLabel.ts | 7 +- src/services/osmApi.ts | 57 ++++++++++++--- src/services/types.ts | 1 + 7 files changed, 128 insertions(+), 23 deletions(-) create mode 100644 src/components/FeaturePanel/MemberFeatures.tsx diff --git a/src/components/FeaturePanel/FeaturePanel.tsx b/src/components/FeaturePanel/FeaturePanel.tsx index 15f5af01a..b2320e1b3 100644 --- a/src/components/FeaturePanel/FeaturePanel.tsx +++ b/src/components/FeaturePanel/FeaturePanel.tsx @@ -21,6 +21,7 @@ import { getLabel } from '../../helpers/featureLabel'; import { ImageSection } from './ImageSection/ImageSection'; import { PublicTransport } from './PublicTransport/PublicTransport'; import { Properties } from './Properties/Properties'; +import { MemberFeatures } from './MemberFeatures'; export const FeaturePanel = () => { const { feature } = useFeatureContext(); @@ -56,6 +57,7 @@ export const FeaturePanel = () => { key={getUrlOsmId(osmMeta) + (deleted && 'del')} /> + {advanced && } diff --git a/src/components/FeaturePanel/ImageSection/PoiDescription.tsx b/src/components/FeaturePanel/ImageSection/PoiDescription.tsx index 2feeab9b3..48ff68087 100644 --- a/src/components/FeaturePanel/ImageSection/PoiDescription.tsx +++ b/src/components/FeaturePanel/ImageSection/PoiDescription.tsx @@ -1,10 +1,9 @@ import React from 'react'; import styled from 'styled-components'; -import { hasName } from '../../../helpers/featureLabel'; +import { getSubclass, hasName } from '../../../helpers/featureLabel'; import { useFeatureContext } from '../../utils/FeatureContext'; import { t } from '../../../services/intl'; import Maki from '../../utils/Maki'; -import { Feature } from '../../../services/types'; const PoiType = styled.div` color: #fff; @@ -23,12 +22,6 @@ const PoiType = styled.div` } `; -const getSubclass = ({ layer, osmMeta, properties, schema }: Feature) => - schema?.label || - properties.subclass?.replace(/_/g, ' ') || - (layer && layer.id) || // layer.id specified only when maplibre-gl skeleton displayed - osmMeta.type; - export const PoiDescription = () => { const { feature } = useFeatureContext(); const { properties } = feature; diff --git a/src/components/FeaturePanel/MemberFeatures.tsx b/src/components/FeaturePanel/MemberFeatures.tsx new file mode 100644 index 000000000..728109a3b --- /dev/null +++ b/src/components/FeaturePanel/MemberFeatures.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { Box, Typography } from '@material-ui/core'; +import Router from 'next/router'; +import { getOsmappLink, getUrlOsmId } from '../../services/helpers'; +import { useFeatureContext } from '../utils/FeatureContext'; +import { Feature } from '../../services/types'; +import { getLabel } from '../../helpers/featureLabel'; +import { useUserThemeContext } from '../../helpers/theme'; +import { useMobileMode } from '../helpers'; +import Maki from '../utils/Maki'; + +const Item = ({ feature }: { feature: Feature }) => { + const { currentTheme } = useUserThemeContext(); + const mobileMode = useMobileMode(); + const { setPreview } = useFeatureContext(); + const { properties, tags, osmMeta } = feature; + const handleClick = (e) => { + e.preventDefault(); + setPreview(null); + Router.push(`/${getUrlOsmId(osmMeta)}${window.location.hash}`); + }; + const handleHover = () => + feature.center && setPreview({ ...feature, noPreviewButton: true }); + + return ( +
  • + setPreview(null)} + > + + {getLabel(feature)} + +
  • + ); +}; + +export const MemberFeatures = () => { + const { + feature: { memberFeatures, tags }, + } = useFeatureContext(); + + if (!memberFeatures?.length) { + return null; + } + + const heading = + tags.climbing === 'crag' + ? 'Routes' + : tags.climbing === 'area' + ? 'Crags' + : 'Subitems'; + return ( + + + {heading} + +
      + {memberFeatures.map((item) => ( + + ))} +
    +
    + ); +}; diff --git a/src/components/Map/behaviour/useFeatureMarker.tsx b/src/components/Map/behaviour/useFeatureMarker.tsx index 0562aac5f..63a9bc1fc 100644 --- a/src/components/Map/behaviour/useFeatureMarker.tsx +++ b/src/components/Map/behaviour/useFeatureMarker.tsx @@ -59,7 +59,7 @@ export const useFeatureMarker = (map) => { useUpdateFeatureMarker(map, feature); useUpdatePreviewMarker(map, preview); - // hide the icon when tiles are fetched TODO sometimes broken (zoom problem) + // hide the icon when tiles are fetched TODO sometimes broken (zoom problem) (also maybe causes webgl blackout) useEffect(() => { if (map) { const handle = setInterval(() => { diff --git a/src/helpers/featureLabel.ts b/src/helpers/featureLabel.ts index 0115a938f..ea8861f66 100644 --- a/src/helpers/featureLabel.ts +++ b/src/helpers/featureLabel.ts @@ -1,8 +1,11 @@ import { Feature } from '../services/types'; import { roundedToDeg } from '../utils'; -const getSubclass = ({ properties, osmMeta, schema }: Feature) => - schema?.label || properties.subclass?.replace(/_/g, ' ') || osmMeta.type; +export const getSubclass = ({ layer, osmMeta, properties, schema }: Feature) => + schema?.label || + properties.subclass?.replace(/_/g, ' ') || + (layer && layer.id) || // layer.id specified only when maplibre-gl skeleton displayed + osmMeta.type; const getRef = (feature: Feature) => feature.tags.ref ? `${getSubclass(feature)} ${feature.tags.ref}` : ''; diff --git a/src/services/osmApi.ts b/src/services/osmApi.ts index 291490dff..fd85d10c1 100644 --- a/src/services/osmApi.ts +++ b/src/services/osmApi.ts @@ -1,4 +1,4 @@ -import { getApiId, getShortId, getUrlOsmId, prod } from './helpers'; +import { getApiId, getShortId, getUrlOsmId, OsmApiId, prod } from './helpers'; import { FetchError, fetchJson } from './fetch'; import { Feature, Position } from './types'; import { removeFetchCache } from './fetchCache'; @@ -97,6 +97,46 @@ const osmToFeature = (element): Feature => { }; }; +async function fetchFeatureWithCenter(apiId: OsmApiId) { + const [element, center] = await Promise.all([ + getOsmPromise(apiId), + getCenterPromise(apiId), + fetchSchemaTranslations(), // TODO this should be mocked in test??? could be moved to setIntl or something + ]); + + const feature = osmToFeature(element); + if (center) { + feature.center = center; + } + + return addSchemaToFeature(feature); +} + +const shouldFetchMembers = (feature: Feature) => + feature.osmMeta.type === 'relation' && + (feature.tags.climbing === 'crag' || feature.tags.climbing === 'area'); + +// TODO we can probably fetch full.json for all relations eg https://api.openstreetmap.org/api/0.6/relation/14334600/full.json - lets measure how long it takes for different sizes +export const addMemberFeatures = async (feature: Feature) => { + if (!shouldFetchMembers(feature)) { + return feature; + } + + const start = performance.now(); + + const apiIds = feature.members.map(({ type, ref }) => ({ type, id: ref })); + const promises = apiIds.map((apiId) => fetchFeatureWithCenter(apiId)); // TODO optimize n+1 center-requests or fetch full + const memberFeatures = await Promise.all(promises); + + const duration = Math.round(performance.now() - start); + console.log(`addMemberFeaturesToCrag took ${duration} ms`); // eslint-disable-line no-console + + return { + ...feature, + memberFeatures, + }; +}; + export const fetchFeature = async (shortId): Promise => { if (!shortId) { return null; @@ -104,24 +144,17 @@ export const fetchFeature = async (shortId): Promise => { try { const apiId = getApiId(shortId); - const [element, center] = await Promise.all([ - getOsmPromise(apiId), - getCenterPromise(apiId), - fetchSchemaTranslations(), // TODO this should be mocked in test??? could be moved to setIntl or something - ]); - - const feature = osmToFeature(element); - if (center) { - feature.center = center; - } + const withCenter = await fetchFeatureWithCenter(apiId); + const withMembers = await addMemberFeatures(withCenter); - return addSchemaToFeature(feature); + return withMembers; } catch (e) { console.error(`fetchFeature(${shortId}):`, e); // eslint-disable-line no-console const error = ( e instanceof FetchError ? e.code : 'unknown' ) as Feature['error']; + return { type: 'Feature', skeleton: true, diff --git a/src/services/types.ts b/src/services/types.ts index 14b2b9278..7aaca2a2f 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -75,6 +75,7 @@ export interface Feature { }; tags: FeatureTags; members?: RelationMember[]; + memberFeatures?: Feature[]; properties: { class: string; subclass: string;