Skip to content

Commit

Permalink
Merge pull request #424 from osmandapp/fix17949
Browse files Browse the repository at this point in the history
update photo gallery
  • Loading branch information
alisa911 committed Jun 22, 2024
2 parents 84dc9fa + 58fae51 commit 436c70b
Show file tree
Hide file tree
Showing 12 changed files with 667 additions and 87 deletions.
5 changes: 5 additions & 0 deletions map/src/assets/icons/ic_arrow_forward.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions map/src/context/AppContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,8 @@ export const AppContextProvider = (props) => {
const [poiIconCache, setPoiIconCache] = useState({});

const [wikiPlaces, setWikiPlaces] = useState(null);
const [photoGallery, setPhotoGallery] = useState(null);
const [selectedPhotoInd, setSelectedPhotoInd] = useState(-1);
const [searchSettings, setSearchSettings] = useState({});

const [routingCache, mutateRoutingCache] = useMutator({});
Expand Down Expand Up @@ -564,6 +566,10 @@ export const AppContextProvider = (props) => {
setPrevPageUrl,
pageParams,
setPageParams,
photoGallery,
setPhotoGallery,
selectedPhotoInd,
setSelectedPhotoInd,
}}
>
{props.children}
Expand Down
2 changes: 2 additions & 0 deletions map/src/frame/components/GlobalFrame.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import DialogActions from '@mui/material/DialogActions';
import _, { isEmpty } from 'lodash';
import TracksManager, { createTrackGroups, getGpxFiles } from '../../manager/track/TracksManager';
import { addCloseTracksToRecently } from '../../menu/visibletracks/VisibleTracks';
import PhotosModal from '../../menu/search/PhotosModal';

