Skip to content

Commit

Permalink
FeaturePanel: display route paths on climbing=area (#502)
Browse files Browse the repository at this point in the history
zbycz authored Sep 2, 2024

Verified

This commit was signed with the committer’s verified signature.
dhontecillas David Hontecillas
1 parent b8fd587 commit bc900e1
Showing 7 changed files with 151 additions and 118 deletions.
4 changes: 2 additions & 2 deletions pages/api/og-image.tsx
Original file line number Diff line number Diff line change
@@ -76,8 +76,8 @@ const renderSvg = async (
export default async (req: NextApiRequest, res: NextApiResponse) => {
const t1 = Date.now();
try {
const shortId = getApiId(req.query.id);
const feature = await fetchWithMemberFeatures(shortId);
const osmId = getApiId(req.query.id);
const feature = await fetchWithMemberFeatures(osmId);
const def = feature.imageDefs?.[0]; // TODO iterate when first not found
if (!def) {
throw new Error('No image definition found');
156 changes: 71 additions & 85 deletions src/components/FeaturePanel/CragsInArea.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import styled from '@emotion/styled';
import { Box, useTheme } from '@mui/material';
import { Box } from '@mui/material';
import React from 'react';
import Router from 'next/router';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import { useFeatureContext } from '../utils/FeatureContext';
import { getOsmappLink, getUrlOsmId } from '../../services/helpers';
import { Feature } from '../../services/types';
import { Feature, isInstant, OsmId } from '../../services/types';
import { useMobileMode } from '../helpers';
import { getWikimediaCommonsKeys } from './Climbing/utils/photo';
import { useScrollShadow } from './Climbing/utils/useScrollShadow';
import { getLabel } from '../../helpers/featureLabel';

import { getCommonsImageUrl } from '../../services/images/getCommonsImageUrl';
import { Slider, Wrapper } from './ImagePane/FeatureImages';
import { Image } from './ImagePane/Image/Image';
import { getInstantImage } from '../../services/images/getImageDefs';

const ArrowIcon = styled(ArrowForwardIosIcon)`
opacity: 0.2;
@@ -21,6 +21,7 @@ const HeadingRow = styled.div`
display: flex;
flex-direction: row;
align-items: center;
padding: 0 12px;
`;
const Container = styled.div`
overflow: auto;
@@ -30,7 +31,7 @@ const Container = styled.div`
justify-content: space-between;
cursor: pointer;
border-radius: 8px;
padding: 12px;
padding: 12px 0;
background-color: ${({ theme }) => theme.palette.background.elevation};
&:hover {
${ArrowIcon} {
@@ -44,12 +45,13 @@ const CragList = styled.div`
flex-direction: column;
gap: 12px;
`;
const Anchor = styled.a`
const Link = styled.a`
text-decoration: none !important;
`;
const Content = styled.div`
flex: 1;
`;

const CragName = styled.div`
padding: 0;
font-weight: 900;
@@ -64,108 +66,92 @@ const NumberOfRoutes = styled.div`
font-size: 13px;
color: ${({ theme }) => theme.palette.secondary.main};
`;
const Gallery = styled.div`
display: flex;
gap: 8px;
border-radius: 8px;
overflow: auto;
margin-top: 12px;
`;
const Image = styled.img`
border-radius: 8px;
height: 200px;
flex: 1;
object-fit: cover;
`;
const CragItem = ({ feature }: { feature: Feature }) => {
const theme: any = useTheme();
const Header = ({
imagesCount,
label,
routesCount,
}: {
label: string;
routesCount: number;
imagesCount: number;
}) => (
<HeadingRow>
<Content>
<CragName>{label}</CragName>{' '}
<Attributes>
{routesCount > 0 && (
<NumberOfRoutes>{routesCount} routes </NumberOfRoutes>
)}
{imagesCount > 0 && (
<NumberOfRoutes>{imagesCount} photos</NumberOfRoutes>
)}
</Attributes>
</Content>
<ArrowIcon color="primary" />
</HeadingRow>
);

const Gallery = ({ images }) => {
return (
<Wrapper>
<Slider>
{images.map((item) => (
<Image key={item.image.imageUrl} def={item.def} image={item.image} />
))}
</Slider>
</Wrapper>
);
};

const getOnClickWithHash = (apiId: OsmId) => (e) => {
e.preventDefault();
Router.push(`/${getUrlOsmId(apiId)}${window.location.hash}`);
};

const CragItem = ({ feature }: { feature: Feature }) => {
const mobileMode = useMobileMode();
const { setPreview } = useFeatureContext();
const { osmMeta } = feature;
const handleClick = (e) => {
e.preventDefault();
setPreview(null);
Router.push(`/${getUrlOsmId(osmMeta)}${window.location.hash}`);
};
const handleHover = () => feature.center && setPreview(feature);

const cragPhotoKeys = getWikimediaCommonsKeys(feature.tags);
const images =
feature?.imageDefs?.filter(isInstant)?.map((def) => ({
def,
image: getInstantImage(def),
})) ?? [];

const {
scrollElementRef,
onScroll,
ShadowContainer,
ShadowLeft,
ShadowRight,
} = useScrollShadow();
return (
<Anchor
href={`/${getUrlOsmId(osmMeta)}`}
onClick={handleClick}
<Link
href={`/${getUrlOsmId(feature.osmMeta)}`}
onClick={getOnClickWithHash(feature.osmMeta)}
onMouseEnter={mobileMode ? undefined : handleHover}
onMouseLeave={() => setPreview(null)}
>
<Container>
<HeadingRow>
<Content>
<CragName>{getLabel(feature)}</CragName>{' '}
<Attributes>
{feature.members?.length > 0 && (
<NumberOfRoutes>
{feature.members.length} routes{' '}
</NumberOfRoutes>
)}
{cragPhotoKeys.length > 0 && (
<NumberOfRoutes>{cragPhotoKeys.length} photos </NumberOfRoutes>
)}
</Attributes>
</Content>
<ArrowIcon color="primary" />
</HeadingRow>
{cragPhotoKeys.length > 0 && (
<ShadowContainer>
<ShadowLeft
backgroundColor={theme.palette.background.elevation}
gradientPercentage={7}
opacity={0.9}
/>
<Gallery onScroll={onScroll} ref={scrollElementRef}>
{cragPhotoKeys.map((cragPhotoTag) => {
const photoPath = feature.tags[cragPhotoTag];
const url = getCommonsImageUrl(photoPath, 410);
return <Image src={url} key={cragPhotoTag} />;
})}
</Gallery>
<ShadowRight
backgroundColor={theme.palette.background.elevation}
gradientPercentage={7}
opacity={0.9}
/>
</ShadowContainer>
)}
<Header
label={getLabel(feature)}
routesCount={feature.members?.length}
imagesCount={images.length}
/>
{images.length ? <Gallery images={images} /> : null}
</Container>
</Anchor>
</Link>
);
};

export const CragsInArea = () => {
const {
feature: { memberFeatures, tags },
} = useFeatureContext();
const { feature } = useFeatureContext();

if (!memberFeatures?.length) {
if (!feature.memberFeatures?.length) {
return null;
}

const isClimbingArea = tags.climbing === 'area';
if (!isClimbingArea) {
if (feature.tags.climbing !== 'area') {
return null;
}

return (
<Box mt={4}>
<CragList>
{memberFeatures.map((item) => (
{feature.memberFeatures.map((item) => (
<CragItem key={getOsmappLink(item)} feature={item} />
))}
</CragList>
5 changes: 4 additions & 1 deletion src/components/FeaturePanel/FeaturePanel.tsx
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ import { RouteDistributionInPanel } from './Climbing/RouteDistribution';
import { RouteListInPanel } from './Climbing/RouteList/RouteList';
import { FeaturePanelFooter } from './FeaturePanelFooter';
import { ClimbingRouteGrade } from './ClimbingRouteGrade';
import { Box } from '@mui/material';

const Flex = styled.div`
flex: 1;
@@ -61,7 +62,9 @@ export const FeaturePanel = () => {
<CragsInArea />
</PanelSidePadding>

<FeatureImages />
<Box mb={2}>
<FeatureImages />
</Box>
<RouteDistributionInPanel />
<RouteListInPanel />

6 changes: 2 additions & 4 deletions src/components/FeaturePanel/ImagePane/FeatureImages.tsx
Original file line number Diff line number Diff line change
@@ -6,12 +6,10 @@ import { useLoadImages } from './useLoadImages';
import { NoImage } from './NoImage';
import { HEIGHT, ImageSkeleton } from './helpers';

const Wrapper = styled.div`
export const Wrapper = styled.div`
width: 100%;
height: calc(${HEIGHT}px + 10px); // 10px for scrollbar
min-height: calc(${HEIGHT}px + 10px); // otherwise it shrinks b/c of flex
margin-bottom: 16px;
`;

const StyledScrollbars = styled(Scrollbars)`
@@ -25,7 +23,7 @@ const StyledScrollbars = styled(Scrollbars)`
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
`;
const Slider = ({ children }) => (
export const Slider = ({ children }) => (
<StyledScrollbars universal autoHide>
{children}
</StyledScrollbars>
1 change: 1 addition & 0 deletions src/components/Map/behaviour/useInitMap.tsx
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ const filterConsoleLog = () => {
// eslint-disable-next-line no-console
console.warn = (message, ...optionalParams) => {
if (
typeof message === 'string' &&
!message.includes(
'Please make sure you have added the image with map.addImage',
)
2 changes: 0 additions & 2 deletions src/components/Map/behaviour/useUpdateStyle.tsx
Original file line number Diff line number Diff line change
@@ -81,9 +81,7 @@ export const useUpdateStyle = createMapEffectHook(

const style = cloneDeep(getBaseStyle(key));
addOverlaysToStyle(map, style, overlays);
console.log('style', map.loaded(), map.getStyle()); // eslint-disable-line no-console
map.setStyle(style, { diff: map.loaded() });
console.log('style2', map.loaded(), map.getStyle()); // eslint-disable-line no-console

setUpHover(map, layersWithOsmId(style));
},
95 changes: 71 additions & 24 deletions src/services/osmApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { resolveCountryCode } from 'next-codegrid';
import { FetchError, getApiId, getShortId, getUrlOsmId, prod } from './helpers';
import { FetchError, getShortId, getUrlOsmId, prod } from './helpers';
import { fetchJson } from './fetch';
import { Feature, LonLat, OsmId, Position, SuccessInfo } from './types';
import { removeFetchCache } from './fetchCache';
@@ -12,6 +12,7 @@ import { getImageDefs, mergeMemberImageDefs } from './images/getImageDefs';
import * as Sentry from '@sentry/nextjs';
import { fetchOverpassCenter } from './overpass/fetchOverpassCenter';
import { isClimbingRelation, isClimbingRoute } from '../utils';
import { getOverpassUrl } from './overpassSearch';

const getOsmUrl = ({ type, id }) =>
`https://api.openstreetmap.org/api/0.6/${type}/${id}.json`;
@@ -114,37 +115,82 @@ const getItemsMap = (elements) => {
return map;
};

const getMemberFeatures = (members: Feature['members'], map) => {
return (
members
?.map(({ type, ref, role }) => {
const element = map[type][ref];
if (!element) {
return null;
}

const feature = addSchemaToFeature(osmToFeature(element));
feature.osmMeta.role = role;
feature.center = element.center
? [element.center.lon, element.center.lat] // from overpass "out center"
: undefined;
return feature;
})
.filter(Boolean) ?? []
);
};

export const fetchWithMemberFeatures = async (apiId: OsmId) => {
// TODO we can compute geometry using cragsToGeojson() and display it in the map
const url =
apiId.type === 'relation' ? getOsmFullUrl(apiId) : getOsmUrl(apiId);
const full = await fetchJson(url);
if (apiId.type !== 'relation') {
const wayOrNode = await fetchJson(getOsmUrl(apiId));
return addSchemaToFeature(osmToFeature(wayOrNode));
}

const full = await fetchJson(getOsmFullUrl(apiId));
const map = getItemsMap(full.elements);
const mainFeature = map[apiId.type][apiId.id];
const relation = map.relation[apiId.id];

const out: Feature = {
...addSchemaToFeature(osmToFeature(relation)),
memberFeatures: getMemberFeatures(relation.members, map),
};
mergeMemberImageDefs(out);
return out;
};

const addMemberFeaturesToArea = async (relation: Feature) => {
const { tags, osmMeta } = relation;
const url = getOverpassUrl(`[out:json];rel(${osmMeta.id});>>;out center qt;`);
const overpass = await fetchJson(url);
const itemsMap = getItemsMap(overpass.elements);
const memberFeatures = getMemberFeatures(relation.members, itemsMap).map(
(memberFeature) => {
const crag: Feature = {
...memberFeature,
memberFeatures: getMemberFeatures(memberFeature.members, itemsMap),
};
mergeMemberImageDefs(crag);
return crag;
},
);

return { ...relation, memberFeatures };
};

const addMemberFeaturesToRelation = async (relation: Feature) => {
const { tags, osmMeta: apiId } = relation;
if (apiId.type !== 'relation') {
return addSchemaToFeature(osmToFeature(mainFeature));
throw new Error('addMemberFeaturesToRelation() called with non-relation');
}

const memberFeatures =
mainFeature.members.map(({ type, ref, role }) => {
const element = map[type][ref];
if (!element) {
return null;
}
if (tags.climbing === 'area') {
return await addMemberFeaturesToArea(relation);
}

const feature = addSchemaToFeature(osmToFeature(element));
feature.osmMeta.role = role;
return feature;
}) ?? [];
const full = await fetchJson(getOsmFullUrl(apiId));
const map = getItemsMap(full.elements);

const featureWithMemberFeatures = {
...addSchemaToFeature(osmToFeature(mainFeature)),
memberFeatures: memberFeatures.filter(Boolean),
const out: Feature = {
...relation,
memberFeatures: getMemberFeatures(relation.members, map),
};
mergeMemberImageDefs(featureWithMemberFeatures); // TODO test + only for crag

return featureWithMemberFeatures;
mergeMemberImageDefs(out);
return out;
};

// TODO parent should be probably fetched for every feaure in fetchFeatureWithCenter()
@@ -162,8 +208,9 @@ export const addMembersAndParents = async (
if (isClimbingRelation(feature)) {
const [parentFeatures, featureWithMemberFeatures] = await Promise.all([
fetchParentFeatures(feature.osmMeta),
fetchWithMemberFeatures(feature.osmMeta),
addMemberFeaturesToRelation(feature),
]);

return {
...featureWithMemberFeatures,
center: feature.center, // feature contains correct center from centerCache or overpass

0 comments on commit bc900e1

Please sign in to comment.