diff --git a/frontend/src/metadata/components/view-toolbar/index.js b/frontend/src/metadata/components/view-toolbar/index.js index 7bfb186b5c..7227ecea75 100644 --- a/frontend/src/metadata/components/view-toolbar/index.js +++ b/frontend/src/metadata/components/view-toolbar/index.js @@ -113,10 +113,11 @@ 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 8d2e0721f4..39fc30be0a 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,14 +1,19 @@ -import React, { useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; -import { PRIVATE_COLUMN_KEY } from '../../../constants'; -import { FilterSetter, MapTypeSetter } from '../../data-process-setter'; +import { EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY, VIEW_TYPE } from '../../../constants'; +import { FilterSetter, GalleryGroupBySetter, GallerySliderSetter, MapTypeSetter, SortSetter } from '../../data-process-setter'; +import { gettext } from '../../../../utils/constants'; const MapViewToolBar = ({ + isCustomPermission, readOnly, - view, collaborators, modifyFilters, + onToggleDetail, }) => { + const [showGalleryToolbar, setShowGalleryToolbar] = useState(false); + const [view, setView] = useState({}); + const viewType = useMemo(() => view.type, [view]); const viewColumns = useMemo(() => { if (!view) return []; @@ -16,38 +21,87 @@ const MapViewToolBar = ({ }, [view]); const filterColumns = useMemo(() => { - return viewColumns.filter(c => c.key !== PRIVATE_COLUMN_KEY.FILE_TYPE); + 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.MAP_GALLERY_VIEW_CHANGE, { sorts }); + }, []); + + const setMapView = useCallback(view => setView(view), []); + + useEffect(() => { + const unsubscribeToggleViewToolbarMode = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.TOGGLE_MAP_VIEW_TOOLBAR, onToggle); + const unsubscribeView = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.MAP_VIEW, setMapView); + return () => { + unsubscribeToggleViewToolbarMode(); + unsubscribeView(); + }; + }, [setMapView, onToggle]); + + useEffect(() => { + setView(window.sfMetadataStore.data.view); + }, []); + return ( <> -
- - -
-
+ {showGalleryToolbar ? ( +
+ <> + + + + + {!isCustomPermission && ( +
+ +
+ )} +
+ ) : + <> +
+ + +
+
+ } ); }; 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/constants/event-bus-type.js b/frontend/src/metadata/constants/event-bus-type.js index c05d276939..9cc80cf45b 100644 --- a/frontend/src/metadata/constants/event-bus-type.js +++ b/frontend/src/metadata/constants/event-bus-type.js @@ -76,4 +76,6 @@ export const EVENT_BUS_TYPE = { // map SWITCH_MAP_TYPE: 'switch_map_type', + TOGGLE_MAP_VIEW_TOOLBAR: 'toggle_map_view_toolbar', + MAP_VIEW: 'map_view', }; diff --git a/frontend/src/metadata/constants/index.js b/frontend/src/metadata/constants/index.js index 01604d5dde..0b83427af8 100644 --- a/frontend/src/metadata/constants/index.js +++ b/frontend/src/metadata/constants/index.js @@ -142,10 +142,15 @@ export const GALLERY_DATE_MODE = { }; export const MAP_TYPE = { - MAP: 'map', + NORMAL_MAP: 'normal_map', SATELLITE: 'satellite', }; +export const MAP_VIEW_TOOLBAR_MODE = { + MAP: 'map', + GALLERY: 'gallery', +}; + export const UNCATEGORIZED = '_uncategorized'; export const PASTE_SOURCE = { diff --git a/frontend/src/metadata/constants/view.js b/frontend/src/metadata/constants/view.js index fd7beb7aa0..1cede63b77 100644 --- a/frontend/src/metadata/constants/view.js +++ b/frontend/src/metadata/constants/view.js @@ -98,7 +98,7 @@ export const VIEW_TYPE_DEFAULT_SORTS = { [VIEW_TYPE.GALLERY]: [{ column_key: PRIVATE_COLUMN_KEY.FILE_CTIME, sort_type: SORT_TYPE.DOWN }], [VIEW_TYPE.FACE_RECOGNITION]: [{ column_key: PRIVATE_COLUMN_KEY.FILE_CTIME, sort_type: SORT_TYPE.DOWN }], [VIEW_TYPE.KANBAN]: [], - [VIEW_TYPE.MAP]: [], + [VIEW_TYPE.MAP]: [{ column_key: PRIVATE_COLUMN_KEY.FILE_CTIME, sort_type: SORT_TYPE.DOWN }], }; export const VIEW_SORT_COLUMN_RULES = { @@ -106,7 +106,7 @@ export const VIEW_SORT_COLUMN_RULES = { [VIEW_TYPE.GALLERY]: (column) => GALLERY_SORT_COLUMN_OPTIONS.includes(column.type) || GALLERY_SORT_PRIVATE_COLUMN_KEYS.includes(column.key), [VIEW_TYPE.FACE_RECOGNITION]: (column) => GALLERY_SORT_COLUMN_OPTIONS.includes(column.type) || GALLERY_SORT_PRIVATE_COLUMN_KEYS.includes(column.key), [VIEW_TYPE.KANBAN]: (column) => SORT_COLUMN_OPTIONS.includes(column.type), - [VIEW_TYPE.MAP]: () => {}, + [VIEW_TYPE.MAP]: (column) => GALLERY_SORT_COLUMN_OPTIONS.includes(column.type) || GALLERY_SORT_PRIVATE_COLUMN_KEYS.includes(column.key), }; export const VIEW_FIRST_SORT_COLUMN_RULES = { @@ -114,7 +114,7 @@ export const VIEW_FIRST_SORT_COLUMN_RULES = { [VIEW_TYPE.GALLERY]: (column) => GALLERY_FIRST_SORT_COLUMN_OPTIONS.includes(column.type) || GALLERY_FIRST_SORT_PRIVATE_COLUMN_KEYS.includes(column.key), [VIEW_TYPE.FACE_RECOGNITION]: (column) => GALLERY_FIRST_SORT_COLUMN_OPTIONS.includes(column.type) || GALLERY_FIRST_SORT_PRIVATE_COLUMN_KEYS.includes(column.key), [VIEW_TYPE.KANBAN]: (column) => SORT_COLUMN_OPTIONS.includes(column.type), - [VIEW_TYPE.MAP]: () => {}, + [VIEW_TYPE.MAP]: (column) => GALLERY_FIRST_SORT_COLUMN_OPTIONS.includes(column.type) || GALLERY_FIRST_SORT_PRIVATE_COLUMN_KEYS.includes(column.key), }; export const KANBAN_SETTINGS_KEYS = { diff --git a/frontend/src/metadata/store/index.js b/frontend/src/metadata/store/index.js index 3bf4422302..a07a40a2f5 100644 --- a/frontend/src/metadata/store/index.js +++ b/frontend/src/metadata/store/index.js @@ -623,6 +623,22 @@ class Store { this.applyOperation(operation); }; + // map + deleteLocationPhotos = (rows_ids) => { + if (!Array.isArray(rows_ids) || rows_ids.length === 0) return; + + const type = OPERATION_TYPE.DELETE_LOCATION_PHOTOS; + const valid_rows_ids = rows_ids.filter((rowId) => { + const row = getRowById(this.data, rowId); + return row && this.context.canModifyRow(row); + }); + const deleted_rows = valid_rows_ids.map((rowId) => getRowById(this.data, rowId)); + const operation = this.createOperation({ + type, repo_id: this.repoId, rows_ids, deleted_rows + }); + this.applyOperation(operation); + }; + } export default Store; diff --git a/frontend/src/metadata/store/operations/apply.js b/frontend/src/metadata/store/operations/apply.js index 7e809ea710..64df8f7eca 100644 --- a/frontend/src/metadata/store/operations/apply.js +++ b/frontend/src/metadata/store/operations/apply.js @@ -335,6 +335,21 @@ export default function apply(data, operation) { return data; } + // map + case OPERATION_TYPE.DELETE_LOCATION_PHOTOS: { + const { rows_ids } = operation; + const idNeedDeletedMap = rows_ids.reduce((currIdNeedDeletedMap, rowId) => ({ ...currIdNeedDeletedMap, [rowId]: true }), {}); + data.rows = data.rows.filter((row) => !idNeedDeletedMap[row._id]); + data.recordsCount = data.rows.length; + // delete rows in id_row_map + rows_ids.forEach(rowId => { + delete data.id_row_map[rowId]; + }); + + data.row_ids = data.row_ids.filter(row_id => !idNeedDeletedMap[row_id]); + return data; + } + default: { return data; } diff --git a/frontend/src/metadata/store/operations/constants.js b/frontend/src/metadata/store/operations/constants.js index 2872778e2a..ee4ce65503 100644 --- a/frontend/src/metadata/store/operations/constants.js +++ b/frontend/src/metadata/store/operations/constants.js @@ -34,6 +34,9 @@ export const OPERATION_TYPE = { // tag UPDATE_FILE_TAGS: 'update_file_tags', + + // map + DELETE_LOCATION_PHOTOS: 'delete_location_photos', }; export const COLUMN_DATA_OPERATION_TYPE = { @@ -74,6 +77,8 @@ export const OPERATION_ATTRIBUTES = { [OPERATION_TYPE.MODIFY_SETTINGS]: ['repo_id', 'view_id', 'settings'], [OPERATION_TYPE.UPDATE_FILE_TAGS]: ['repo_id', 'file_tags_data'], + + [OPERATION_TYPE.DELETE_LOCATION_PHOTOS]: ['repo_id', 'rows_ids', 'deleted_rows'], }; export const UNDO_OPERATION_TYPE = [ diff --git a/frontend/src/metadata/views/map/cluster-photos/index.css b/frontend/src/metadata/views/map/cluster-photos/index.css new file mode 100644 index 0000000000..b2f844b537 --- /dev/null +++ b/frontend/src/metadata/views/map/cluster-photos/index.css @@ -0,0 +1,54 @@ +.sf-metadata-map-photos-container { + padding: 0 !important; + overflow-y: hidden !important; +} + +.sf-metadata-map-photos-container .sf-metadata-map-photos-header { + height: 48px; + display: flex; + align-items: center; + padding: 0 16px; +} + +.sf-metadata-map-photos-container .sf-metadata-icon-btn { + margin-left: -4px; + border-radius: 3px; +} + +.sf-metadata-map-photos-container .sf-metadata-icon-btn:hover { + background-color: #EFEFEF; + cursor: pointer; +} + +.sf-metadata-map-photos-container .sf-metadata-map-photos-header .sf-metadata-map-photos-header-back { + font-size: 14px; + height: 24px; + min-width: 24px; + display: flex; + align-items: center; + justify-content: center; + margin-left: -5px; + border-radius: 3px; +} + +.sf-metadata-map-photos-container .sf-metadata-map-photos-header .sf-metadata-map-photos-header-back:hover { + background-color: #EFEFEF; + cursor: pointer; +} + +.sf-metadata-map-photos-container .sf-metadata-map-photos-header-back .sf3-font-arrow { + color: #666; + font-size: 14px !important; +} + +.sf-metadata-map-photos-container .sf-metadata-map-photos-header .sf-metadata-map-location { + margin-left: 4px; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sf-metadata-map-photos-container .sf-metadata-gallery-container { + height: calc(100% - 48px); +} diff --git a/frontend/src/metadata/views/map/cluster-photos/index.js b/frontend/src/metadata/views/map/cluster-photos/index.js new file mode 100644 index 0000000000..921b1645e7 --- /dev/null +++ b/frontend/src/metadata/views/map/cluster-photos/index.js @@ -0,0 +1,145 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import deepCopy from 'deep-copy'; +import { CenteredLoading } from '@seafile/sf-metadata-ui-component'; +import Gallery from '../../gallery/main'; +import { gettext } from '../../../../utils/constants'; +import { EVENT_BUS_TYPE, PER_LOAD_NUMBER } from '../../../constants'; +import metadataAPI from '../../../api'; +import { normalizeColumns } from '../../../utils/column'; +import Metadata from '../../../model/metadata'; +import { Utils } from '../../../../utils/utils'; +import toaster from '../../../../components/toast'; +import { useMetadataView } from '../../../hooks/metadata-view'; + +import './index.css'; + +const ClusterPhotos = ({ view, markerIds, onClose, onDelete }) => { + const [isLoading, setLoading] = useState(true); + const [metadata, setMetadata] = useState({ rows: [] }); + + const { deleteFilesCallback } = useMetadataView(); + + const repoID = window.sfMetadataContext.getSetting('repoID'); + + const loadData = useCallback((view) => { + setLoading(true); + const params = { + view_id: view._id, + start: 0, + limit: PER_LOAD_NUMBER, + }; + metadataAPI.getMetadata(repoID, params).then(res => { + const rows = res?.data?.results || []; + const filteredRows = rows.filter(row => markerIds.includes(row._id)); + const columns = normalizeColumns(res?.data?.metadata); + const metadata = new Metadata({ rows: filteredRows, columns, view }); + metadata.hasMore = rows.length >= PER_LOAD_NUMBER; + setMetadata(metadata); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MAP_VIEW, metadata.view); + setLoading(false); + }).catch(error => { + const errorMessage = Utils.getErrorMsg(error); + toaster.danger(errorMessage); + setLoading(false); + }); + }, [repoID, markerIds]); + + 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(); + } + + onDelete(ids); + }, [metadata, onClose, onDelete]); + + const handelDelete = useCallback((deletedImages, { success_callback } = {}) => { + if (!deletedImages.length) return; + let recordIds = []; + let paths = []; + let fileNames = []; + deletedImages.forEach((record) => { + const { id, parentDir, name } = record || {}; + if (parentDir && name) { + const path = Utils.joinPath(parentDir, name); + paths.push(path); + fileNames.push(name); + recordIds.push(id); + } + }); + window.sfMetadataContext.batchDeleteFiles(repoID, paths).then(res => { + deletedByIds(recordIds); + deleteFilesCallback(paths, fileNames); + let msg = fileNames.length > 1 + ? gettext('Successfully deleted {name} and {n} other items') + : gettext('Successfully deleted {name}'); + msg = msg.replace('{name}', fileNames[0]) + .replace('{n}', fileNames.length - 1); + toaster.success(msg); + success_callback && success_callback(); + }).catch(error => { + toaster.danger(gettext('Failed to delete records')); + }); + }, [deleteFilesCallback, repoID, deletedByIds]); + + const handleViewChange = useCallback((update) => { + metadataAPI.modifyView(repoID, view._id, update).then(res => { + const newView = { ...view, ...update }; + loadData(newView); + }).catch(error => { + const errorMessage = Utils.getErrorMsg(error); + toaster.danger(errorMessage); + }); + }, [view, repoID, loadData]); + + useEffect(() => { + loadData({ _id: view._id, sorts: view.sorts }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + const unsubscribeViewChange = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.MAP_GALLERY_VIEW_CHANGE, handleViewChange); + return () => { + unsubscribeViewChange(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + isLoading ? ( + + ) : ( +
+
+
+ +
+
{gettext('Location')}
+
+ +
+ ) + ); +}; + +ClusterPhotos.propTypes = { + view: PropTypes.object, + markerIds: PropTypes.array, + onClose: PropTypes.func, + onDelete: PropTypes.func, +}; + +export default ClusterPhotos; diff --git a/frontend/src/metadata/views/map/geolocation-control.js b/frontend/src/metadata/views/map/control/geolocation-control.js similarity index 94% rename from frontend/src/metadata/views/map/geolocation-control.js rename to frontend/src/metadata/views/map/control/geolocation-control.js index 6a2b227daa..8ce5e38053 100644 --- a/frontend/src/metadata/views/map/geolocation-control.js +++ b/frontend/src/metadata/views/map/control/geolocation-control.js @@ -1,5 +1,5 @@ -import { mediaUrl } from '../../../utils/constants'; -import { Utils } from '../../../utils/utils'; +import { mediaUrl } from '../../../../utils/constants'; +import { Utils } from '../../../../utils/utils'; export function createBMapGeolocationControl(BMap, callback) { function GeolocationControl() { diff --git a/frontend/src/metadata/views/map/control/index.js b/frontend/src/metadata/views/map/control/index.js new file mode 100644 index 0000000000..46577bee52 --- /dev/null +++ b/frontend/src/metadata/views/map/control/index.js @@ -0,0 +1,7 @@ +import { createBMapGeolocationControl } from './geolocation-control'; +import { createBMapZoomControl } from './zoom-control'; + +export { + createBMapGeolocationControl, + createBMapZoomControl +}; diff --git a/frontend/src/metadata/views/map/zoom-control.js b/frontend/src/metadata/views/map/control/zoom-control.js similarity index 87% rename from frontend/src/metadata/views/map/zoom-control.js rename to frontend/src/metadata/views/map/control/zoom-control.js index 3600ba85ff..995590dc31 100644 --- a/frontend/src/metadata/views/map/zoom-control.js +++ b/frontend/src/metadata/views/map/control/zoom-control.js @@ -1,4 +1,4 @@ -import { Utils } from '../../../utils/utils'; +import { Utils } from '../../../../utils/utils'; export function createBMapZoomControl(BMap, callback) { function ZoomControl() { @@ -12,7 +12,7 @@ export function createBMapZoomControl(BMap, callback) { div.style = 'display: flex; justify-content: center; align-items: center;'; const zoomInButton = document.createElement('button'); - zoomInButton.className = 'sf-BMap-zoom-button'; + zoomInButton.className = 'sf-BMap-zoom-button btn btn-secondary'; zoomInButton.style = 'display: flex; justify-content: center; align-items: center;'; zoomInButton.innerHTML = ''; div.appendChild(zoomInButton); @@ -22,7 +22,7 @@ export function createBMapZoomControl(BMap, callback) { div.appendChild(divider); const zoomOutButton = document.createElement('button'); - zoomOutButton.className = 'sf-BMap-zoom-button'; + zoomOutButton.className = 'sf-BMap-zoom-button btn btn-secondary'; zoomOutButton.style = 'display: flex; justify-content: center; align-items: center;'; zoomOutButton.innerHTML = ''; div.appendChild(zoomOutButton); @@ -35,8 +35,8 @@ export function createBMapZoomControl(BMap, callback) { const updateButtonStates = () => { const zoomLevel = map.getZoom(); - const maxZoom = map.getMaxZoom(); - const minZoom = map.getMinZoom(); + const maxZoom = map.getMapType().getMaxZoom(); + const minZoom = map.getMapType().getMinZoom(); zoomInButton.disabled = zoomLevel >= maxZoom; zoomOutButton.disabled = zoomLevel <= minZoom; diff --git a/frontend/src/metadata/views/map/index.css b/frontend/src/metadata/views/map/index.css index 3a8524d8f2..98cbfca6d2 100644 --- a/frontend/src/metadata/views/map/index.css +++ b/frontend/src/metadata/views/map/index.css @@ -1,30 +1,38 @@ .sf-metadata-view-map { + width: 100%; + height: 100%; display: flex; flex-direction: column; } .sf-metadata-view-map .sf-metadata-map-container { - width: 100%;; + width: 100%; height: 100%; - min-height: 0; } .sf-metadata-view-map .custom-image-container { - padding: 4px; - background: #fff; - width: 80px; - height: 80px; - cursor: default; - border-radius: 4px; - box-shadow: 0 1px 3px 0 rgb(0 0 0 / 10%); + width: 86px; + height: 86px; + background-color: #fff; + padding: 3px; + border-radius: 6px; position: relative; + cursor: default; +} + +.sf-metadata-view-map .custom-image-container img { + width: 100%; + height: 100%; + border-radius: 6px; } .sf-metadata-view-map .custom-image-number { position: absolute; right: -15px; - top: -8px; - padding: 0 12px; + top: -16px; + width: 32px; + height: 32px; + padding: 6px; background: #007bff; color: #fff; border-radius: 50%; @@ -33,26 +41,23 @@ line-height: 20px; } -.sf-metadata-view-map .custom-image-container .plugin-label-arrow { +.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; - bottom: 5px; - transform: translate( -50%, 100%); - left: 50%; - color: #fff; - display: inline-block; - line-height: 16px; - height: 16px; + 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 .image-overlay-arrow { - bottom: 5px; - color: #fff; - display: inline-block; - height: 16px; - left: 50%; - line-height: 16px; - position: absolute; - transform: translate(-50%, 100%); +.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 { @@ -66,14 +71,21 @@ border-left: 10px solid transparent; border-right: 10px solid transparent; border-top: 10px solid #fff; + border-radius: 2px; } -.sf-metadata-view-map .sf-BMap-geolocation-control { - background-color: #ffffff; - box-shadow: 0 0 4px rgb(0 0 0 / 12%); +.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); +} + +.sf-metadata-view-map .sf-BMap-geolocation-control, +.sf-metadata-view-map .sf-BMap-zoom-control { + background-color: #fff; + opacity: 1; + overflow: hidden; border-radius: 6px; - text-align: center; - color: #212529; + box-shadow: -2px -2px 4px 2px rgba(0, 0, 0, 0.1); } .sf-metadata-view-map .sf-BMap-geolocation-control-loading { @@ -81,24 +93,17 @@ } .sf-metadata-view-map .sf-BMap-geolocation-control:hover, -.sf-metadata-view-map .sf-BMap-zoom-button:hover { +.sf-metadata-view-map .sf-BMap-zoom-button:not(.disabled):hover { background-color: #f5f5f5; cursor: pointer; } -.sf-metadata-view-map .sf-BMap-zoom-control { - background-color: #fff; - opacity: 1; - overflow: hidden; - border-radius: 6px; - box-shadow: 2px 2px 4px 2px rgba(0, 0, 0, 0.1); -} - .sf-metadata-view-map .sf-BMap-zoom-button { width: 100%; height: 100%; padding: 0; margin: 0; + color: #666; background-color: #fff; border: none; overflow: hidden; @@ -109,15 +114,17 @@ .sf-metadata-view-map .sf-BMap-zoom-button .zoom-out-icon { width: 18px; height: 18px; - fill: #666; + fill: currentColor; } -.sf-metadata-view-map .sf-BMap-zoom-button:hover .zoom-in-icon, -.sf-metadata-view-map .sf-BMap-zoom-button:hover .zoom-out-icon { - fill: #212529; +.sf-metadata-view-map .sf-BMap-zoom-button:not(:disabled):active:focus { + box-shadow: none; } -.sf-metadata-view-map .sf-BMap-zoom-button:disabled .zoom-in-icon, -.sf-metadata-view-map .sf-BMap-zoom-button:disabled .zoom-out-icon { - fill: #ccc; +.sf-metadata-view-map .sf-BMap-zoom-button:hover { + color: #212529; +} + +.sf-metadata-view-map .sf-BMap-zoom-button:disabled { + color: #ccc !important; } diff --git a/frontend/src/metadata/views/map/index.js b/frontend/src/metadata/views/map/index.js index 1f735b16e4..cf57e2bda6 100644 --- a/frontend/src/metadata/views/map/index.js +++ b/frontend/src/metadata/views/map/index.js @@ -1,38 +1,21 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { CenteredLoading } from '@seafile/sf-metadata-ui-component'; -import loadBMap, { initMapInfo } from '../../../utils/map-utils'; -import { wgs84_to_gcj02, gcj02_to_bd09 } from '../../../utils/coord-transform'; -import { isValidPosition } from '../../utils/validate'; -import { MAP_TYPE as MAP_PROVIDER } from '../../../constants'; -import { appAvatarURL, baiduMapKey, gettext, googleMapKey, mediaUrl, siteRoot, thumbnailSizeForGrid } from '../../../utils/constants'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { getFileNameFromRecord, getFileTypeFromRecord, getImageLocationFromRecord, getParentDirFromRecord, getRecordIdFromRecord } from '../../utils/cell'; +import ClusterPhotos from './cluster-photos'; +import Main from './main'; +import { EVENT_BUS_TYPE, PREDEFINED_FILE_TYPE_OPTION_KEY } from '../../constants'; import { useMetadataView } from '../../hooks/metadata-view'; -import { EVENT_BUS_TYPE, PREDEFINED_FILE_TYPE_OPTION_KEY, MAP_TYPE } from '../../constants'; -import { getRecordIdFromRecord, getFileNameFromRecord, getImageLocationFromRecord, getParentDirFromRecord, - getFileTypeFromRecord -} from '../../utils/cell'; import { Utils } from '../../../utils/utils'; -import customImageOverlay from './custom-image-overlay'; -import customAvatarOverlay from './custom-avatar-overlay'; -import { createBMapGeolocationControl } from './geolocation-control'; -import toaster from '../../../components/toast'; +import { siteRoot, thumbnailSizeForGrid } from '../../../utils/constants'; +import { isValidPosition } from '../../utils/validate'; +import { gcj02_to_bd09, wgs84_to_gcj02 } from '../../../utils/coord-transform'; import './index.css'; -import { createBMapZoomControl } from './zoom-control'; - -const DEFAULT_POSITION = { lng: 104.195, lat: 35.861 }; -const DEFAULT_ZOOM = 4; -const BATCH_SIZE = 500; const Map = () => { - const [isLoading, setIsLoading] = useState(true); + const [showGallery, setShowGallery] = useState(false); + const [markerIds, setMarkerIds] = useState([]); + const { metadata, store } = useMetadataView(); - const mapRef = useRef(null); - const clusterRef = useRef(null); - const batchIndexRef = useRef(0); - - const { metadata } = useMetadataView(); - - const mapInfo = useMemo(() => initMapInfo({ baiduMapKey, googleMapKey }), []); const repoID = window.sfMetadataContext.getSetting('repoID'); const validImages = useMemo(() => { @@ -54,150 +37,36 @@ const Map = () => { return { id, src, lng: bdPosition.lng, lat: bdPosition.lat }; }) .filter(Boolean); - }, [repoID, metadata]); + }, [repoID, metadata.rows]); - const addMapController = useCallback(() => { - const ZoomControl = createBMapZoomControl(window.BMap); - const zoomControl = new ZoomControl(); - const GeolocationControl = createBMapGeolocationControl(window.BMap, (err, point) => { - if (!err && point) { - mapRef.current.setCenter({ lng: point.lng, lat: point.lat }); - } - }); + const openGallery = useCallback((cluster_marker_ids) => { + setMarkerIds(cluster_marker_ids); + setShowGallery(true); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_MAP_VIEW_TOOLBAR, true); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MAP_VIEW, metadata.view); + }, [metadata.view]); - const geolocationControl = new GeolocationControl(); - - mapRef.current.addControl(zoomControl); - mapRef.current.addControl(geolocationControl); + const closeGallery = useCallback(() => { + setShowGallery(false); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_MAP_VIEW_TOOLBAR, false); }, []); - const renderMarkersBatch = useCallback(() => { - if (!validImages.length || !clusterRef.current) return; - - const startIndex = batchIndexRef.current * BATCH_SIZE; - const endIndex = Math.min(startIndex + BATCH_SIZE, validImages.length); - const batchMarkers = []; - - for (let i = startIndex; i < endIndex; i++) { - const image = validImages[i]; - const { lng, lat } = image; - const point = new window.BMap.Point(lng, lat); - const marker = customImageOverlay(point, image.src); - batchMarkers.push(marker); - } - - clusterRef.current.addMarkers(batchMarkers); - - if (endIndex < validImages.length) { - batchIndexRef.current += 1; - setTimeout(renderMarkersBatch, 20); // Schedule the next batch - } - }, [validImages]); - - const initializeClusterer = useCallback(() => { - if (mapRef.current && !clusterRef.current) { - clusterRef.current = new window.BMapLib.MarkerClusterer(mapRef.current); - } - }, []); - - const initializeUserMarker = useCallback(() => { - if (!window.BMap) return; - - const imageUrl = `${mediaUrl}/img/marker.png`; - const addMarker = (lng, lat) => { - const gcPosition = wgs84_to_gcj02(lng, lat); - const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat); - const point = new window.BMap.Point(bdPosition.lng, bdPosition.lat); - const avatarMarker = customAvatarOverlay(point, appAvatarURL, imageUrl); - mapRef.current.addOverlay(avatarMarker); - }; - - if (!navigator.geolocation) { - addMarker(DEFAULT_POSITION.lng, DEFAULT_POSITION.lat); - return; - } - navigator.geolocation.getCurrentPosition( - position => addMarker(position.coords.longitude, position.coords.latitude), - () => { - addMarker(DEFAULT_POSITION.lng, DEFAULT_POSITION.lat); - toaster.danger(gettext('Failed to get user location')); - } - ); - }, []); - - const getMapType = useCallback((type) => { - if (!mapRef.current) return; - switch (type) { - case MAP_TYPE.SATELLITE: - return window.BMAP_SATELLITE_MAP; - default: - return window.BMAP_NORMAL_MAP; - } - }, []); - - const renderBaiduMap = useCallback(() => { - setIsLoading(false); - if (!window.BMap.Map) return; - let mapCenter = window.sfMetadataContext.localStorage.getItem('map-center') || DEFAULT_POSITION; - // ask for user location - if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition((userInfo) => { - mapCenter = { lng: userInfo.coords.longitude, lat: userInfo.coords.latitude }; - window.sfMetadataContext.localStorage.setItem('map-center', mapCenter); - }); - } - if (!isValidPosition(mapCenter?.lng, mapCenter?.lat)) return; - - const gcPosition = wgs84_to_gcj02(mapCenter.lng, mapCenter.lat); - const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat); - const { lng, lat } = bdPosition; - - mapRef.current = new window.BMap.Map('sf-metadata-map-container', { enableMapClick: false }); - const point = new window.BMap.Point(lng, lat); - mapRef.current.centerAndZoom(point, DEFAULT_ZOOM); - mapRef.current.enableScrollWheelZoom(true); - // const type = window.sfMetadataContext.localStorage.getItem('map-type'); - // mapRef.current.setMapType(getMapType(type)); - - addMapController(); - initializeUserMarker(); - initializeClusterer(); - - batchIndexRef.current = 0; // Reset batch index - renderMarkersBatch(); - }, [addMapController, initializeClusterer, initializeUserMarker, renderMarkersBatch]); + const onDeleteLocationPhotos = useCallback((ids) => { + store.deleteLocationPhotos(ids); + }, [store]); useEffect(() => { - const switchMapTypeSubscribe = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.SWITCH_MAP_TYPE, (newType) => { - window.sfMetadataContext.localStorage.setItem('map-type', newType); - mapRef.current && mapRef.current.setMapType(getMapType(newType)); - }); - - return () => { - switchMapTypeSubscribe(); - }; - - }, [getMapType]); - - useEffect(() => { - if (mapInfo.type === MAP_PROVIDER.B_MAP) { - window.renderMap = renderBaiduMap; - loadBMap(mapInfo.key).then(() => renderBaiduMap()); - return () => { - window.renderMap = null; - }; - } - return; - }, [mapInfo, renderBaiduMap]); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MAP_VIEW, metadata.view); + }, [metadata.view]); return ( -
- {isLoading ? ( - + <> + {showGallery ? ( + ) : ( -
+
)} -
+ ); }; diff --git a/frontend/src/metadata/views/map/main.js b/frontend/src/metadata/views/map/main.js new file mode 100644 index 0000000000..d1f04d94c0 --- /dev/null +++ b/frontend/src/metadata/views/map/main.js @@ -0,0 +1,177 @@ +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import PropTypes from 'prop-types'; +import loadBMap, { initMapInfo } from '../../../utils/map-utils'; +import { appAvatarURL, baiduMapKey, gettext, 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 } from '../../constants'; +import { createBMapGeolocationControl, createBMapZoomControl } from './control'; +import { customAvatarOverlay, customImageOverlay } from './overlay'; +import toaster from '../../../components/toast'; + +const DEFAULT_POSITION = { lng: 104.195, lat: 35.861 }; +const DEFAULT_ZOOM = 4; +const BATCH_SIZE = 500; + +const Main = ({ validImages, onOpen }) => { + const mapInfo = useMemo(() => initMapInfo({ baiduMapKey, googleMapKey }), []); + + const mapRef = useRef(null); + const clusterRef = useRef(null); + const batchIndexRef = useRef(0); + + const addMapController = useCallback(() => { + const ZoomControl = createBMapZoomControl(window.BMap); + const zoomControl = new ZoomControl(); + const GeolocationControl = createBMapGeolocationControl(window.BMap, (err, point) => { + if (!err && point) { + mapRef.current.setCenter({ lng: point.lng, lat: point.lat }); + } + }); + + const geolocationControl = new GeolocationControl(); + + mapRef.current.addControl(zoomControl); + mapRef.current.addControl(geolocationControl); + }, []); + + const initializeUserMarker = useCallback(() => { + if (!window.BMap) return; + + const imageUrl = `${mediaUrl}/img/marker.png`; + const addMarker = (lng, lat) => { + const gcPosition = wgs84_to_gcj02(lng, lat); + const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat); + const point = new window.BMap.Point(bdPosition.lng, bdPosition.lat); + const avatarMarker = customAvatarOverlay(point, appAvatarURL, imageUrl); + mapRef.current && mapRef.current.addOverlay(avatarMarker); + }; + + if (!navigator.geolocation) { + addMarker(DEFAULT_POSITION.lng, DEFAULT_POSITION.lat); + return; + } + navigator.geolocation.getCurrentPosition( + position => addMarker(position.coords.longitude, position.coords.latitude), + () => { + addMarker(DEFAULT_POSITION.lng, DEFAULT_POSITION.lat); + toaster.danger(gettext('Failed to get user location')); + } + ); + }, []); + + const getMapType = useCallback((type) => { + if (!mapRef.current) return; + switch (type) { + case MAP_TYPE.SATELLITE: + return window.BMAP_SATELLITE_MAP; + default: + return window.BMAP_NORMAL_MAP; + } + }, []); + + const onClickMarker = useCallback((e, markers) => { + const imageIds = markers.map(marker => marker._imageId); + onOpen(imageIds); + }, [onOpen]); + + const renderMarkersBatch = useCallback(() => { + if (!validImages.length || !clusterRef.current) return; + + const startIndex = batchIndexRef.current * BATCH_SIZE; + const endIndex = Math.min(startIndex + BATCH_SIZE, validImages.length); + const batchMarkers = []; + + for (let i = startIndex; i < endIndex; i++) { + const image = validImages[i]; + const { lng, lat } = image; + const point = new window.BMap.Point(lng, lat); + const marker = customImageOverlay(point, image, { + callback: (e, markers) => onClickMarker(e, markers) + }); + batchMarkers.push(marker); + } + clusterRef.current.addMarkers(batchMarkers); + + if (endIndex < validImages.length) { + batchIndexRef.current += 1; + setTimeout(renderMarkersBatch, 20); // Schedule the next batch + } + }, [validImages, onClickMarker]); + + const initializeClusterer = useCallback(() => { + if (mapRef.current && !clusterRef.current) { + clusterRef.current = new window.BMapLib.MarkerClusterer(mapRef.current, { + callback: (e, markers) => onClickMarker(e, markers) + }); + } + }, [onClickMarker]); + + const renderBaiduMap = useCallback(() => { + if (!mapRef.current || !window.BMap.Map) return; + let mapCenter = window.sfMetadataContext.localStorage.getItem('map-center') || DEFAULT_POSITION; + // ask for user location + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition((userInfo) => { + mapCenter = { lng: userInfo.coords.longitude, lat: userInfo.coords.latitude }; + window.sfMetadataContext.localStorage.setItem('map-center', mapCenter); + }); + } + if (!isValidPosition(mapCenter?.lng, mapCenter?.lat)) return; + + const gcPosition = wgs84_to_gcj02(mapCenter.lng, mapCenter.lat); + const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat); + const { lng, lat } = bdPosition; + + mapRef.current = new window.BMap.Map('sf-metadata-map-container', { enableMapClick: false }); + const point = new window.BMap.Point(lng, lat); + mapRef.current.centerAndZoom(point, DEFAULT_ZOOM); + mapRef.current.enableScrollWheelZoom(true); + + const savedValue = window.sfMetadataContext.localStorage.getItem('map-type'); + mapRef.current && mapRef.current.setMapType(getMapType(savedValue)); + + addMapController(); + initializeUserMarker(); + initializeClusterer(); + + batchIndexRef.current = 0; + renderMarkersBatch(); + }, [addMapController, initializeClusterer, initializeUserMarker, renderMarkersBatch, getMapType]); + + useEffect(() => { + const switchMapTypeSubscribe = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.SWITCH_MAP_TYPE, (newType) => { + window.sfMetadataContext.localStorage.setItem('map-type', newType); + mapRef.current && mapRef.current.setMapType(getMapType(newType)); + }); + + return () => { + switchMapTypeSubscribe(); + }; + + }, [getMapType]); + + useEffect(() => { + if (mapInfo.type === MAP_PROVIDER.B_MAP) { + loadBMap(mapInfo.key).then(() => renderBaiduMap()); + return () => { + window.renderMap = null; + }; + } + return; + }, [mapInfo, renderBaiduMap]); + + return ( +
+
+
+ ); +}; + +Main.propTypes = { + validImages: PropTypes.array, + onOpen: PropTypes.func, +}; + +export default Main; diff --git a/frontend/src/metadata/views/map/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/custom-avatar-overlay.js rename to frontend/src/metadata/views/map/overlay/custom-avatar-overlay.js diff --git a/frontend/src/metadata/views/map/custom-image-overlay.js b/frontend/src/metadata/views/map/overlay/custom-image-overlay.js similarity index 70% rename from frontend/src/metadata/views/map/custom-image-overlay.js rename to frontend/src/metadata/views/map/overlay/custom-image-overlay.js index ddb61059b9..b58ecd096e 100644 --- a/frontend/src/metadata/views/map/custom-image-overlay.js +++ b/frontend/src/metadata/views/map/overlay/custom-image-overlay.js @@ -1,29 +1,28 @@ -import { Utils } from '../../../utils/utils'; +import { Utils } from '../../../../utils/utils'; -const customImageOverlay = (center, imageUrl) => { - class ImageOverlay extends window.BMap.Overlay { - constructor(center, imageUrl) { - super(); +const customImageOverlay = (center, image, callback) => { + class ImageOverlay extends window.BMapLib.TextIconOverlay { + constructor(center, image, { callback } = {}) { + super(center, '', { styles: [] }); this._center = center; - this._imageUrl = imageUrl; + this._imageUrl = image.src; + this._imageId = image.id; + this._callback = callback; } initialize(map) { this._map = map; const div = document.createElement('div'); div.style.position = 'absolute'; - div.style.width = '80px'; - div.style.height = '80px'; div.style.zIndex = 2000; map.getPanes().markerPane.appendChild(div); this._div = div; - const imageElement = ``; + const imageElement = ``; const htmlString = `
${this._imageUrl ? imageElement : '
'} -
`; const labelDocument = new DOMParser().parseFromString(htmlString, 'text/html'); @@ -33,6 +32,7 @@ const customImageOverlay = (center, imageUrl) => { const eventHandler = (event) => { event.stopPropagation(); event.preventDefault(); + this._callback && this._callback(event, [{ _imageId: this._imageId }]); }; if (Utils.isDesktop()) { @@ -51,7 +51,7 @@ const customImageOverlay = (center, imageUrl) => { } getImageUrl() { - return imageUrl || ''; + return image.src || ''; } getPosition() { @@ -63,7 +63,7 @@ const customImageOverlay = (center, imageUrl) => { } } - return new ImageOverlay(center, imageUrl); + return new ImageOverlay(center, image, callback); }; export default customImageOverlay; diff --git a/frontend/src/metadata/views/map/overlay/index.js b/frontend/src/metadata/views/map/overlay/index.js new file mode 100644 index 0000000000..21f2d71fa9 --- /dev/null +++ b/frontend/src/metadata/views/map/overlay/index.js @@ -0,0 +1,7 @@ +import customAvatarOverlay from './custom-avatar-overlay'; +import customImageOverlay from './custom-image-overlay'; + +export { + customAvatarOverlay, + customImageOverlay +}; diff --git a/media/js/map/marker-clusterer.js b/media/js/map/marker-clusterer.js index 66cc930def..5f7e3100f0 100644 --- a/media/js/map/marker-clusterer.js +++ b/media/js/map/marker-clusterer.js @@ -130,6 +130,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; this._isAverageCenter = opts['isAverageCenter']; } this._styles = opts["styles"] || []; + this._callback = opts["callback"] || function(){}; var that = this; this._map.addEventListener("zoomend",function(){ @@ -443,6 +444,10 @@ var BMapLib = window.BMapLib = BMapLib || {}; return count; }; + MarkerClusterer.prototype.getCallback = function() { + return this._callback; + } + /** * @ignore * Cluster @@ -575,8 +580,12 @@ var BMapLib = window.BMapLib = BMapLib || {}; var thatMap = this._map; var thatBounds = this.getBounds(); - this._clusterMarker.addEventListener("click", function(event){ - thatMap.setViewport(thatBounds); + this._clusterMarker.addEventListener("click",(event) => { + // thatMap.setViewport(thatBounds); + if (this._markerClusterer && typeof this._markerClusterer.getCallback() === 'function') { + const markers = this._markers; + this._markerClusterer.getCallback()(event, markers); + } }); }; diff --git a/media/js/map/text-icon-overlay.js b/media/js/map/text-icon-overlay.js index 725e04efe2..9c4ead85a0 100644 --- a/media/js/map/text-icon-overlay.js +++ b/media/js/map/text-icon-overlay.js @@ -774,7 +774,8 @@ var BMapLib = window.BMapLib = BMapLib || {}; TextIconOverlay.prototype.initialize = function(map){ this._map = map; - this._domElement = document.createElement('div'); + this._domElement = document.createElement('div'); + this._domElement.className = 'custom-image-overlay'; // this._updateCss(); // this._updateText(); this._updatePosition();