diff --git a/frontend/src/metadata/components/view-toolbar/index.js b/frontend/src/metadata/components/view-toolbar/index.js index efa6bd8386..0c6fc2b158 100644 --- a/frontend/src/metadata/components/view-toolbar/index.js +++ b/frontend/src/metadata/components/view-toolbar/index.js @@ -114,11 +114,9 @@ const ViewToolBar = ({ viewId, isCustomPermission, onToggleDetail, onCloseDetail {viewType === VIEW_TYPE.MAP && ( )} diff --git a/frontend/src/metadata/components/view-toolbar/map-view-toolbar/index.js b/frontend/src/metadata/components/view-toolbar/map-view-toolbar/index.js index b961c3a458..5b5e779858 100644 --- a/frontend/src/metadata/components/view-toolbar/map-view-toolbar/index.js +++ b/frontend/src/metadata/components/view-toolbar/map-view-toolbar/index.js @@ -1,20 +1,14 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; -import { EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY, VIEW_TYPE } from '../../../constants'; -import { FilterSetter, GalleryGroupBySetter, MapTypeSetter, SortSetter } from '../../data-process-setter'; -import { gettext } from '../../../../utils/constants'; +import { PRIVATE_COLUMN_KEY, VIEW_TYPE } from '../../../constants'; +import { FilterSetter, MapTypeSetter } from '../../data-process-setter'; const MapViewToolBar = ({ - isCustomPermission, readOnly, view: oldView, collaborators, modifyFilters, - onToggleDetail, }) => { - const [showGalleryToolbar, setShowGalleryToolbar] = useState(false); - const [view, setView] = useState(oldView); - const viewType = useMemo(() => VIEW_TYPE.MAP, []); const viewColumns = useMemo(() => { if (!oldView) return []; @@ -26,54 +20,6 @@ const MapViewToolBar = ({ return viewColumns && viewColumns.filter(c => c.key !== PRIVATE_COLUMN_KEY.FILE_TYPE); }, [viewColumns]); - const onToggle = useCallback((value) => { - setShowGalleryToolbar(value); - }, []); - - const modifySorts = useCallback((sorts) => { - window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.UPDATE_SERVER_VIEW, { sorts }); - }, []); - - const resetView = useCallback(view => { - setView(view); - }, []); - - useEffect(() => { - setShowGalleryToolbar(false); - const unsubscribeToggle = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, onToggle); - const unsubscribeView = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.RESET_VIEW, resetView); - return () => { - unsubscribeToggle && unsubscribeToggle(); - unsubscribeView && unsubscribeView(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [viewID]); - - if (showGalleryToolbar) { - return ( - <> -
- - - {!isCustomPermission && ( -
- -
- )} -
-
- - ); - } - return ( <>
@@ -99,11 +45,10 @@ const MapViewToolBar = ({ }; MapViewToolBar.propTypes = { - isCustomPermission: PropTypes.bool, readOnly: PropTypes.bool, + view: PropTypes.object, collaborators: PropTypes.array, modifyFilters: PropTypes.func, - onToggleDetail: PropTypes.func, }; export default MapViewToolBar; diff --git a/frontend/src/metadata/views/map/cluster-photos/index.css b/frontend/src/metadata/views/map/cluster-photos/index.css deleted file mode 100644 index e190c9727a..0000000000 --- a/frontend/src/metadata/views/map/cluster-photos/index.css +++ /dev/null @@ -1,4 +0,0 @@ -.sf-metadata-map-photos-container { - padding: 0 !important; - overflow: hidden !important; -} diff --git a/frontend/src/metadata/views/map/cluster-photos/index.js b/frontend/src/metadata/views/map/cluster-photos/index.js deleted file mode 100644 index 4678e616fc..0000000000 --- a/frontend/src/metadata/views/map/cluster-photos/index.js +++ /dev/null @@ -1,159 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; -import deepCopy from 'deep-copy'; -import dayjs from 'dayjs'; -import utc from 'dayjs/plugin/utc'; -import { CenteredLoading } from '@seafile/sf-metadata-ui-component'; -import Gallery from '../../gallery/main'; -import { EVENT_BUS_TYPE, UTC_FORMAT_DEFAULT } from '../../../constants'; -import metadataAPI from '../../../api'; -import { Utils } from '../../../../utils/utils'; -import toaster from '../../../../components/toast'; -import { useMetadataView } from '../../../hooks/metadata-view'; -import { getRowsByIds } from '../../../utils/table'; -import Metadata from '../../../model/metadata'; -import { sortTableRows } from '../../../utils/sort'; -import { useCollaborators } from '../../../hooks/collaborators'; -import { getRecordIdFromRecord, getParentDirFromRecord, getFileNameFromRecord } from '../../../utils/cell'; - -import './index.css'; - -dayjs.extend(utc); - -const ClusterPhotos = ({ photoIds, onClose }) => { - const { repoID, viewID, metadata: allMetadata, store, addFolder, deleteRecords } = useMetadataView(); - const { collaborators } = useCollaborators(); - - const [isLoading, setLoading] = useState(true); - const [metadata, setMetadata] = useState({ rows: getRowsByIds(allMetadata, photoIds), columns: allMetadata?.columns || [] }); - - const loadData = useCallback((view) => { - setLoading(true); - const columns = metadata.columns; - const orderRows = sortTableRows({ columns }, metadata.rows, view?.sorts || [], { collaborators, isReturnID: false }); - let newMetadata = new Metadata({ rows: orderRows, columns, view }); - newMetadata.hasMore = false; - setMetadata(newMetadata); - window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.RESET_VIEW, newMetadata.view); - setLoading(false); - }, [metadata, collaborators]); - - const deletedByIds = useCallback((ids) => { - if (!Array.isArray(ids) || ids.length === 0) return; - const newMetadata = deepCopy(metadata); - const idNeedDeletedMap = ids.reduce((currIdNeedDeletedMap, rowId) => ({ ...currIdNeedDeletedMap, [rowId]: true }), {}); - newMetadata.rows = newMetadata.rows.filter((row) => !idNeedDeletedMap[row._id]); - newMetadata.row_ids = newMetadata.row_ids.filter((id) => !idNeedDeletedMap[id]); - - // delete rows in id_row_map - ids.forEach(rowId => { - delete newMetadata.id_row_map[rowId]; - }); - newMetadata.recordsCount = newMetadata.row_ids.length; - setMetadata(newMetadata); - - if (newMetadata.rows.length === 0) { - onClose && onClose(); - } - }, [metadata, onClose]); - - const handelDelete = useCallback((deletedImages, { success_callback } = {}) => { - if (!deletedImages.length) return; - let recordIds = []; - deletedImages.forEach((record) => { - const { id, parentDir, name } = record || {}; - if (parentDir && name) { - recordIds.push(id); - } - }); - deleteRecords(recordIds, { - success_callback: () => { - success_callback(); - deletedByIds(recordIds); - } - }); - }, [deleteRecords, deletedByIds]); - - const onViewChange = useCallback((update) => { - metadataAPI.modifyView(repoID, viewID, update).then(res => { - store.modifyLocalView(update); - const newView = { ...metadata.view, ...update }; - loadData(newView); - }).catch(error => { - const errorMessage = Utils.getErrorMsg(error); - toaster.danger(errorMessage); - }); - }, [metadata, repoID, viewID, store, loadData]); - - const onRecordChange = useCallback(({ recordId, parentDir, fileName }, update) => { - const modifyTime = dayjs().utc().format(UTC_FORMAT_DEFAULT); - const modifier = window.sfMetadataContext.getUsername(); - const { rows, columns, view } = metadata; - let newRows = [...rows]; - newRows.forEach((row, index) => { - const _rowId = getRecordIdFromRecord(row); - const _parentDir = getParentDirFromRecord(row); - const _fileName = getFileNameFromRecord(row); - if ((_rowId === recordId || (_parentDir === parentDir && _fileName === fileName)) && update) { - const updatedRow = Object.assign({}, row, update, { - '_mtime': modifyTime, - '_last_modifier': modifier, - }); - newRows[index] = updatedRow; - } - }); - let updatedColumnKeyMap = { - '_mtime': true, - '_last_modifier': true - }; - Object.keys(update).forEach(key => { - updatedColumnKeyMap[key] = true; - }); - if (view.sorts.some(sort => updatedColumnKeyMap[sort.column_key])) { - newRows = sortTableRows({ columns }, newRows, view?.sorts || [], { collaborators, isReturnID: false }); - } - let newMetadata = new Metadata({ rows: newRows, columns, view }); - newMetadata.hasMore = false; - setMetadata(newMetadata); - }, [metadata, collaborators]); - - useEffect(() => { - const eventBus = window?.sfMetadataContext?.eventBus; - if (!eventBus) return; - eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, true); - return () => { - eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, false); - }; - }, []); - - useEffect(() => { - const eventBus = window?.sfMetadataContext?.eventBus; - if (!eventBus) return; - const unsubscribeViewChange = eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_SERVER_VIEW, onViewChange); - const localRecordChangedSubscribe = eventBus.subscribe(EVENT_BUS_TYPE.LOCAL_RECORD_CHANGED, onRecordChange); - return () => { - unsubscribeViewChange && unsubscribeViewChange(); - localRecordChangedSubscribe && localRecordChangedSubscribe(); - }; - }, [onViewChange, onRecordChange]); - - useEffect(() => { - loadData({ sorts: allMetadata.view.sorts }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (isLoading) return (); - - return ( -
- -
- ); -}; - -ClusterPhotos.propTypes = { - photoIds: PropTypes.array, - onClose: PropTypes.func, -}; - -export default ClusterPhotos; diff --git a/frontend/src/metadata/views/map/map-view/control/geolocation-control/index.css b/frontend/src/metadata/views/map/control/geolocation-control/index.css similarity index 61% rename from frontend/src/metadata/views/map/map-view/control/geolocation-control/index.css rename to frontend/src/metadata/views/map/control/geolocation-control/index.css index 49359bb5bd..f0e25e0ace 100644 --- a/frontend/src/metadata/views/map/map-view/control/geolocation-control/index.css +++ b/frontend/src/metadata/views/map/control/geolocation-control/index.css @@ -1,4 +1,4 @@ .sf-map-control-container.sf-map-geolocation-control { - width: 40px; - line-height: 40px; + width: 30px; + line-height: 30px; } diff --git a/frontend/src/metadata/views/map/map-view/control/geolocation-control/index.js b/frontend/src/metadata/views/map/control/geolocation-control/index.js similarity index 96% rename from frontend/src/metadata/views/map/map-view/control/geolocation-control/index.js rename to frontend/src/metadata/views/map/control/geolocation-control/index.js index 62e05f53c3..575cc15088 100644 --- a/frontend/src/metadata/views/map/map-view/control/geolocation-control/index.js +++ b/frontend/src/metadata/views/map/control/geolocation-control/index.js @@ -1,5 +1,5 @@ import classnames from 'classnames'; -import { Utils } from '../../../../../../utils/utils'; +import { Utils } from '../../../../../utils/utils'; import './index.css'; diff --git a/frontend/src/metadata/views/map/map-view/control/index.css b/frontend/src/metadata/views/map/control/index.css similarity index 87% rename from frontend/src/metadata/views/map/map-view/control/index.css rename to frontend/src/metadata/views/map/control/index.css index c9157de961..97181cb2b6 100644 --- a/frontend/src/metadata/views/map/map-view/control/index.css +++ b/frontend/src/metadata/views/map/control/index.css @@ -1,12 +1,12 @@ .sf-map-control-container { - height: 40px; + height: 30px; width: fit-content; background-color: rgba(255, 255, 255, .9); opacity: 1; overflow: hidden; border-radius: 6px; box-shadow: -2px -2px 4px 2px rgba(0, 0, 0, 0.1); - line-height: 40px; + line-height: 30px; } .sf-map-control-container.sf-map-control-loading { @@ -14,8 +14,8 @@ } .sf-map-control-container.sf-map-control-container-mobile { - height: 35px; - line-height: 35px; + height: 25px; + line-height: 25px; opacity: .75; } @@ -36,21 +36,21 @@ } .sf-map-control-container .sf-map-control { - height: 40px; - width: 40px; + height: 30px; + width: 30px; color: #666; background-color: inherit; text-align: center; } .sf-map-control-container.sf-map-control-container-mobile .sf-map-control { - height: 35px; - width: 35px; + height: 25px; + width: 25px; opacity: .75; } .sf-map-control-container .sf-map-control .sf-map-control-icon { - font-size: 18px; + font-size: 14px; } .sf-map-control-container .sf-map-control:not(.disabled):hover { diff --git a/frontend/src/metadata/views/map/map-view/control/index.js b/frontend/src/metadata/views/map/control/index.js similarity index 100% rename from frontend/src/metadata/views/map/map-view/control/index.js rename to frontend/src/metadata/views/map/control/index.js diff --git a/frontend/src/metadata/views/map/map-view/control/zoom-control/index.css b/frontend/src/metadata/views/map/control/zoom-control/index.css similarity index 83% rename from frontend/src/metadata/views/map/map-view/control/zoom-control/index.css rename to frontend/src/metadata/views/map/control/zoom-control/index.css index 98deb1eabd..f2dd9dd107 100644 --- a/frontend/src/metadata/views/map/map-view/control/zoom-control/index.css +++ b/frontend/src/metadata/views/map/control/zoom-control/index.css @@ -1,3 +1,3 @@ .sf-map-control-container.sf-map-zoom-control-container .sf-map-control { - width: 55px; + width: 40px; } diff --git a/frontend/src/metadata/views/map/map-view/control/zoom-control/index.js b/frontend/src/metadata/views/map/control/zoom-control/index.js similarity index 94% rename from frontend/src/metadata/views/map/map-view/control/zoom-control/index.js rename to frontend/src/metadata/views/map/control/zoom-control/index.js index 79057f567e..5a82f84ff9 100644 --- a/frontend/src/metadata/views/map/map-view/control/zoom-control/index.js +++ b/frontend/src/metadata/views/map/control/zoom-control/index.js @@ -1,12 +1,12 @@ import classnames from 'classnames'; -import { Utils } from '../../../../../../utils/utils'; +import { Utils } from '../../../../../utils/utils'; import './index.css'; export function createBMapZoomControl(BMapGL, { maxZoom, minZoom }, callback) { function ZoomControl() { this.defaultAnchor = window.BMAP_ANCHOR_BOTTOM_RIGHT; - this.defaultOffset = new BMapGL.Size(80, Utils.isDesktop() ? 30 : 90); + this.defaultOffset = new BMapGL.Size(66, Utils.isDesktop() ? 30 : 90); } ZoomControl.prototype = new BMapGL.Control(); ZoomControl.prototype.initialize = function (map) { diff --git a/frontend/src/metadata/views/map/index.css b/frontend/src/metadata/views/map/index.css index 22ff37c943..faf70d2ac4 100644 --- a/frontend/src/metadata/views/map/index.css +++ b/frontend/src/metadata/views/map/index.css @@ -4,3 +4,86 @@ display: flex; flex-direction: column; } + +.sf-metadata-view-map #platform div:has(.custom-avatar-overlay) { + display: block !important; +} + +.sf-metadata-view-map #platform div:has(.custom-image-overlay) { + display: block !important; +} + +.sf-metadata-view-map .sf-metadata-map-container { + width: 100%; + height: 100%; +} + +.sf-metadata-view-map .custom-image-container { + width: 86px; + height: 86px; + display: flex; + align-items: center; + justify-content: center; + background-color: #fff; + padding: 3px; + border-radius: 6px; + position: relative; + cursor: default; +} + +.sf-metadata-view-map .custom-image-container img { + border-radius: 6px; +} + +.sf-metadata-view-map .custom-image-number { + position: absolute; + right: -16px; + top: -16px; + width: 32px; + height: 32px; + line-height: 32px; + background: #007bff; + color: #fff; + border-radius: 50%; + text-align: center; + font-size: 16px; + font-weight: 400; +} + +.sf-metadata-view-map .custom-image-container:active::before, +.sf-metadata-view-map .custom-image-container:active .custom-image-number::before, +.sf-metadata-view-map .custom-image-number:active::before, +.sf-metadata-view-map .custom-image-number:active .custom-image-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.4); + border-radius: 6px; +} + +.sf-metadata-view-map .custom-image-container:active .custom-image-number::before, +.sf-metadata-view-map .custom-image-number:active::before { + border-radius: 50%; +} + +.sf-metadata-view-map .custom-image-container::after { + content: ''; + position: absolute; + bottom: -10px; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 10px solid transparent; + border-right: 10px solid transparent; + border-top: 10px solid #fff; + border-radius: 2px; +} + +.sf-metadata-view-map .custom-image-container:active::after, +.sf-metadata-view-map .custom-image-number:active .custom-image-container::after { + border-top: 10px solid rgba(0, 0, 0, 0.4); +} diff --git a/frontend/src/metadata/views/map/index.js b/frontend/src/metadata/views/map/index.js index b6d25eecce..35d90aeab4 100644 --- a/frontend/src/metadata/views/map/index.js +++ b/frontend/src/metadata/views/map/index.js @@ -1,17 +1,32 @@ -import React, { useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { getFileNameFromRecord, getFileTypeFromRecord, getImageLocationFromRecord, getParentDirFromRecord, getRecordIdFromRecord } from '../../utils/cell'; -import MapView from './map-view'; -import { PREDEFINED_FILE_TYPE_OPTION_KEY } from '../../constants'; import { useMetadataView } from '../../hooks/metadata-view'; import { Utils } from '../../../utils/utils'; -import { fileServerRoot, siteRoot, thumbnailSizeForGrid, thumbnailSizeForOriginal } from '../../../utils/constants'; import { isValidPosition } from '../../utils/validate'; import { gcj02_to_bd09, wgs84_to_gcj02 } from '../../../utils/coord-transform'; -import { PRIVATE_FILE_TYPE } from '../../../constants'; +import loadBMap, { initMapInfo } from '../../../utils/map-utils'; +import { appAvatarURL, baiduMapKey, fileServerRoot, googleMapKey, mediaUrl, siteRoot, thumbnailSizeForGrid, thumbnailSizeForOriginal } from '../../../utils/constants'; +import { MAP_TYPE as MAP_PROVIDER, PRIVATE_FILE_TYPE } from '../../../constants'; +import { EVENT_BUS_TYPE, MAP_TYPE, PREDEFINED_FILE_TYPE_OPTION_KEY, STORAGE_MAP_CENTER_KEY, STORAGE_MAP_TYPE_KEY, STORAGE_MAP_ZOOM_KEY } from '../../constants'; +import { createBMapGeolocationControl, createBMapZoomControl } from './control'; +import { customAvatarOverlay, customImageOverlay } from './overlay'; +import ModalPortal from '../../../components/modal-portal'; +import ImageDialog from '../../../components/dialog/image-dialog'; import './index.css'; +const DEFAULT_POSITION = { lng: 104.195, lat: 35.861 }; +const DEFAULT_ZOOM = 4; +const MAX_ZOOM = 21; +const MIN_ZOOM = 3; + const Map = () => { + const [imageIndex, setImageIndex] = useState(0); + const [clusterLeaveIds, setClusterLeaveIds] = useState([]); + + const mapRef = useRef(null); + const clusterRef = useRef(null); + const clickTimeoutRef = useRef(null); const { metadata, viewID, updateCurrentPath } = useMetadataView(); const repoID = window.sfMetadataContext.getSetting('repoID'); @@ -52,18 +67,219 @@ const Map = () => { downloadURL: `${fileServerRoot}repos/${repoID}/files${path}?op=download`, thumbnail, parentDir, - location: { lng: bdPosition.lng, lat: bdPosition.lat } + location: bdPosition }; }) .filter(Boolean); }, [repoID, repoInfo.encrypted, metadata]); + const mapInfo = useMemo(() => initMapInfo({ baiduMapKey, googleMapKey }), []); + const clusterLeaves = useMemo(() => images.filter(image => clusterLeaveIds.includes(image.id)), [images, clusterLeaveIds]); + + const getPoints = useCallback(() => { + if (!window.Cluster || !images) return []; + return window.Cluster.pointTransformer(images, (data) => ({ + point: [data.location.lng, data.location.lat], + properties: { + id: data.id, + src: data.src, + } + })); + }, [images]); + + const saveMapState = useCallback(() => { + if (!mapRef.current) return; + const point = mapRef.current.getCenter && mapRef.current.getCenter(); + const zoom = mapRef.current.getZoom && mapRef.current.getZoom(); + window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_CENTER_KEY, point); + window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_ZOOM_KEY, zoom); + }, []); + + const addMapController = useCallback(() => { + const ZoomControl = createBMapZoomControl(window.BMapGL, { maxZoom: MAX_ZOOM, minZoom: MIN_ZOOM }, saveMapState); + const zoomControl = new ZoomControl(); + const GeolocationControl = createBMapGeolocationControl(window.BMapGL, (point) => { + point && mapRef.current && mapRef.current.setCenter(point); + }); + + const geolocationControl = new GeolocationControl(); + mapRef.current.addControl(zoomControl); + mapRef.current.addControl(geolocationControl); + }, [saveMapState]); + + const initializeUserMarker = useCallback((centerPoint) => { + if (!window.BMapGL || !mapRef.current) return; + const imageUrl = `${mediaUrl}img/marker.png`; + const avatarMarker = customAvatarOverlay(centerPoint, appAvatarURL, imageUrl); + mapRef.current.addOverlay(avatarMarker); + }, []); + + const getBMapType = useCallback((type) => { + switch (type) { + case MAP_TYPE.SATELLITE: { + return window.BMAP_EARTH_MAP; + } + default: { + return window.BMAP_NORMAL_MAP; + } + } + }, []); + + const loadMapState = useCallback(() => { + const savedCenter = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_CENTER_KEY) || DEFAULT_POSITION; + const savedZoom = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_ZOOM_KEY) || DEFAULT_ZOOM; + return { center: savedCenter, zoom: savedZoom }; + }, []); + + const initializeCluster = useCallback(() => { + clusterRef.current = new window.Cluster.View(mapRef.current, { + clusterRadius: 60, + updateRealTime: true, + fitViewOnClick: false, + isAnimation: true, + clusterMap: (properties) => ({ src: properties.src, id: properties.id }), + clusterReduce: (acc, properties) => { + if (!acc.properties) { + acc.properties = []; + } + acc.properties.push(properties); + }, + renderClusterStyle: { + type: window.Cluster.ClusterRender.DOM, + style: { offsetX: -40, offsetY: -80 }, + inject: (props) => customImageOverlay(props), + }, + }); + + clusterRef.current.setData(getPoints()); + + clusterRef.current.on(window.Cluster.ClusterEvent.CLICK, (element) => { + if (clickTimeoutRef.current) { + clearTimeout(clickTimeoutRef.current); + clickTimeoutRef.current = null; + return; + } else { + clickTimeoutRef.current = setTimeout(() => { + let imageIds = []; + if (element.isCluster) { + imageIds = clusterRef.current.getLeaves(element.id).map(item => item.properties.id).filter(Boolean); + } else { + imageIds = [element.properties.id]; + } + clickTimeoutRef.current = null; + setClusterLeaveIds(imageIds); + }, 300); + } + }); + window.BMapCluster = clusterRef.current; + }, [getPoints]); + + const renderBaiduMap = useCallback(() => { + if (!window.BMapGL.Map) return; + let { center, zoom } = loadMapState(); + let userPosition = { lng: 116.40396418840683, lat: 39.915106021711345 }; + // ask for user location + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition((userInfo) => { + const gcPosition = wgs84_to_gcj02(userInfo.coords.longitude, userInfo.coords.latitude); + const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat); + const { lng, lat } = bdPosition; + userPosition = new window.BMapGL.Point(lng, lat); + center = userPosition; + window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_CENTER_KEY, center); + }); + } + const mapTypeValue = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_TYPE_KEY); + + if (!window.BMapInstance) { + mapRef.current = new window.BMapGL.Map('sf-metadata-map-container', { + enableMapClick: false, + minZoom: MIN_ZOOM, + maxZoom: MAX_ZOOM, + mapType: getBMapType(mapTypeValue), + }); + window.BMapInstance = mapRef.current; + + if (isValidPosition(center?.lng, center?.lat)) { + mapRef.current.centerAndZoom(center, zoom); + } + + mapRef.current.enableScrollWheelZoom(true); + addMapController(); + + initializeUserMarker(userPosition); + initializeCluster(); + } else { + const viewDom = document.getElementById('sf-metadata-view-map'); + const container = window.BMapInstance.getContainer(); + viewDom.replaceChild(container, mapRef.current); + + mapRef.current = window.BMapInstance; + clusterRef.current = window.BMapCluster; + clusterRef.current.setData(getPoints()); + } + }, [addMapController, initializeCluster, initializeUserMarker, getBMapType, loadMapState, getPoints]); + + const handleClose = useCallback(() => { + setImageIndex(0); + setClusterLeaveIds([]); + }, []); + + const moveToPrevImage = useCallback(() => { + setImageIndex((imageIndex + clusterLeaves.length - 1) % clusterLeaves.length); + }, [imageIndex, clusterLeaves.length]); + + const moveToNextImage = useCallback(() => { + setImageIndex((imageIndex + 1) % clusterLeaves.length); + }, [imageIndex, clusterLeaves.length]); + useEffect(() => { updateCurrentPath(`/${PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES}/${viewID}`); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - return (); + useEffect(() => { + const modifyMapTypeSubscribe = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.MODIFY_MAP_TYPE, (newType) => { + window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_TYPE_KEY, newType); + const mapType = getBMapType(newType); + mapRef.current && mapRef.current.setMapType(mapType); + mapRef.current.setCenter(mapRef.current.getCenter()); + }); + + return () => { + modifyMapTypeSubscribe(); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (mapInfo.type === MAP_PROVIDER.B_MAP) { + loadBMap(mapInfo.key).then(() => renderBaiduMap()); + return () => { + window.renderMap = null; + }; + } + return; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+
+ {clusterLeaveIds.length > 0 && ( + + + + )} +
+ ); }; export default Map; diff --git a/frontend/src/metadata/views/map/map-view/index.css b/frontend/src/metadata/views/map/map-view/index.css deleted file mode 100644 index 58f9773770..0000000000 --- a/frontend/src/metadata/views/map/map-view/index.css +++ /dev/null @@ -1,82 +0,0 @@ -.sf-metadata-view-map #platform div:has(.custom-avatar-overlay) { - display: block !important; -} - -.sf-metadata-view-map #platform div:has(.custom-image-overlay) { - display: block !important; -} - -.sf-metadata-view-map .sf-metadata-map-container { - width: 100%; - height: 100%; -} - -.sf-metadata-view-map .custom-image-container { - width: 86px; - height: 86px; - display: flex; - align-items: center; - justify-content: center; - background-color: #fff; - padding: 3px; - border-radius: 6px; - position: relative; - cursor: default; -} - -.sf-metadata-view-map .custom-image-container img { - border-radius: 6px; -} - -.sf-metadata-view-map .custom-image-number { - position: absolute; - right: -16px; - top: -16px; - width: 32px; - height: 32px; - line-height: 32px; - background: #007bff; - color: #fff; - border-radius: 50%; - text-align: center; - font-size: 16px; - font-weight: 400; -} - -.sf-metadata-view-map .custom-image-container:active::before, -.sf-metadata-view-map .custom-image-container:active .custom-image-number::before, -.sf-metadata-view-map .custom-image-number:active::before, -.sf-metadata-view-map .custom-image-number:active .custom-image-container::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.4); - border-radius: 6px; -} - -.sf-metadata-view-map .custom-image-container:active .custom-image-number::before, -.sf-metadata-view-map .custom-image-number:active::before { - border-radius: 50%; -} - -.sf-metadata-view-map .custom-image-container::after { - content: ''; - position: absolute; - bottom: -10px; - left: 50%; - transform: translateX(-50%); - width: 0; - height: 0; - border-left: 10px solid transparent; - border-right: 10px solid transparent; - border-top: 10px solid #fff; - border-radius: 2px; -} - -.sf-metadata-view-map .custom-image-container:active::after, -.sf-metadata-view-map .custom-image-number:active .custom-image-container::after { - border-top: 10px solid rgba(0, 0, 0, 0.4); -} diff --git a/frontend/src/metadata/views/map/map-view/index.js b/frontend/src/metadata/views/map/map-view/index.js deleted file mode 100644 index dea03ed4b7..0000000000 --- a/frontend/src/metadata/views/map/map-view/index.js +++ /dev/null @@ -1,226 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import PropTypes from 'prop-types'; -import loadBMap, { initMapInfo } from '../../../../utils/map-utils'; -import { appAvatarURL, baiduMapKey, googleMapKey, mediaUrl } from '../../../../utils/constants'; -import { isValidPosition } from '../../../utils/validate'; -import { wgs84_to_gcj02, gcj02_to_bd09 } from '../../../../utils/coord-transform'; -import { MAP_TYPE as MAP_PROVIDER } from '../../../../constants'; -import { EVENT_BUS_TYPE, MAP_TYPE, STORAGE_MAP_CENTER_KEY, STORAGE_MAP_TYPE_KEY, STORAGE_MAP_ZOOM_KEY } from '../../../constants'; -import { createBMapGeolocationControl, createBMapZoomControl } from './control'; -import { customAvatarOverlay, customImageOverlay } from './overlay'; -import ModalPortal from '../../../../components/modal-portal'; -import ImageDialog from '../../../../components/dialog/image-dialog'; - -import './index.css'; - -const DEFAULT_POSITION = { lng: 104.195, lat: 35.861 }; -const DEFAULT_ZOOM = 4; -const MAX_ZOOM = 21; -const MIN_ZOOM = 3; - -const MapView = ({ images }) => { - const [imageIndex, setImageIndex] = useState(0); - const [clusterLeaveIds, setClusterLeaveIds] = useState([]); - - const mapInfo = useMemo(() => initMapInfo({ baiduMapKey, googleMapKey }), []); - const clusterLeaves = useMemo(() => images.filter(image => clusterLeaveIds.includes(image.id)), [images, clusterLeaveIds]); - - const mapRef = useRef(null); - const clusterRef = useRef(null); - const batchIndexRef = useRef(0); - const clickTimeoutRef = useRef(null); - - const saveMapState = useCallback(() => { - if (!mapRef.current) return; - const point = mapRef.current.getCenter && mapRef.current.getCenter(); - const zoom = mapRef.current.getZoom && mapRef.current.getZoom(); - window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_CENTER_KEY, point); - window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_ZOOM_KEY, zoom); - }, []); - - const addMapController = useCallback(() => { - const ZoomControl = createBMapZoomControl(window.BMapGL, { maxZoom: MAX_ZOOM, minZoom: MIN_ZOOM }, saveMapState); - const zoomControl = new ZoomControl(); - const GeolocationControl = createBMapGeolocationControl(window.BMapGL, (point) => { - point && mapRef.current && mapRef.current.setCenter(point); - }); - - const geolocationControl = new GeolocationControl(); - mapRef.current.addControl(zoomControl); - mapRef.current.addControl(geolocationControl); - }, [saveMapState]); - - const initializeUserMarker = useCallback((centerPoint) => { - if (!window.BMapGL || !mapRef.current) return; - const imageUrl = `${mediaUrl}img/marker.png`; - const avatarMarker = customAvatarOverlay(centerPoint, appAvatarURL, imageUrl); - mapRef.current.addOverlay(avatarMarker); - }, []); - - const getBMapType = useCallback((type) => { - switch (type) { - case MAP_TYPE.SATELLITE: { - return window.BMAP_EARTH_MAP; - } - default: { - return window.BMAP_NORMAL_MAP; - } - } - }, []); - - const loadMapState = useCallback(() => { - const savedCenter = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_CENTER_KEY) || DEFAULT_POSITION; - const savedZoom = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_ZOOM_KEY) || DEFAULT_ZOOM; - return { center: savedCenter, zoom: savedZoom }; - }, []); - - const getPoints = useCallback((images) => { - if (!window.Cluster || !images) return []; - return window.Cluster.pointTransformer(images, (data) => ({ - point: [data.location.lng, data.location.lat], - properties: { - id: data.id, - src: data.src, - } - })); - }, []); - - const initializeCluster = useCallback(() => { - clusterRef.current = new window.Cluster.View(mapRef.current, { - clusterRadius: 80, - updateRealTime: true, - fitViewOnClick: false, - isAnimation: true, - clusterMap: (properties) => ({ src: properties.src, id: properties.id }), - clusterReduce: (acc, properties) => { - if (!acc.properties) { - acc.properties = []; - } - acc.properties.push(properties); - }, - renderClusterStyle: { - type: window.Cluster.ClusterRender.DOM, - inject: (props) => customImageOverlay(props), - }, - }); - - clusterRef.current.setData(getPoints(images)); - - clusterRef.current.on(window.Cluster.ClusterEvent.CLICK, (element) => { - if (clickTimeoutRef.current) { - clearTimeout(clickTimeoutRef.current); - clickTimeoutRef.current = null; - return; - } else { - clickTimeoutRef.current = setTimeout(() => { - let imageIds = []; - if (element.isCluster) { - imageIds = clusterRef.current.getLeaves(element.id).map(item => item.properties.id).filter(Boolean); - } else { - imageIds = [element.properties.id]; - } - clickTimeoutRef.current = null; - setClusterLeaveIds(imageIds); - }, 300); - } - }); - }, [images, getPoints]); - - const renderBaiduMap = useCallback(() => { - if (!mapRef.current || !window.BMapGL.Map) return; - let { center, zoom } = loadMapState(); - let userPosition = { lng: 116.40396418840683, lat: 39.915106021711345 }; - // ask for user location - if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition((userInfo) => { - const gcPosition = wgs84_to_gcj02(userInfo.coords.longitude, userInfo.coords.latitude); - const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat); - const { lng, lat } = bdPosition; - userPosition = new window.BMapGL.Point(lng, lat); - center = userPosition; - window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_CENTER_KEY, center); - }); - } - const mapTypeValue = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_TYPE_KEY); - mapRef.current = new window.BMapGL.Map('sf-metadata-map-container', { - enableMapClick: false, - minZoom: MIN_ZOOM, - maxZoom: MAX_ZOOM, - mapType: getBMapType(mapTypeValue), - }); - - if (isValidPosition(center?.lng, center?.lat)) { - mapRef.current.centerAndZoom(center, zoom); - } - - mapRef.current.enableScrollWheelZoom(true); - addMapController(); - - initializeUserMarker(userPosition); - initializeCluster(); - - batchIndexRef.current = 0; - }, [addMapController, initializeCluster, initializeUserMarker, getBMapType, loadMapState]); - - const handleClose = useCallback(() => { - setImageIndex(0); - setClusterLeaveIds([]); - }, []); - - const moveToPrevImage = useCallback(() => { - setImageIndex((imageIndex + clusterLeaves.length - 1) % clusterLeaves.length); - }, [imageIndex, clusterLeaves.length]); - - const moveToNextImage = useCallback(() => { - setImageIndex((imageIndex + 1) % clusterLeaves.length); - }, [imageIndex, clusterLeaves.length]); - - useEffect(() => { - const modifyMapTypeSubscribe = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.MODIFY_MAP_TYPE, (newType) => { - window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_TYPE_KEY, newType); - const mapType = getBMapType(newType); - mapRef.current && mapRef.current.setMapType(mapType); - }); - - return () => { - modifyMapTypeSubscribe(); - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (mapInfo.type === MAP_PROVIDER.B_MAP) { - loadBMap(mapInfo.key).then(() => renderBaiduMap()); - return () => { - window.renderMap = null; - }; - } - return; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( -
-
- {clusterLeaveIds.length > 0 && ( - - - - )} -
- ); -}; - -MapView.propTypes = { - images: PropTypes.array, - onOpenCluster: PropTypes.func, -}; - -export default MapView; diff --git a/frontend/src/metadata/views/map/map-view/overlay/custom-avatar-overlay.js b/frontend/src/metadata/views/map/overlay/custom-avatar-overlay.js similarity index 100% rename from frontend/src/metadata/views/map/map-view/overlay/custom-avatar-overlay.js rename to frontend/src/metadata/views/map/overlay/custom-avatar-overlay.js diff --git a/frontend/src/metadata/views/map/map-view/overlay/custom-image-overlay.js b/frontend/src/metadata/views/map/overlay/custom-image-overlay.js similarity index 100% rename from frontend/src/metadata/views/map/map-view/overlay/custom-image-overlay.js rename to frontend/src/metadata/views/map/overlay/custom-image-overlay.js diff --git a/frontend/src/metadata/views/map/map-view/overlay/index.js b/frontend/src/metadata/views/map/overlay/index.js similarity index 100% rename from frontend/src/metadata/views/map/map-view/overlay/index.js rename to frontend/src/metadata/views/map/overlay/index.js