const GlobalFrame = () => {
const ctx = useContext(AppContext);
Expand Down Expand Up @@ -204,6 +205,7 @@ const GlobalFrame = () => {
setMenuInfo={setMenuInfo}
setOpenVisibleMenu={setOpenVisibleMenu}
/>
{ctx.selectedPhotoInd !== -1 && <PhotosModal photos={ctx.photoGallery} />}
</Box>
<MainMenu
size={MAIN_MENU_SIZE}
Expand Down
18 changes: 11 additions & 7 deletions map/src/infoblock/components/InformationBlock.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import styles from '../../menu/trackfavmenu.module.css';
import { isVisibleTrack } from '../../menu/visibletracks/VisibleTracks';
import WeatherForecastDetails from '../../menu/weather/WeatherForecastDetails';
import WptDetails from './wpt/WptDetails';
import WptPhotoList from './wpt/WptPhotoList';

const PersistentTabPanel = ({ tabId, selectedTabId, children }) => {
const [mounted, setMounted] = useState(false);
Expand Down Expand Up @@ -201,13 +202,16 @@ export default function InformationBlock({ showInfoBlock, setShowInfoBlock, setC
{showInfoBlock && (
<>
{openWeatherForecastDetails && <WeatherForecastDetails setShowInfoBlock={setShowInfoBlock} />}
{openWptDetails && (
<WptDetails
isDetails={ctx.selectedWpt?.trackWptItem || ctx.selectedWpt?.favItem}
setOpenWptTab={setOpenWptTab}
setShowInfoBlock={setShowInfoBlock}
/>
)}
{openWptDetails &&
(ctx.photoGallery ? (
<WptPhotoList photos={ctx.photoGallery} />
) : (
<WptDetails
isDetails={ctx.selectedWpt?.trackWptItem || ctx.selectedWpt?.favItem}
setOpenWptTab={setOpenWptTab}
setShowInfoBlock={setShowInfoBlock}
/>
))}
{hasOldTabs() && (
<Box anchor={'right'} sx={{ height: 'auto', width: getWidth(), overflowX: 'hidden' }}>
<div id="se-infoblock-all">
Expand Down
50 changes: 50 additions & 0 deletions map/src/infoblock/components/wpt/WptPhotoList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { AppBar, Box, IconButton, ImageList, Toolbar, Typography } from '@mui/material';
import React, { useContext, useState, useCallback } from 'react';
import headerStyles from '../../../menu/trackfavmenu.module.css';
import styles from '../../infoblock.module.css';
import AppContext from '../../../context/AppContext';
import { ReactComponent as BackIcon } from '../../../assets/icons/ic_arrow_back.svg';
import ImageItem from '../../../menu/search/ImageItem';

export default function WptPhotoList({ photos }) {
const ctx = useContext(AppContext);
const [loadedImages, setLoadedImages] = useState({});

const handleImageLoad = useCallback((index) => {
setLoadedImages((prevState) => ({
...prevState,
[index]: true,
}));
}, []);

return (
<Box minWidth={ctx.infoBlockWidth} maxWidth={ctx.infoBlockWidth} sx={{ height: 'auto', overflowX: 'hidden' }}>
<AppBar position="sticky" sx={{ top: 0, width: '100%' }} className={headerStyles.appbar}>
<Toolbar className={headerStyles.toolbar}>
<IconButton
variant="contained"
type="button"
className={styles.closeIcon}
onClick={() => ctx.setPhotoGallery(null)}
>
<BackIcon />
</IconButton>
<Typography component="div" className={headerStyles.title}>
Photos
</Typography>
</Toolbar>
</AppBar>
<ImageList sx={{ pr: '16px', pl: '16px', overflowX: 'hidden' }} gap={16} cols={2}>
{photos.map((photo, index) => (
<ImageItem
key={index}
photo={photo}
index={index}
handleImageLoad={handleImageLoad}
isLoaded={!!loadedImages[index]}
/>
))}
</ImageList>
</Box>
);
}
164 changes: 164 additions & 0 deletions map/src/manager/SearchManager.js
Original file line number Diff line number Diff line change
@@ -1 +1,165 @@
import { apiGet } from '../util/HttpApi';

export const WIKI_IMAGE_BASE_URL = 'https://commons.wikimedia.org/wiki/Special:FilePath/';

export async function fetchPhotoProperties(photo) {
if (!photo.properties.date || !photo.properties.author || !photo.properties.license) {
const imageTitle = photo.properties.imageTitle;
const url = `/wiki/File:${imageTitle}?action=raw`;
try {
const response = await apiGet(url, {
apiCache: true,
});
const data = response.data;

const parseWikiText = (wikiText) => {
const lines = wikiText.split('\n');
let author = 'Unknown';
let date = 'Unknown';
let license = 'Unknown';
let inInformationBlock = false;

lines.forEach((line) => {
if (line === '{{Information') {
inInformationBlock = true;
} else if (line === '}}' && inInformationBlock) {
inInformationBlock = false;
}
// Parse date
if (line.toLowerCase().includes('|date={{')) {
const parts = line.split('|date={{')[1]?.split('|') || line.split('|Date={{')[1].split('|');
for (let part of parts) {
const potentialDate = part.split('}}')[0].trim();
if (/^\d{4}-\d{2}-\d{2}$/.test(potentialDate)) {
date = potentialDate;
break;
}
}
} else if (line.toLowerCase().includes('|date=')) {
date = line.split('=')[1].trim().split(' ')[0];
} else if (line.includes('| Date = ')) {
date = line.split('| Date = ')[1].split(' ')[0];
}

// Parse author (priority for {{Information block)
if (inInformationBlock && line.toLowerCase().includes('|author=')) {
if (line.includes('[https://')) {
author = line.split('[https://')[1].split(']')[0].split(' ')[1];
} else if (line.includes('|author={{')) {
author = line.split('|author={{')[1].split('}}')[0];
} else if (line.includes('|Author={{')) {
author = line.split('|Author={{')[1].split('}}')[0];
} else if (line.includes('[[')) {
author = line.split('[[')[1].split(']]')[0].split('|')[1];
} else if (line.includes('|author=')) {
author = line.split('|author=')[1].trim();
} else {
author = line.split('|Author=')[1].trim();
}
} else if (!inInformationBlock && line.toLowerCase().includes('|author=') && author === 'Unknown') {
if (line.includes('{{unknown|author}}')) {
author = 'Unknown';
} else if (line.includes('[[')) {
author = line.split('[[')[1].split(']]')[0].split('|')[1];
} else if (line.includes('{{Creator:')) {
author = line.split('{{Creator:')[1].split('}}')[0];
} else if (line.includes('{{creator:')) {
author = line.split('{{creator:')[1].split('}}')[0];
} else if (line.includes('{{user at project|')) {
author = line.split('{{user at project|')[1].split('|')[0];
} else if (line.includes('[https://')) {
author = line.split('[https://')[1].split(']')[0].split(' ')[1];
} else {
author = line.split('|author=')[1].trim();
}
} else if (line.includes('| Author = ')) {
author = line.split('| Author = ')[1].split(']')[0].split(' ')[1];
}
if (author === '-') {
author = 'Unknown';
}

// Parse license
if (line.includes('|license=')) {
license = line.split('|license=')[1].split('|')[0].split('}')[0];
} else if (line.includes('|permission=')) {
if (line.includes('{{')) {
license = line.split('{{')[1].split('}}')[0].split('|')[0];
} else {
license = line.split('|permission=')[1].split('|')[0].split('}')[0];
}
} else if (line.includes('{{Self|')) {
const parts = line.split('{{Self|')[1].split('|');
license = parts.find((part) => part.startsWith('CC-BY') || part.startsWith('cc-by'));
} else if (line.includes('{{cc-by-')) {
license = line.split('{{')[1].split('}}')[0].split('|')[0];
} else if (line.includes('{{RCE-license}}')) {
license = 'RCE-license';
} else if (line.includes('{{RCE license}}')) {
license = 'RCE license';
} else if (line.includes('No known copyright restrictions')) {
license = 'No known copyright restrictions';
}
});

return {
author,
date,
license,
};
};

const parsedData = parseWikiText(data);

return {
...photo,
properties: {
...photo.properties,
date: parsedData.date !== 'Unknown' ? parsedData.date : photo.properties.date,
author: parsedData.author !== 'Unknown' ? parsedData.author : photo.properties.author,
license: parsedData.license !== 'Unknown' ? parsedData.license : photo.properties.license,
},
};
} catch (error) {
console.error('Failed to fetch photo properties:', error);
return photo;
}
}
return photo;
}

/**
* Examples:
* Date:
* |date={{Original upload date|2015-04-15}} => 2015-04-15
* |Date={{original upload date|2006-11-05}} => 2006-11-05
* |date=2011-10-08 => 2011-10-08
* |Date=2009-12-06 23:11 => 2009-12-06
* |date=1940-05 => 1940-05
* | Date = 2018-04-06 12:25 => 2018-04-06
* |date={{Taken on|2014-03-09|location=Netherlands}} => 2014-03-09
* |date={{According to Exif data|2021-07-17}} => 2021-07-17
*
* Author:
* |author=[https://web.archive.org/web/20161031223609/http://www.panoramio.com/user/4678999?with_photo_id=118704129 Ben Bender] => Ben Bender
* |author={{Creator:Johannes Petrus Albertus Antonietti}} => Johannes Petrus Albertus Antonietti
* |author={{creator:Johannes Petrus Albertus Antonietti}} => Johannes Petrus Albertus Antonietti
* |author=[[User:PersianDutchNetwork|PersianDutchNetwork]] => PersianDutchNetwork
* |Author={{user at project|MelvinvdC|wikipedia|nl}} => MelvinvdC
* |Author=[https://www.flickr.com/people/13088710@N02 Jos van Zetten] from Amsterdam, the Netherlands => Jos van Zetten
* |author={{unknown|author}} => 'Unknown'
* | Author = [https://www.flickr.com/people/141420435@N08 Nanda Sluijsmans] from Den Haag, Nederland => Nanda Sluijsmans
* | Author = Coalition for the ICC / Credit: UN => Coalition for the ICC
* | Author = - => 'Unknown'
*
* License:
* |license={{cc-by-sa-3.0|Author Name}} => cc-by-sa-3.0
* |permission={{cc-by-sa-3.0|ekstijn}} => cc-by-sa-3.0
* == {{int:license-header}} ==
* {{Self|author={{user at project|MelvinvdC|wikipedia|nl}}|GFDL|CC-BY-SA-2.5|migration=relicense}} => CC-BY-SA-2.5
* {{self|cc-by-sa-3.0}} => cc-by-sa-3.0
* {{cc-by-2.0}} => cc-by-2.0
* {{RCE-license}} => RCE-license
* {{RCE license}} => RCE license
* {{User:FlickreviewR/reviewed-pass|Nationaal Archief|https://flickr.com/photos/29998366@N02/2949392968|2016-11-27 10:53:09|No known copyright restrictions|}} => No known copyright restrictions
*/
1 change: 1 addition & 0 deletions map/src/map/layers/SearchLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ export default function SearchLayer() {
ctx.setSearchSettings({ ...ctx.searchSettings, getPoi: feature });
}
}
ctx.setPhotoGallery(null);
}

const markerClusterGroup = new L.MarkerClusterGroup({
Expand Down
63 changes: 63 additions & 0 deletions map/src/menu/search/ImageItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useInView } from 'react-intersection-observer';
import React, { useCallback, useContext, useEffect, useState, useRef } from 'react';
import { ImageListItem, Skeleton } from '@mui/material';
import { WIKI_IMAGE_BASE_URL } from '../../manager/SearchManager';
import AppContext from '../../context/AppContext';
import styles from '../search/search.module.css';

export default function ImageItem({ photo, index, handleImageLoad, isLoaded }) {
const ctx = useContext(AppContext);
const [isSelected, setIsSelected] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const itemRef = useRef(null);
const { ref, inView } = useInView({
triggerOnce: true,
threshold: 0.1,
});

const handleMouseEnter = () => setIsHovered(true);
const handleMouseLeave = () => setIsHovered(false);

const combinedRef = (node) => {
ref(node); // assign the node to useInView ref
itemRef.current = node; // assign the node to the scroll ref
};

const handlePhotoClick = useCallback((index) => {
ctx.setSelectedPhotoInd(index);
}, []);

useEffect(() => {
setIsSelected(ctx.selectedPhotoInd === index);
}, [ctx.selectedPhotoInd, index]);

useEffect(() => {
if (isSelected && itemRef.current) {
itemRef.current.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
}, [isSelected]);

return (
<ImageListItem
ref={combinedRef}
className={styles.imageItem}
onClick={() => handlePhotoClick(index)}
onMouseOver={handleMouseEnter}
onMouseOut={handleMouseLeave}
>
{!isLoaded && <Skeleton className={styles.skeleton} />}
{inView && (
<img
src={`${WIKI_IMAGE_BASE_URL}${photo.properties.imageTitle}?width=300`}
alt={photo.properties.imageTitle}
className={styles.image}
onLoad={() => handleImageLoad(index)}
/>
)}
{(isSelected || isHovered) && <div className={styles.selectedImage} />}
</ImageListItem>
);
}
Loading

0 comments on commit 436c70b

Please sign in to comment.