-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
22 changed files
with
673 additions
and
180 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
226
projects/bp-gallery/src/components/common/PictureMapView.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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='© <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; |
Oops, something went wrong.