Skip to content

Commit

Permalink
574 display images on map (#592)
Browse files Browse the repository at this point in the history
* add basic map with images for location tags to discover view

* cobine sub locations instead of not showing them

* fix saving tab selection in local storage

* prevent map from overlaying bottom bar on mobile

* add support for full size map

* reduce duplicate code, internationalization

* allow to zoom deeper into map

* store map zoom and center in history, to restore map state

* remove duplicate coordinates attribute in FlatTag interface

* refactoring

* try to fix tests

* try to fix last failing test

* requested changes

* custom resover stuff

Co-authored-by: MariusDoe <MariusDoe@users.noreply.github.com>

* fix fetching error

* fix something-went-wrong message being shown for a split second

* requested change remove unnecessary line of code

* fix loading of decades

* fix test

* fix tests

---------

Co-authored-by: MariusDoe <MariusDoe@users.noreply.github.com>
  • Loading branch information
LinoH5 and MariusDoe authored Jul 5, 2023
1 parent b4670a0 commit 46b4b1f
Show file tree
Hide file tree
Showing 22 changed files with 673 additions and 180 deletions.
5 changes: 3 additions & 2 deletions projects/bp-gallery/cypress/e2e/discover.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,15 @@ describe('Discover View', () => {
});

it('shows "Orte" tag overview', () => {
cy.get('.overview-selection-container:eq(2)').contains('Orte').click();
// check for basic components (title, show more button)
cy.get('.overview-container:eq(3)')
cy.get('.overview-selection-container:eq(2)')
.children()
.should('contain.text', 'Orte')
.and('contain.text', 'Mehr anzeigen');

// check if it contains first 6 verified locations
cy.get('.overview-container:eq(3) .overview-collection-grid-container .items')
cy.get('.overview-selection-container:eq(2) .overview-collection-grid-container .items')
.should('contain.text', 'VERIFIZIERTER TESTORT 1')
.and('contain.text', 'VERIFIZIERTER TESTORT 2')
.and('contain.text', 'VERIFIZIERTER TESTORT 3')
Expand Down
8 changes: 5 additions & 3 deletions projects/bp-gallery/cypress/e2e/show-more.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,20 @@ describe('Navigation to Show More View from Discover View', () => {
});

it('works for "Orte"', () => {
cy.get('.overview-container:contains(Orte)').contains('Mehr anzeigen').click();
cy.get('.overview-selection-container:eq(2)').contains('Orte').click();
cy.get('.overview-selection-container:contains(Orte)').contains('Mehr anzeigen').click();
urlIs('/show-more/location');
});

it('works for single locations', () => {
cy.get('.overview-selection-container:eq(2)').contains('Orte').click();
// IDs of the six locations shown in tag overview
const targetIDs = [7, 8, 9, 10, 11, 13];
const targetIDs = [7, 13, 10, 11, 9, 8];
// iterate over the six locations shown in tag overview
//for (const targetID of targetIDs) {
targetIDs.forEach((targetID, index) => {
cy.get(
`.overview-container:contains(Orte) .overview-collection-grid-container .items .item:eq(${index})`
`.overview-selection-container:contains(Orte) .overview-collection-grid-container .items .item:eq(${index})`
).click();
urlIs(`/show-more/location/${targetID}`);
cy.go(-1); // is a bit faster using cy.go(-1) instead of cy.visit('/discover)
Expand Down
3 changes: 3 additions & 0 deletions projects/bp-gallery/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@sentry/react": "^7.24.2",
"@tailwindcss/line-clamp": "^0.4.2",
"@types/apollo-upload-client": "^17.0.2",
"@types/leaflet.markercluster": "^1.5.1",
"apollo-upload-client": "^17.0.0",
"bp-graphql": "../bp-graphql",
"dayjs": "^1.11.7",
Expand All @@ -32,6 +33,7 @@
"isomorphic-dompurify": "^0.24.0",
"jodit-react": "^1.3.31",
"leaflet": "^1.9.3",
"leaflet.markercluster": "^1.5.3",
"lodash": "^4.17.21",
"meilisearch": "^0.33.0",
"nanoid": "^4.0.2",
Expand All @@ -46,6 +48,7 @@
"react-html-parser": "^2.0.2",
"react-i18next": "^11.13.0",
"react-leaflet": "^4.2.1",
"react-leaflet-cluster": "^2.1.0",
"react-perfect-scrollbar": "^1.5.8",
"react-router-config": "^5.1.1",
"react-router-dom": "^5.3.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { MobileContext } from '../provider/MobileProvider';
export interface OverviewContainerTab {
title: string;
icon: ReactElement<IconProps>;
desktopText?: string;
content: ReactElement;
}

Expand Down
71 changes: 71 additions & 0 deletions projects/bp-gallery/src/components/common/PictureMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Map } from 'leaflet';
import { useMemo, useRef, useState } from 'react';
import { useSimplifiedQueryResponseData } from '../../graphql/queryUtils';
import { useVisit } from '../../helpers/history';
import useGetTagsWithThumbnail, { NO_LIMIT } from '../../hooks/get-tags-with-thumbnail.hook';
import { TagType } from '../../types/additionalFlatTypes';
import { BAD_HARZBURG_COORDINATES } from '../views/location-curating/tag-structure-helpers';
import Loading from './Loading';
import PictureMapView, { ExtendedFlatTag } from './PictureMapView';
import QueryErrorDisplay from './QueryErrorDisplay';

const PictureMap = () => {
const { location } = useVisit();

const initialMapValues = useMemo(() => {
return location.state?.mapState ?? { center: BAD_HARZBURG_COORDINATES, zoom: 10 };
}, [location.state?.mapState]);

const [isMaximized, setIsMaximized] = useState<boolean>(location.state?.openMap ?? false);
const map = useRef<Map>(null);

const { data, loading, error } = useGetTagsWithThumbnail(
{},
{},
TagType.LOCATION,
['name:asc'],
NO_LIMIT,
'cache-first'
);

const flattened = useSimplifiedQueryResponseData(data);
const flattenedTags: ExtendedFlatTag[] | undefined = flattened
? Object.values(flattened)[0]
: undefined;

const localMap = useMemo(
() => (
<PictureMapView
isMaximized={isMaximized}
setIsMaximized={setIsMaximized}
initialMapValues={{
center: map.current?.getCenter() ?? initialMapValues.center,
zoom: map.current?.getZoom() ?? initialMapValues.zoom,
}}
locations={flattenedTags}
widthStyle='w-full'
heightStyle={isMaximized ? 'h-full' : 'h-[500px]'}
map={map}
/>
),
[flattenedTags, initialMapValues.center, initialMapValues.zoom, isMaximized]
);

if (error) {
return <QueryErrorDisplay error={error} />;
} else if (loading) {
return <Loading />;
} else {
return (
<>
{isMaximized ? (
<div className='w-full h-full overflow-hidden fixed left-0 top-0 z-[999]'>{localMap}</div>
) : (
localMap
)}
</>
);
}
};

export default PictureMap;
226 changes: 226 additions & 0 deletions projects/bp-gallery/src/components/common/PictureMapView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import { ZoomInMapOutlined, ZoomOutMapOutlined } from '@mui/icons-material';
import { DivIcon, LatLng, Map, MarkerCluster, MarkerOptions, Point } from 'leaflet';
import myMarkerIcon from 'leaflet/dist/images/marker-icon-2x.png';
import markerShadow from 'leaflet/dist/images/marker-shadow.png';
import { Dispatch, RefObject, SetStateAction } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { useTranslation } from 'react-i18next';
import { MapContainer, Marker, TileLayer } from 'react-leaflet';
import MarkerClusterGroup from 'react-leaflet-cluster';
import { PictureOrigin, asUploadPath } from '../../helpers/app-helpers';
import { useVisit } from '../../helpers/history';
import { FlatPicture, FlatTag, Thumbnail } from '../../types/additionalFlatTypes';
import { useGetDescendantsMatrix } from '../views/location-curating/tag-structure-helpers';

export interface ExtendedFlatTag extends FlatTag {
thumbnail: Thumbnail[];
pictures: FlatPicture[];
verified_pictures: FlatPicture[];
}

interface ExtendedMarkerOptions extends MarkerOptions {
locationTag?: ExtendedFlatTag;
}

const getCommonSupertag = (
tags: ExtendedFlatTag[],
descendantsMatrix?: {
[k: string]: {
[k: string]: boolean;
};
}
) => {
if (!tags.length || !descendantsMatrix) {
return;
}
let potentialCommonSupertag = tags[0];
for (const tag of tags) {
if (descendantsMatrix[potentialCommonSupertag.id][tag.id]) {
potentialCommonSupertag = tag;
} else if (!descendantsMatrix[tag.id][potentialCommonSupertag.id]) {
return;
}
}
return potentialCommonSupertag;
};

const PictureMapView = ({
isMaximized,
setIsMaximized,
initialMapValues,
locations,
widthStyle,
heightStyle,
map,
}: {
isMaximized: boolean;
setIsMaximized: Dispatch<SetStateAction<boolean>>;
initialMapValues: {
center: LatLng;
zoom: number;
};
locations: ExtendedFlatTag[] | undefined;
widthStyle: string;
heightStyle: string;
map: RefObject<Map>;
}) => {
const { descendantsMatrix } = useGetDescendantsMatrix(locations);
const { t } = useTranslation();

const getDividerIcon = (locationTags: ExtendedFlatTag[], clusterLocationCount?: number) => {
const locationTag = locationTags.length === 1 ? locationTags[0] : undefined;
let pictureCount = 0;
let subLocationCount = 0;
const thumbnails = locationTags.map(tag => tag.thumbnail[0]);
locationTags.forEach(tag => {
pictureCount += tag.pictures.length;
subLocationCount += tag.child_tags?.length ?? 0;
});
return new DivIcon({
html: renderToStaticMarkup(
<div className='flex relative'>
{[
'bottom-0 left-0 z-50',
'bg-white bottom-2 left-2 z-40',
'bg-white bottom-4 left-4 shadow-[5px_-5px_10px_10px_rgba(0,0,0,0.2)] z-30',
].map((extraClassNames, index) => (
<div
key={index}
className={`w-[150px] h-[150px] absolute border-solid border-white border-2 ${extraClassNames}`}
>
<img
className='object-cover !w-full !h-full'
src={asUploadPath(thumbnails[index % thumbnails.length].media, {
highQuality: false,
pictureOrigin: PictureOrigin.REMOTE,
})}
/>
</div>
))}
<div className='absolute bottom-[112px] left-[170px] p-1 flex flex-col bg-white/50 whitespace-nowrap z-20'>
<h2 className='mb-0 ml-0 mt-[-5px] text-black'>
{locationTag
? locationTag.name
: t('map.locations', { count: clusterLocationCount ?? 0 })}
</h2>
<div className='ml-[2px] mt-[-4px] text-black'>
{t('common.pictureCount', { count: pictureCount })}
</div>
<div className='ml-[2px] mt-[-4px] text-black mb-auto'>
{t('map.sublocations', { count: subLocationCount })}
</div>
</div>
{clusterLocationCount && clusterLocationCount > 1 ? (
<div className='absolute left-[220px] bottom-[80px] z-20 bg-red-500 rounded-full'>
<span className='px-1 py-1'>
{clusterLocationCount > 99 ? '99+' : clusterLocationCount}
</span>
</div>
) : null}
<div className='w-[25px] h-[40px] absolute left-[202px] bottom-[52px] z-10'>
<img className='object-fit !w-full !h-full' src={myMarkerIcon} />
</div>
<div className='w-[40px] h-[40px] absolute left-[202px] bottom-[52px] z-0'>
<img className='object-fit !w-full !h-full' src={markerShadow} />
</div>
</div>
),
iconSize: new Point(0, 0),
iconAnchor: new Point(214.5, -52),
});
};

const MyMarker = ({
position,
locationTag,
}: {
position: LatLng;
locationTag: ExtendedFlatTag;
}) => {
const { visit } = useVisit();
const dividerIcon = getDividerIcon([locationTag]);

const options = {
icon: dividerIcon,
position: position,
eventHandlers: {
click: (_: any) => {
visit('/show-more/location/' + locationTag.id, {
mapState: {
center: map.current?.getCenter() ?? initialMapValues.center,
zoom: map.current?.getZoom() ?? initialMapValues.zoom,
},
wasOpenMap: isMaximized,
});
},
},
locationTag: locationTag,
};

return <Marker {...options}></Marker>;
};

const createCustomClusterIcon = (cluster: MarkerCluster) => {
const tags = cluster
.getAllChildMarkers()
.map(marker => (marker.options as ExtendedMarkerOptions).locationTag!);
const tagsWithoutParents = tags.filter(tag => !tag.parent_tags?.length);
if (tagsWithoutParents.length) {
return getDividerIcon(tagsWithoutParents, tags.length);
} else {
const commonSupertag = getCommonSupertag(tags, descendantsMatrix);
if (commonSupertag) {
return getDividerIcon([commonSupertag], tags.length);
} else {
return getDividerIcon(tags, tags.length);
}
}
};

return (
<div className={`${widthStyle} ${heightStyle} z-0 relative`}>
<div
className='w-fit p-2 absolute z-[999] right-2 top-4 cursor-pointer decoration-black bg-white border-solid flex justify-center border-gray-400'
onClick={event => {
event.stopPropagation();
setIsMaximized(value => !value);
}}
>
{isMaximized ? <ZoomInMapOutlined /> : <ZoomOutMapOutlined />}
</div>
<MapContainer
center={initialMapValues.center}
zoom={initialMapValues.zoom}
className='map-container w-full h-full mb-1'
scrollWheelZoom={true}
ref={map}
>
<TileLayer
maxZoom={22}
maxNativeZoom={19}
className='z-10'
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
/>

<MarkerClusterGroup
iconCreateFunction={createCustomClusterIcon}
maxClusterRadius={350}
animate={true}
>
{locations?.map(location =>
location.coordinates && location.thumbnail.length && location.thumbnail[0].media ? (
<MyMarker
key={location.id}
position={new LatLng(location.coordinates.latitude, location.coordinates.longitude)}
locationTag={location}
/>
) : null
)}
</MarkerClusterGroup>
</MapContainer>
</div>
);
};

export default PictureMapView;
Loading

0 comments on commit 46b4b1f

Please sign in to comment.