From 4d703eb91063107385239a471c7aad1292e91a61 Mon Sep 17 00:00:00 2001 From: zhouwenxuan Date: Wed, 25 Dec 2024 14:38:00 +0800 Subject: [PATCH 1/9] add map mode setter --- .../components/data-process-setter/index.js | 2 + .../map-type-setter/index.css | 57 ++++++++++++++++ .../map-type-setter/index.js | 49 ++++++++++++++ .../view-toolbar/map-view-toolbar/index.js | 3 +- .../src/metadata/constants/event-bus-type.js | 3 + frontend/src/metadata/constants/index.js | 5 ++ .../metadata/views/map/geolocation-control.js | 6 +- frontend/src/metadata/views/map/index.css | 48 ++++++++++++-- frontend/src/metadata/views/map/index.js | 38 +++++++++-- .../src/metadata/views/map/zoom-control.js | 65 +++++++++++++++++++ 10 files changed, 262 insertions(+), 14 deletions(-) create mode 100644 frontend/src/metadata/components/data-process-setter/map-type-setter/index.css create mode 100644 frontend/src/metadata/components/data-process-setter/map-type-setter/index.js create mode 100644 frontend/src/metadata/views/map/zoom-control.js diff --git a/frontend/src/metadata/components/data-process-setter/index.js b/frontend/src/metadata/components/data-process-setter/index.js index 92add309ebf..7d87844e987 100644 --- a/frontend/src/metadata/components/data-process-setter/index.js +++ b/frontend/src/metadata/components/data-process-setter/index.js @@ -5,6 +5,7 @@ import SortSetter from './sort-setter'; import GroupbySetter from './groupby-setter'; import PreHideColumnSetter from './pre-hide-column-setter'; import HideColumnSetter from './hide-column-setter'; +import MapTypeSetter from './map-type-setter'; export { GalleryGroupBySetter, @@ -14,4 +15,5 @@ export { GroupbySetter, PreHideColumnSetter, HideColumnSetter, + MapTypeSetter, }; diff --git a/frontend/src/metadata/components/data-process-setter/map-type-setter/index.css b/frontend/src/metadata/components/data-process-setter/map-type-setter/index.css new file mode 100644 index 00000000000..cac45ca19e5 --- /dev/null +++ b/frontend/src/metadata/components/data-process-setter/map-type-setter/index.css @@ -0,0 +1,57 @@ +.metadata-map-type-setter { + width: fit-content; + height: 36px; + display: flex; + justify-content: center; + align-items: center; + border: 1px solid #e2e2e2; + border-radius: 3px; +} + +.metadata-map-type-setter .metadata-map-type-button { + width: 66px; + height: 28px; + color: #212529; + background-color: #fff; + position: relative; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.875rem; + border: 0; + border-radius: 2px; +} + +.metadata-map-type-setter .metadata-map-type-button:hover { + background-color: #f0f0f0; + cursor: pointer; +} + +.metadata-map-type-setter .metadata-map-type-button.active { + background-color: #f5f5f5; +} + +.metadata-map-type-setter .metadata-map-type-button span { + display: block; + text-align: center; + width: 100%; +} + +.metadata-map-type-button:not(:first-child)::before { + content: ''; + width: 1px; + height: 22px; + background-color: #e2e2e2; + position: absolute; + top: 50%; + transform: translateY(-50%); + transition: opacity 0.3s; + left: -1px; +} + +.metadata-map-type-button:hover::before, +.metadata-map-type-button.active::before, +.metadata-map-type-button:hover + .metadata-map-type-button::before, +.metadata-map-type-button.active + .metadata-map-type-button::before { + opacity: 0; +} \ No newline at end of file diff --git a/frontend/src/metadata/components/data-process-setter/map-type-setter/index.js b/frontend/src/metadata/components/data-process-setter/map-type-setter/index.js new file mode 100644 index 00000000000..8fe2cb1ba5e --- /dev/null +++ b/frontend/src/metadata/components/data-process-setter/map-type-setter/index.js @@ -0,0 +1,49 @@ +import React, { useState, useCallback, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { EVENT_BUS_TYPE, MAP_TYPE } from '../../../constants'; +import { gettext } from '../../../../utils/constants'; + +import './index.css'; + +const TYPE_MAP = { + [MAP_TYPE.MAP]: gettext('Map'), + [MAP_TYPE.SATELLITE]: gettext('Satellite') +}; + +const MapTypeSetter = ({ view }) => { + const [currentType, setCurrentType] = useState(MAP_TYPE.MAP); + + useEffect(() => { + const savedValue = window.sfMetadataContext.localStorage.getItem('map-type', MAP_TYPE.MAP); + setCurrentType(savedValue || MAP_TYPE.MAP); + }, [view?._id]); + + const handleTypeChange = useCallback((newType) => { + setCurrentType(newType); + window.sfMetadataContext.localStorage.setItem('map-type', newType); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SWITCH_MAP_TYPE, newType); + }, []); + + return ( +
+ {Object.entries(TYPE_MAP).map(([type, label]) => ( + + ))} +
+ ); +}; + +MapTypeSetter.propTypes = { + view: PropTypes.shape({ + _id: PropTypes.string + }) +}; + +export default MapTypeSetter; 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 4be657a74f8..8d2e0721f4d 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,7 +1,7 @@ import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; import { PRIVATE_COLUMN_KEY } from '../../../constants'; -import { FilterSetter } from '../../data-process-setter'; +import { FilterSetter, MapTypeSetter } from '../../data-process-setter'; const MapViewToolBar = ({ readOnly, @@ -22,6 +22,7 @@ const MapViewToolBar = ({ return ( <>
+ { }, [repoID, metadata]); const addMapController = useCallback(() => { - var navigation = new window.BMap.NavigationControl(); + 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); - mapRef.current.addControl(navigation); }, []); const renderMarkersBatch = useCallback(() => { @@ -121,6 +125,16 @@ const Map = () => { ); }, []); + 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; @@ -142,6 +156,8 @@ const Map = () => { 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(); @@ -152,7 +168,19 @@ const Map = () => { }, [addMapController, initializeClusterer, initializeUserMarker, renderMarkersBatch]); useEffect(() => { - if (mapInfo.type === MAP_TYPE.B_MAP) { + 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 () => { diff --git a/frontend/src/metadata/views/map/zoom-control.js b/frontend/src/metadata/views/map/zoom-control.js new file mode 100644 index 00000000000..3600ba85ff0 --- /dev/null +++ b/frontend/src/metadata/views/map/zoom-control.js @@ -0,0 +1,65 @@ +import { Utils } from '../../../utils/utils'; + +export function createBMapZoomControl(BMap, callback) { + function ZoomControl() { + this.defaultAnchor = window.BMAP_ANCHOR_BOTTOM_RIGHT; + this.defaultOffset = new BMap.Size(80, Utils.isDesktop() ? 30 : 90); + } + ZoomControl.prototype = new window.BMap.Control(); + ZoomControl.prototype.initialize = function (map) { + const div = document.createElement('div'); + div.className = 'sf-BMap-zoom-control'; + div.style = 'display: flex; justify-content: center; align-items: center;'; + + const zoomInButton = document.createElement('button'); + zoomInButton.className = 'sf-BMap-zoom-button'; + zoomInButton.style = 'display: flex; justify-content: center; align-items: center;'; + zoomInButton.innerHTML = ''; + div.appendChild(zoomInButton); + + const divider = document.createElement('div'); + divider.style = 'height: 22px; width: 1px; background-color: #ccc;'; + div.appendChild(divider); + + const zoomOutButton = document.createElement('button'); + zoomOutButton.className = 'sf-BMap-zoom-button'; + zoomOutButton.style = 'display: flex; justify-content: center; align-items: center;'; + zoomOutButton.innerHTML = ''; + div.appendChild(zoomOutButton); + + if (Utils.isDesktop()) { + setNodeStyle(div, 'height: 40px; width: 111px; line-height: 40px'); + } else { + setNodeStyle(div, 'height: 35px; width: 80px; line-height: 35px; opacity: 0.75'); + } + + const updateButtonStates = () => { + const zoomLevel = map.getZoom(); + const maxZoom = map.getMaxZoom(); + const minZoom = map.getMinZoom(); + + zoomInButton.disabled = zoomLevel >= maxZoom; + zoomOutButton.disabled = zoomLevel <= minZoom; + }; + + zoomInButton.onclick = (e) => { + e.preventDefault(); + map.zoomTo(map.getZoom() + 2); + }; + + zoomOutButton.onclick = (e) => { + e.preventDefault(); + map.zoomTo(map.getZoom() - 2); + }; + + map.addEventListener('zoomend', updateButtonStates); + map.getContainer().appendChild(div); + return div; + }; + + return ZoomControl; +} + +function setNodeStyle(dom, styleText) { + dom.style.cssText += styleText; +} From 86d5d564dad3314b04dcce21beeab321d4871440 Mon Sep 17 00:00:00 2001 From: zhouwenxuan Date: Sat, 28 Dec 2024 11:47:46 +0800 Subject: [PATCH 2/9] show cluster photos --- .../metadata/components/view-toolbar/index.js | 3 +- .../view-toolbar/map-view-toolbar/index.js | 102 ++++++--- .../src/metadata/constants/event-bus-type.js | 2 + frontend/src/metadata/constants/index.js | 7 +- frontend/src/metadata/constants/view.js | 6 +- frontend/src/metadata/store/index.js | 16 ++ .../src/metadata/store/operations/apply.js | 15 ++ .../metadata/store/operations/constants.js | 5 + .../views/map/cluster-photos/index.css | 54 +++++ .../views/map/cluster-photos/index.js | 145 +++++++++++++ .../map/{ => control}/geolocation-control.js | 4 +- .../src/metadata/views/map/control/index.js | 7 + .../views/map/{ => control}/zoom-control.js | 10 +- frontend/src/metadata/views/map/index.css | 105 +++++----- frontend/src/metadata/views/map/index.js | 197 +++--------------- frontend/src/metadata/views/map/main.js | 177 ++++++++++++++++ .../{ => overlay}/custom-avatar-overlay.js | 0 .../map/{ => overlay}/custom-image-overlay.js | 24 +-- .../src/metadata/views/map/overlay/index.js | 7 + media/js/map/marker-clusterer.js | 13 +- media/js/map/text-icon-overlay.js | 3 +- 21 files changed, 638 insertions(+), 264 deletions(-) create mode 100644 frontend/src/metadata/views/map/cluster-photos/index.css create mode 100644 frontend/src/metadata/views/map/cluster-photos/index.js rename frontend/src/metadata/views/map/{ => control}/geolocation-control.js (94%) create mode 100644 frontend/src/metadata/views/map/control/index.js rename frontend/src/metadata/views/map/{ => control}/zoom-control.js (87%) create mode 100644 frontend/src/metadata/views/map/main.js rename frontend/src/metadata/views/map/{ => overlay}/custom-avatar-overlay.js (100%) rename frontend/src/metadata/views/map/{ => overlay}/custom-image-overlay.js (70%) create mode 100644 frontend/src/metadata/views/map/overlay/index.js diff --git a/frontend/src/metadata/components/view-toolbar/index.js b/frontend/src/metadata/components/view-toolbar/index.js index 7bfb186b5c3..7227ecea750 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 8d2e0721f4d..39fc30be0ab 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 c05d276939a..9cc80cf45b4 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 01604d5dded..0b83427af8e 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 fd7beb7aa00..1cede63b774 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 3bf4422302e..a07a40a2f5d 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 7e809ea710b..64df8f7eca7 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 2872778e2a8..ee4ce655038 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 00000000000..b2f844b5374 --- /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 00000000000..921b1645e70 --- /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 6a2b227daaf..8ce5e38053f 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 00000000000..46577bee520 --- /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 3600ba85ff0..995590dc316 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 3a8524d8f29..98cbfca6d2d 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:hover { + color: #212529; } -.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:disabled { + color: #ccc !important; } diff --git a/frontend/src/metadata/views/map/index.js b/frontend/src/metadata/views/map/index.js index 1f735b16e42..cf57e2bda61 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 mapRef = useRef(null); - const clusterRef = useRef(null); - const batchIndexRef = useRef(0); + const [showGallery, setShowGallery] = useState(false); + const [markerIds, setMarkerIds] = useState([]); + const { metadata, store } = useMetadataView(); - 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]); - - 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 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); - } + }, [repoID, metadata.rows]); + + 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 closeGallery = useCallback(() => { + setShowGallery(false); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_MAP_VIEW_TOOLBAR, false); }, []); - 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]); - - 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]); + const onDeleteLocationPhotos = useCallback((ids) => { + store.deleteLocationPhotos(ids); + }, [store]); 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 00000000000..d1f04d94c0c --- /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 ddb61059b95..b58ecd096ea 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 00000000000..21f2d71fa90 --- /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 66cc930defe..5f7e3100f0d 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 725e04efe22..9c4ead85a00 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(); From 89b5100102469ac9f3ab845cd6e11a404ae1771e Mon Sep 17 00:00:00 2001 From: zhouwenxuan Date: Sat, 4 Jan 2025 18:00:16 +0800 Subject: [PATCH 3/9] optimize --- frontend/src/metadata/store/index.js | 16 --- .../src/metadata/store/operations/apply.js | 15 --- .../metadata/store/operations/constants.js | 5 - .../views/map/cluster-photos/index.js | 122 ++++-------------- frontend/src/metadata/views/map/index.js | 8 +- frontend/src/metadata/views/map/main.js | 34 +++-- .../views/map/overlay/custom-image-overlay.js | 21 ++- media/js/map/marker-clusterer.js | 23 +++- 8 files changed, 88 insertions(+), 156 deletions(-) diff --git a/frontend/src/metadata/store/index.js b/frontend/src/metadata/store/index.js index a07a40a2f5d..3bf4422302e 100644 --- a/frontend/src/metadata/store/index.js +++ b/frontend/src/metadata/store/index.js @@ -623,22 +623,6 @@ 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 64df8f7eca7..7e809ea710b 100644 --- a/frontend/src/metadata/store/operations/apply.js +++ b/frontend/src/metadata/store/operations/apply.js @@ -335,21 +335,6 @@ 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 ee4ce655038..2872778e2a8 100644 --- a/frontend/src/metadata/store/operations/constants.js +++ b/frontend/src/metadata/store/operations/constants.js @@ -34,9 +34,6 @@ export const OPERATION_TYPE = { // tag UPDATE_FILE_TAGS: 'update_file_tags', - - // map - DELETE_LOCATION_PHOTOS: 'delete_location_photos', }; export const COLUMN_DATA_OPERATION_TYPE = { @@ -77,8 +74,6 @@ 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.js b/frontend/src/metadata/views/map/cluster-photos/index.js index 921b1645e70..4fc69e70c2d 100644 --- a/frontend/src/metadata/views/map/cluster-photos/index.js +++ b/frontend/src/metadata/views/map/cluster-photos/index.js @@ -1,114 +1,43 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo } 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 { EVENT_BUS_TYPE } 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 ClusterPhotos = ({ metadata, markerIds, onClose }) => { + const { store, duplicateRecord, addFolder } = 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 clusterMetadata = useMemo(() => { + const filteredRows = metadata.rows.filter(row => markerIds.includes(row._id)); 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]); + newMetadata.rows = filteredRows; + return newMetadata; + }, [metadata, markerIds]); 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]); + deletedImages.forEach((record) => recordIds.push(record.id)); + store.deleteRecords(recordIds, { success_callback }); + }, [store]); const handleViewChange = useCallback((update) => { - metadataAPI.modifyView(repoID, view._id, update).then(res => { - const newView = { ...view, ...update }; - loadData(newView); + metadataAPI.modifyView(repoID, metadata.view._id, update).then(res => { + }).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 - }, []); + }, [metadata, repoID,]); useEffect(() => { const unsubscribeViewChange = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.MAP_GALLERY_VIEW_CHANGE, handleViewChange); @@ -119,27 +48,22 @@ const ClusterPhotos = ({ view, markerIds, onClose, onDelete }) => { }, []); return ( - isLoading ? ( - - ) : ( -
-
-
- -
-
{gettext('Location')}
+
+
+
+
- +
{gettext('Location')}
- ) + +
); }; ClusterPhotos.propTypes = { - view: PropTypes.object, + metadata: PropTypes.object, markerIds: PropTypes.array, onClose: PropTypes.func, - onDelete: PropTypes.func, }; export default ClusterPhotos; diff --git a/frontend/src/metadata/views/map/index.js b/frontend/src/metadata/views/map/index.js index cf57e2bda61..a4d51bf9eb7 100644 --- a/frontend/src/metadata/views/map/index.js +++ b/frontend/src/metadata/views/map/index.js @@ -14,7 +14,7 @@ import './index.css'; const Map = () => { const [showGallery, setShowGallery] = useState(false); const [markerIds, setMarkerIds] = useState([]); - const { metadata, store } = useMetadataView(); + const { metadata } = useMetadataView(); const repoID = window.sfMetadataContext.getSetting('repoID'); @@ -51,10 +51,6 @@ const Map = () => { window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_MAP_VIEW_TOOLBAR, false); }, []); - const onDeleteLocationPhotos = useCallback((ids) => { - store.deleteLocationPhotos(ids); - }, [store]); - useEffect(() => { window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MAP_VIEW, metadata.view); }, [metadata.view]); @@ -62,7 +58,7 @@ const Map = () => { return ( <> {showGallery ? ( - + ) : (
)} diff --git a/frontend/src/metadata/views/map/main.js b/frontend/src/metadata/views/map/main.js index d1f04d94c0c..e9770b9fc48 100644 --- a/frontend/src/metadata/views/map/main.js +++ b/frontend/src/metadata/views/map/main.js @@ -71,10 +71,26 @@ const Main = ({ validImages, onOpen }) => { } }, []); + 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('map-center', point); + window.sfMetadataContext.localStorage.setItem('map-zoom', zoom); + }, []); + + const loadMapState = useCallback(() => { + const savedCenter = window.sfMetadataContext.localStorage.getItem('map-center') || DEFAULT_POSITION; + const savedZoom = window.sfMetadataContext.localStorage.getItem('map-zoom') || DEFAULT_ZOOM; + return { center: savedCenter, zoom: savedZoom }; + }, []); + const onClickMarker = useCallback((e, markers) => { + saveMapState(); + const imageIds = markers.map(marker => marker._imageId); onOpen(imageIds); - }, [onOpen]); + }, [onOpen, saveMapState]); const renderMarkersBatch = useCallback(() => { if (!validImages.length || !clusterRef.current) return; @@ -110,23 +126,23 @@ const Main = ({ validImages, onOpen }) => { const renderBaiduMap = useCallback(() => { if (!mapRef.current || !window.BMap.Map) return; - let mapCenter = window.sfMetadataContext.localStorage.getItem('map-center') || DEFAULT_POSITION; + let { center, zoom } = loadMapState(); // 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); + center = { lng: userInfo.coords.longitude, lat: userInfo.coords.latitude }; + window.sfMetadataContext.localStorage.setItem('map-center', center); }); } - if (!isValidPosition(mapCenter?.lng, mapCenter?.lat)) return; + if (!isValidPosition(center?.lng, center?.lat)) return; - const gcPosition = wgs84_to_gcj02(mapCenter.lng, mapCenter.lat); + const gcPosition = wgs84_to_gcj02(center.lng, center.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.centerAndZoom(point, zoom); mapRef.current.enableScrollWheelZoom(true); const savedValue = window.sfMetadataContext.localStorage.getItem('map-type'); @@ -138,7 +154,7 @@ const Main = ({ validImages, onOpen }) => { batchIndexRef.current = 0; renderMarkersBatch(); - }, [addMapController, initializeClusterer, initializeUserMarker, renderMarkersBatch, getMapType]); + }, [addMapController, initializeClusterer, initializeUserMarker, renderMarkersBatch, getMapType, loadMapState]); useEffect(() => { const switchMapTypeSubscribe = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.SWITCH_MAP_TYPE, (newType) => { @@ -150,7 +166,7 @@ const Main = ({ validImages, onOpen }) => { switchMapTypeSubscribe(); }; - }, [getMapType]); + }, [getMapType, saveMapState]); useEffect(() => { if (mapInfo.type === MAP_PROVIDER.B_MAP) { diff --git a/frontend/src/metadata/views/map/overlay/custom-image-overlay.js b/frontend/src/metadata/views/map/overlay/custom-image-overlay.js index b58ecd096ea..b882bfbe114 100644 --- a/frontend/src/metadata/views/map/overlay/custom-image-overlay.js +++ b/frontend/src/metadata/views/map/overlay/custom-image-overlay.js @@ -30,13 +30,30 @@ const customImageOverlay = (center, image, callback) => { this._div.append(label); const eventHandler = (event) => { - event.stopPropagation(); event.preventDefault(); this._callback && this._callback(event, [{ _imageId: this._imageId }]); }; if (Utils.isDesktop()) { - this._div.addEventListener('click', eventHandler); + let clickTimeout; + this._div.addEventListener('click', (event) => { + if (clickTimeout) { + clearTimeout(clickTimeout); + clickTimeout = null; + return; + } + clickTimeout = setTimeout(() => { + eventHandler(event); + clickTimeout = null; + }, 300); + }); + this._div.addEventListener('dblclick', (e) => { + e.preventDefault(); + if (clickTimeout) { + clearTimeout(clickTimeout); + clickTimeout = null; + } + }); } else { this._div.addEventListener('touchend', eventHandler); } diff --git a/media/js/map/marker-clusterer.js b/media/js/map/marker-clusterer.js index 5f7e3100f0d..41f1aca44f1 100644 --- a/media/js/map/marker-clusterer.js +++ b/media/js/map/marker-clusterer.js @@ -580,14 +580,29 @@ var BMapLib = window.BMapLib = BMapLib || {}; var thatMap = this._map; var thatBounds = this.getBounds(); - this._clusterMarker.addEventListener("click",(event) => { - // thatMap.setViewport(thatBounds); + let clickTimeout; + this._clusterMarker.addEventListener("click", (event) => { + if (clickTimeout) { + clearTimeout(clickTimeout); + clickTimeout = null; + return; + } + clickTimeout = setTimeout(() => { if (this._markerClusterer && typeof this._markerClusterer.getCallback() === 'function') { - const markers = this._markers; - this._markerClusterer.getCallback()(event, markers); + const markers = this._markers; + this._markerClusterer.getCallback()(event, markers); } + clickTimeout = null; + }, 300); // Delay to differentiate between single and double click }); + this._clusterMarker.addEventListener("dblclick", (event) => { + if (clickTimeout) { + clearTimeout(clickTimeout); + clickTimeout = null; + } + // Do nothing on double click + }); }; /** From ff44d97405119f523298a01af9db67aa783b31ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E5=9B=BD=E7=92=87?= Date: Wed, 8 Jan 2025 14:40:28 +0800 Subject: [PATCH 4/9] fix: bug --- .../src/components/cur-dir-path/dir-path.js | 5 +- .../gallery-group-by-setter.js | 37 +++++ .../gallery-group-by-setter/index.css | 57 ------- .../gallery-group-by-setter/index.js | 51 ------- .../components/data-process-setter/index.js | 2 +- .../data-process-setter/map-type-setter.js | 35 +++++ .../map-type-setter/index.css | 57 ------- .../map-type-setter/index.js | 49 ------ .../metadata/components/radio-group/index.css | 54 +++++++ .../metadata/components/radio-group/index.js | 46 ++++++ .../view-toolbar/face-recognition/index.js | 6 +- .../metadata/components/view-toolbar/index.js | 5 +- .../view-toolbar/map-view-toolbar/index.js | 101 +++++++------ .../src/metadata/constants/event-bus-type.js | 13 +- frontend/src/metadata/constants/index.js | 140 ------------------ .../src/metadata/constants/view/gallery.js | 16 ++ .../constants/{view.js => view/index.js} | 11 +- .../src/metadata/constants/view/kanban.js | 1 + frontend/src/metadata/constants/view/map.js | 15 ++ frontend/src/metadata/constants/view/table.js | 101 +++++++++++++ frontend/src/metadata/hooks/metadata-view.js | 2 + .../face-recognition/person-photos/index.js | 4 +- .../views/gallery/context-menu/index.js | 8 +- frontend/src/metadata/views/gallery/main.js | 8 +- .../views/map/cluster-photos/index.css | 52 +------ .../views/map/cluster-photos/index.js | 98 ++++++++---- frontend/src/metadata/views/map/index.js | 57 ++++--- .../metadata/views/map/{main.js => map.js} | 65 ++++---- .../metadata/views/table/utils/grid-utils.js | 5 +- media/js/map/marker-clusterer.js | 64 ++++---- 30 files changed, 562 insertions(+), 603 deletions(-) create mode 100644 frontend/src/metadata/components/data-process-setter/gallery-group-by-setter.js delete mode 100644 frontend/src/metadata/components/data-process-setter/gallery-group-by-setter/index.css delete mode 100644 frontend/src/metadata/components/data-process-setter/gallery-group-by-setter/index.js create mode 100644 frontend/src/metadata/components/data-process-setter/map-type-setter.js delete mode 100644 frontend/src/metadata/components/data-process-setter/map-type-setter/index.css delete mode 100644 frontend/src/metadata/components/data-process-setter/map-type-setter/index.js create mode 100644 frontend/src/metadata/components/radio-group/index.css create mode 100644 frontend/src/metadata/components/radio-group/index.js create mode 100644 frontend/src/metadata/constants/view/gallery.js rename frontend/src/metadata/constants/{view.js => view/index.js} (95%) create mode 100644 frontend/src/metadata/constants/view/kanban.js create mode 100644 frontend/src/metadata/constants/view/map.js create mode 100644 frontend/src/metadata/constants/view/table.js rename frontend/src/metadata/views/map/{main.js => map.js} (75%) diff --git a/frontend/src/components/cur-dir-path/dir-path.js b/frontend/src/components/cur-dir-path/dir-path.js index c2cee204af4..4f2c89f1ea5 100644 --- a/frontend/src/components/cur-dir-path/dir-path.js +++ b/frontend/src/components/cur-dir-path/dir-path.js @@ -9,7 +9,7 @@ import { siteRoot, gettext } from '../../utils/constants'; import { Utils } from '../../utils/utils'; import { PRIVATE_FILE_TYPE } from '../../constants'; import { debounce } from '../../metadata/utils/common'; -import { EVENT_BUS_TYPE, FACE_RECOGNITION_VIEW_ID } from '../../metadata/constants'; +import { EVENT_BUS_TYPE } from '../../metadata/constants'; import { ALL_TAGS_ID } from '../../tag/constants'; const propTypes = { @@ -126,13 +126,12 @@ class DirPath extends React.Component { turnViewPathToLink = (pathList) => { if (!Array.isArray(pathList) || pathList.length === 0) return null; const [, , viewId, children] = pathList; - const isViewSupportClick = viewId === FACE_RECOGNITION_VIEW_ID && children; return ( <> / {gettext('Views')} / - {}}> + {}}> {children && ( diff --git a/frontend/src/metadata/components/data-process-setter/gallery-group-by-setter.js b/frontend/src/metadata/components/data-process-setter/gallery-group-by-setter.js new file mode 100644 index 00000000000..2952839c39e --- /dev/null +++ b/frontend/src/metadata/components/data-process-setter/gallery-group-by-setter.js @@ -0,0 +1,37 @@ +import React, { useState, useCallback, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { EVENT_BUS_TYPE, GALLERY_DATE_MODE, STORAGE_GALLERY_DATE_MODE_KEY } from '../../constants'; +import { gettext } from '../../../utils/constants'; +import RadioGroup from '../radio-group'; + +const DATE_MODES = [ + { value: GALLERY_DATE_MODE.YEAR, label: gettext('Year') }, + { value: GALLERY_DATE_MODE.MONTH, label: gettext('Month') }, + { value: GALLERY_DATE_MODE.DAY, label: gettext('Day') }, + { value: GALLERY_DATE_MODE.ALL, label: gettext('All') }, +]; + +const GalleryGroupBySetter = ({ view }) => { + const [currentMode, setCurrentMode] = useState(GALLERY_DATE_MODE.DAY); + + useEffect(() => { + const savedValue = window.sfMetadataContext.localStorage.getItem(STORAGE_GALLERY_DATE_MODE_KEY) || GALLERY_DATE_MODE.DAY; + setCurrentMode(savedValue); + }, [view?._id]); + + const handleGroupByChange = useCallback((newMode) => { + if (currentMode === newMode) return; + setCurrentMode(newMode); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SWITCH_GALLERY_GROUP_BY, newMode); + }, [currentMode]); + + return (); +}; + +GalleryGroupBySetter.propTypes = { + view: PropTypes.shape({ + _id: PropTypes.string + }) +}; + +export default GalleryGroupBySetter; diff --git a/frontend/src/metadata/components/data-process-setter/gallery-group-by-setter/index.css b/frontend/src/metadata/components/data-process-setter/gallery-group-by-setter/index.css deleted file mode 100644 index 69560fc4b1b..00000000000 --- a/frontend/src/metadata/components/data-process-setter/gallery-group-by-setter/index.css +++ /dev/null @@ -1,57 +0,0 @@ -.metadata-gallery-group-by-setter { - width: 272px; - height: 36px; - display: flex; - justify-content: center; - align-items: center; - border: 1px solid #e2e2e2; - border-radius: 3px; -} - -.metadata-gallery-group-by-setter .metadata-gallery-group-by-button { - width: 66px; - height: 28px; - color: #212529; - background-color: #fff; - position: relative; - display: flex; - align-items: center; - justify-content: center; - font-size: 0.875rem; - border: 0; - border-radius: 2px; -} - -.metadata-gallery-group-by-setter .metadata-gallery-group-by-button:hover { - background-color: #f0f0f0; - cursor: pointer; -} - -.metadata-gallery-group-by-setter .metadata-gallery-group-by-button.active { - background-color: #f5f5f5; -} - -.metadata-gallery-group-by-setter .metadata-gallery-group-by-button span { - display: block; - text-align: center; - width: 100%; -} - -.metadata-gallery-group-by-button:not(:first-child)::before { - content: ''; - width: 1px; - height: 22px; - background-color: #e2e2e2; - position: absolute; - top: 50%; - transform: translateY(-50%); - transition: opacity 0.3s; - left: -1px; -} - -.metadata-gallery-group-by-button:hover::before, -.metadata-gallery-group-by-button.active::before, -.metadata-gallery-group-by-button:hover + .metadata-gallery-group-by-button::before, -.metadata-gallery-group-by-button.active + .metadata-gallery-group-by-button::before { - opacity: 0; -} diff --git a/frontend/src/metadata/components/data-process-setter/gallery-group-by-setter/index.js b/frontend/src/metadata/components/data-process-setter/gallery-group-by-setter/index.js deleted file mode 100644 index a79364d7f42..00000000000 --- a/frontend/src/metadata/components/data-process-setter/gallery-group-by-setter/index.js +++ /dev/null @@ -1,51 +0,0 @@ -import React, { useState, useCallback, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import { EVENT_BUS_TYPE, GALLERY_DATE_MODE } from '../../../constants'; -import { gettext } from '../../../../utils/constants'; - -import './index.css'; - -const DATE_MODE_MAP = { - [GALLERY_DATE_MODE.YEAR]: gettext('Year'), - [GALLERY_DATE_MODE.MONTH]: gettext('Month'), - [GALLERY_DATE_MODE.DAY]: gettext('Day'), - [GALLERY_DATE_MODE.ALL]: gettext('All') -}; - -const GalleryGroupBySetter = ({ view }) => { - const [currentMode, setCurrentMode] = useState(GALLERY_DATE_MODE.DAY); - - useEffect(() => { - const savedValue = window.sfMetadataContext.localStorage.getItem('gallery-group-by', GALLERY_DATE_MODE.DAY); - setCurrentMode(savedValue || GALLERY_DATE_MODE.DAY); - }, [view?._id]); - - const handleGroupByChange = useCallback((newMode) => { - setCurrentMode(newMode); - window.sfMetadataContext.localStorage.setItem('gallery-group-by', newMode); - window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SWITCH_GALLERY_GROUP_BY, newMode); - }, []); - - return ( -
- {Object.entries(DATE_MODE_MAP).map(([dateMode, label]) => ( - - ))} -
- ); -}; - -GalleryGroupBySetter.propTypes = { - view: PropTypes.shape({ - _id: PropTypes.string - }) -}; - -export default GalleryGroupBySetter; diff --git a/frontend/src/metadata/components/data-process-setter/index.js b/frontend/src/metadata/components/data-process-setter/index.js index 7d87844e987..fb3425f8abe 100644 --- a/frontend/src/metadata/components/data-process-setter/index.js +++ b/frontend/src/metadata/components/data-process-setter/index.js @@ -1,4 +1,4 @@ -import GalleryGroupBySetter from './gallery-group-by-setter/index'; +import GalleryGroupBySetter from './gallery-group-by-setter'; import GallerySliderSetter from './gallery-slider-setter/index'; import FilterSetter from './filter-setter'; import SortSetter from './sort-setter'; diff --git a/frontend/src/metadata/components/data-process-setter/map-type-setter.js b/frontend/src/metadata/components/data-process-setter/map-type-setter.js new file mode 100644 index 00000000000..643d64c1805 --- /dev/null +++ b/frontend/src/metadata/components/data-process-setter/map-type-setter.js @@ -0,0 +1,35 @@ +import React, { useState, useCallback, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { EVENT_BUS_TYPE, MAP_TYPE, STORAGE_MAP_TYPE_KEY } from '../../constants'; +import { gettext } from '../../../utils/constants'; +import RadioGroup from '../radio-group'; + +const MAP_TYPES = [ + { value: MAP_TYPE.MAP, label: gettext('Map') }, + { value: MAP_TYPE.SATELLITE, label: gettext('Satellite') }, +]; + +const MapTypeSetter = ({ view }) => { + const [currentType, setCurrentType] = useState(MAP_TYPE.MAP); + + useEffect(() => { + const type = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_TYPE_KEY) || MAP_TYPE.MAP; + setCurrentType(type); + }, [view?._id]); + + const onChange = useCallback((type) => { + if (currentType === type) return; + setCurrentType(type); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MODIFY_MAP_TYPE, type); + }, [currentType]); + + return (); +}; + +MapTypeSetter.propTypes = { + view: PropTypes.shape({ + _id: PropTypes.string + }) +}; + +export default MapTypeSetter; diff --git a/frontend/src/metadata/components/data-process-setter/map-type-setter/index.css b/frontend/src/metadata/components/data-process-setter/map-type-setter/index.css deleted file mode 100644 index cac45ca19e5..00000000000 --- a/frontend/src/metadata/components/data-process-setter/map-type-setter/index.css +++ /dev/null @@ -1,57 +0,0 @@ -.metadata-map-type-setter { - width: fit-content; - height: 36px; - display: flex; - justify-content: center; - align-items: center; - border: 1px solid #e2e2e2; - border-radius: 3px; -} - -.metadata-map-type-setter .metadata-map-type-button { - width: 66px; - height: 28px; - color: #212529; - background-color: #fff; - position: relative; - display: flex; - align-items: center; - justify-content: center; - font-size: 0.875rem; - border: 0; - border-radius: 2px; -} - -.metadata-map-type-setter .metadata-map-type-button:hover { - background-color: #f0f0f0; - cursor: pointer; -} - -.metadata-map-type-setter .metadata-map-type-button.active { - background-color: #f5f5f5; -} - -.metadata-map-type-setter .metadata-map-type-button span { - display: block; - text-align: center; - width: 100%; -} - -.metadata-map-type-button:not(:first-child)::before { - content: ''; - width: 1px; - height: 22px; - background-color: #e2e2e2; - position: absolute; - top: 50%; - transform: translateY(-50%); - transition: opacity 0.3s; - left: -1px; -} - -.metadata-map-type-button:hover::before, -.metadata-map-type-button.active::before, -.metadata-map-type-button:hover + .metadata-map-type-button::before, -.metadata-map-type-button.active + .metadata-map-type-button::before { - opacity: 0; -} \ No newline at end of file diff --git a/frontend/src/metadata/components/data-process-setter/map-type-setter/index.js b/frontend/src/metadata/components/data-process-setter/map-type-setter/index.js deleted file mode 100644 index 8fe2cb1ba5e..00000000000 --- a/frontend/src/metadata/components/data-process-setter/map-type-setter/index.js +++ /dev/null @@ -1,49 +0,0 @@ -import React, { useState, useCallback, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import { EVENT_BUS_TYPE, MAP_TYPE } from '../../../constants'; -import { gettext } from '../../../../utils/constants'; - -import './index.css'; - -const TYPE_MAP = { - [MAP_TYPE.MAP]: gettext('Map'), - [MAP_TYPE.SATELLITE]: gettext('Satellite') -}; - -const MapTypeSetter = ({ view }) => { - const [currentType, setCurrentType] = useState(MAP_TYPE.MAP); - - useEffect(() => { - const savedValue = window.sfMetadataContext.localStorage.getItem('map-type', MAP_TYPE.MAP); - setCurrentType(savedValue || MAP_TYPE.MAP); - }, [view?._id]); - - const handleTypeChange = useCallback((newType) => { - setCurrentType(newType); - window.sfMetadataContext.localStorage.setItem('map-type', newType); - window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SWITCH_MAP_TYPE, newType); - }, []); - - return ( -
- {Object.entries(TYPE_MAP).map(([type, label]) => ( - - ))} -
- ); -}; - -MapTypeSetter.propTypes = { - view: PropTypes.shape({ - _id: PropTypes.string - }) -}; - -export default MapTypeSetter; diff --git a/frontend/src/metadata/components/radio-group/index.css b/frontend/src/metadata/components/radio-group/index.css new file mode 100644 index 00000000000..5bc9682ddb0 --- /dev/null +++ b/frontend/src/metadata/components/radio-group/index.css @@ -0,0 +1,54 @@ +.sf-metadata-radio-group { + width: fit-content; + height: 36px; + display: flex; + justify-content: center; + align-items: center; + border: 1px solid #e2e2e2; + border-radius: 3px; + padding: 0 3px; +} + +.sf-metadata-radio-group .sf-metadata-radio-group-option { + min-width: 66px; + width: fit-content; + height: 28px; + color: #212529; + background-color: #fff; + position: relative; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.875rem; + border: 0; + border-radius: 2px; +} + +.sf-metadata-radio-group .sf-metadata-radio-group-option:hover { + background-color: #f0f0f0; + cursor: pointer; +} + +.sf-metadata-radio-group .sf-metadata-radio-group-option.active { + background-color: #f5f5f5; +} + +.sf-metadata-radio-group .sf-metadata-radio-group-option:not(:first-child)::before { + content: ''; + width: 1px; + height: 22px; + background-color: #e2e2e2; + position: absolute; + top: 50%; + transform: translateY(-50%); + transition: opacity 0.3s; + left: -1px; +} + +.sf-metadata-radio-group .sf-metadata-radio-group-option:hover::before, +.sf-metadata-radio-group .sf-metadata-radio-group-option.active::before, +.sf-metadata-radio-group .sf-metadata-radio-group-option:hover + .sf-metadata-radio-group-option::before, +.sf-metadata-radio-group .sf-metadata-radio-group-option.active + .sf-metadata-radio-group-option::before { + opacity: 0; +} + diff --git a/frontend/src/metadata/components/radio-group/index.js b/frontend/src/metadata/components/radio-group/index.js new file mode 100644 index 00000000000..4718e3409a1 --- /dev/null +++ b/frontend/src/metadata/components/radio-group/index.js @@ -0,0 +1,46 @@ + +import React, { useCallback, useMemo } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +import './index.css'; + +const RadioGroup = ({ value, options, className, onChange: onChangeAPI }) => { + const selected = useMemo(() => { + const selectedOption = options.find(o => value === o.value) || options[0]; + return selectedOption.value; + }, [value, options]); + + const onChange = useCallback((event) => { + const newValue = event.target.dataset.option; + if (selected === newValue) return; + onChangeAPI(newValue); + }, [selected, onChangeAPI]); + + return ( +
+ {options.map(option => { + const { value, label } = option; + return ( +
+ {label} +
+ ); + })} +
+ ); +}; + +RadioGroup.propTypes = { + value: PropTypes.string, + options: PropTypes.array, + className: PropTypes.string, + onChange: PropTypes.func, +}; + +export default RadioGroup; diff --git a/frontend/src/metadata/components/view-toolbar/face-recognition/index.js b/frontend/src/metadata/components/view-toolbar/face-recognition/index.js index 601aab0a59e..8023d77f5fe 100644 --- a/frontend/src/metadata/components/view-toolbar/face-recognition/index.js +++ b/frontend/src/metadata/components/view-toolbar/face-recognition/index.js @@ -17,17 +17,17 @@ const FaceRecognitionViewToolbar = ({ readOnly, isCustomPermission, onToggleDeta setShow(isShow); }, []); - const setRecognitionView = useCallback(view => { + const resetView = useCallback(view => { setView(view); }, []); const modifySorts = useCallback((sorts) => { - window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.FACE_RECOGNITION_VIEW_CHANGE, { sorts }); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.UPDATE_SERVER_VIEW, { sorts }); }, []); useEffect(() => { const unsubscribeToggle = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, onToggle); - const unsubscribeView = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.FACE_RECOGNITION_VIEW, setRecognitionView); + const unsubscribeView = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.RESET_VIEW, resetView); return () => { unsubscribeToggle && unsubscribeToggle(); unsubscribeView && unsubscribeView(); diff --git a/frontend/src/metadata/components/view-toolbar/index.js b/frontend/src/metadata/components/view-toolbar/index.js index 7227ecea750..8ced5ef287a 100644 --- a/frontend/src/metadata/components/view-toolbar/index.js +++ b/frontend/src/metadata/components/view-toolbar/index.js @@ -94,8 +94,8 @@ const ViewToolBar = ({ viewId, isCustomPermission, onToggleDetail, onCloseDetail )} {viewType === VIEW_TYPE.FACE_RECOGNITION && ( )} @@ -113,8 +113,9 @@ const ViewToolBar = ({ viewId, isCustomPermission, onToggleDetail, onCloseDetail )} {viewType === VIEW_TYPE.MAP && ( view.type, [view]); + const viewType = useMemo(() => VIEW_TYPE.MAP, []); const viewColumns = useMemo(() => { if (!view) return []; return view.columns; @@ -29,69 +30,73 @@ const MapViewToolBar = ({ }, []); const modifySorts = useCallback((sorts) => { - window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MAP_GALLERY_VIEW_CHANGE, { sorts }); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.UPDATE_SERVER_VIEW, { sorts }); }, []); - const setMapView = useCallback(view => setView(view), []); + const resetView = 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); + 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 () => { - unsubscribeToggleViewToolbarMode(); - unsubscribeView(); + unsubscribeToggle && unsubscribeToggle(); + unsubscribeView && unsubscribeView(); }; - }, [setMapView, onToggle]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); useEffect(() => { - setView(window.sfMetadataStore.data.view); - }, []); + setShowGalleryToolbar(false); + }, [viewID]); - return ( - <> - {showGalleryToolbar ? ( + if (showGalleryToolbar) { + return ( + <>
- <> - - - - + + + {!isCustomPermission && (
)}
- ) : - <> -
- - -
-
- } +
+ + ); + } + + return ( + <> +
+ + +
+
); }; diff --git a/frontend/src/metadata/constants/event-bus-type.js b/frontend/src/metadata/constants/event-bus-type.js index 9cc80cf45b4..43d479dea46 100644 --- a/frontend/src/metadata/constants/event-bus-type.js +++ b/frontend/src/metadata/constants/event-bus-type.js @@ -60,22 +60,21 @@ export const EVENT_BUS_TYPE = { SAVED: 'saved', ERROR: 'error', + // view + TOGGLE_VIEW_TOOLBAR: 'toggle_view_toolbar', + RESET_VIEW: 'reset_view', + UPDATE_SERVER_VIEW: 'update_server_view', + // gallery MODIFY_GALLERY_ZOOM_GEAR: 'modify_gallery_zoom_gear', SWITCH_GALLERY_GROUP_BY: 'switch_gallery_group_by', - // face recognition - TOGGLE_VIEW_TOOLBAR: 'toggle_view_toolbar', - FACE_RECOGNITION_VIEW: 'face_recognition_view', - FACE_RECOGNITION_VIEW_CHANGE: 'face_recognition_view_change', - // kanban TOGGLE_KANBAN_SETTINGS: 'toggle_kanban_settings', OPEN_KANBAN_SETTINGS: 'open_kanban_settings', CLOSE_KANBAN_SETTINGS: 'close_kanban_settings', // map - SWITCH_MAP_TYPE: 'switch_map_type', - TOGGLE_MAP_VIEW_TOOLBAR: 'toggle_map_view_toolbar', + MODIFY_MAP_TYPE: 'modify_map_type', MAP_VIEW: 'map_view', }; diff --git a/frontend/src/metadata/constants/index.js b/frontend/src/metadata/constants/index.js index 0b83427af8e..e69790e7ebb 100644 --- a/frontend/src/metadata/constants/index.js +++ b/frontend/src/metadata/constants/index.js @@ -1,4 +1,3 @@ -import CellType from './column/type'; import { EVENT_BUS_TYPE } from './event-bus-type'; import TRANSFER_TYPES from './TransferTypes'; import * as metadataZIndexes from './z-index'; @@ -13,147 +12,8 @@ export * from './sort'; export * from './error'; export * from './view'; -export const CELL_NAVIGATION_MODE = { - NONE: 'none', - CHANGE_ROW: 'changeRow', - LOOP_OVER_ROW: 'loopOverRow', -}; - -export const SEQUENCE_COLUMN_WIDTH = 80; - -export const ROW_HEIGHT = 32; - -export const GRID_HEADER_DEFAULT_HEIGHT = 32; - -export const GRID_HEADER_DOUBLE_HEIGHT = 56; - -export const GROUP_VIEW_OFFSET = 16; - -export const GROUP_HEADER_HEIGHT = 48; - -export const TABLE_LEFT_MARGIN = 10; - -export const TABLE_BORDER_WIDTH = 1; - -export const UNABLE_TO_CALCULATE = '--'; - -export const FROZEN_COLUMN_SHADOW = '2px 0 5px -2px hsla(0,0%,53.3%,.3)'; - -export const TABLE_NOT_SUPPORT_EDIT_TYPE_MAP = { - [CellType.CREATOR]: true, - [CellType.LAST_MODIFIER]: true, - [CellType.CTIME]: true, - [CellType.MTIME]: true, - [CellType.FILE_NAME]: true, -}; - -export const TABLE_SUPPORT_EDIT_TYPE_MAP = { - [CellType.TEXT]: true, - [CellType.DATE]: true, - [CellType.NUMBER]: true, - [CellType.SINGLE_SELECT]: true, - [CellType.MULTIPLE_SELECT]: true, - [CellType.COLLABORATOR]: true, - [CellType.CHECKBOX]: true, - [CellType.LONG_TEXT]: true, - [CellType.LINK]: true, - [CellType.TAGS]: true, -}; - -export const TABLE_MOBILE_SUPPORT_EDIT_CELL_TYPE_MAP = { - [CellType.TEXT]: true, -}; - -export const CANVAS_RIGHT_INTERVAL = 44; - -export const LEFT_NAV = 280; -export const ROW_DETAIL_PADDING = 40 * 2; -export const ROW_DETAIL_MARGIN = 20 * 2; -export const EDITOR_PADDING = 1.5 * 16; // 1.5: 0.75 * 2 - -export const COLUMN_RATE_MAX_NUMBER = [ - { name: 1 }, - { name: 2 }, - { name: 3 }, - { name: 4 }, - { name: 5 }, - { name: 6 }, - { name: 7 }, - { name: 8 }, - { name: 9 }, - { name: 10 }, -]; - -export const GROUP_ROW_TYPE = { - GROUP_CONTAINER: 'group_container', - ROW: 'row', - BTN_INSERT_ROW: 'btn_insert_row', -}; - -export const INSERT_ROW_HEIGHT = 32; - -export const CHANGE_HEADER_WIDTH = 'CHANGE_HEADER_WIDTH'; - -export const NOT_SUPPORT_DRAG_COPY_COLUMN_TYPES = [ -]; - -export const SUPPORT_PREVIEW_COLUMN_TYPES = []; - -export const OVER_SCAN_COLUMNS = 10; - -export const DELETED_OPTION_BACKGROUND_COLOR = '#eaeaea'; - -export const DELETED_OPTION_TIPS = 'deleted_option'; - -export const SUPPORT_BATCH_DOWNLOAD_TYPES = []; - -export const PER_LOAD_NUMBER = 1000; - -export const DEFAULT_RETRY_TIMES = 4; - -export const DEFAULT_RETRY_INTERVAL = 1000; - -export const MAX_LOAD_NUMBER = 10000; - -export const EDITOR_TYPE = { - PREVIEWER: 'previewer', - ADDITION: 'addition', -}; - export { EVENT_BUS_TYPE, TRANSFER_TYPES, metadataZIndexes, }; - -export const DATE_TAG_HEIGHT = 44; - -export const GALLERY_ZOOM_GEAR_MIN = -2; - -export const GALLERY_ZOOM_GEAR_MAX = 2; - -export const GALLERY_IMAGE_GAP = 2; - -export const GALLERY_DATE_MODE = { - YEAR: 'year', - MONTH: 'month', - DAY: 'day', - ALL: 'all', -}; - -export const MAP_TYPE = { - NORMAL_MAP: 'normal_map', - SATELLITE: 'satellite', -}; - -export const MAP_VIEW_TOOLBAR_MODE = { - MAP: 'map', - GALLERY: 'gallery', -}; - -export const UNCATEGORIZED = '_uncategorized'; - -export const PASTE_SOURCE = { - COPY: 'copy', - CUT: 'cut', -}; diff --git a/frontend/src/metadata/constants/view/gallery.js b/frontend/src/metadata/constants/view/gallery.js new file mode 100644 index 00000000000..61f1b82b358 --- /dev/null +++ b/frontend/src/metadata/constants/view/gallery.js @@ -0,0 +1,16 @@ +export const DATE_TAG_HEIGHT = 44; + +export const GALLERY_ZOOM_GEAR_MIN = -2; + +export const GALLERY_ZOOM_GEAR_MAX = 2; + +export const GALLERY_IMAGE_GAP = 2; + +export const GALLERY_DATE_MODE = { + YEAR: 'year', + MONTH: 'month', + DAY: 'day', + ALL: 'all', +}; + +export const STORAGE_GALLERY_DATE_MODE_KEY = 'gallery_date_mode'; diff --git a/frontend/src/metadata/constants/view.js b/frontend/src/metadata/constants/view/index.js similarity index 95% rename from frontend/src/metadata/constants/view.js rename to frontend/src/metadata/constants/view/index.js index 1cede63b774..264711c992e 100644 --- a/frontend/src/metadata/constants/view.js +++ b/frontend/src/metadata/constants/view/index.js @@ -1,8 +1,13 @@ -import { PRIVATE_COLUMN_KEY } from './column'; -import { FILTER_PREDICATE_TYPE } from './filter'; +import { PRIVATE_COLUMN_KEY } from '../column'; +import { FILTER_PREDICATE_TYPE } from '../filter'; import { SORT_COLUMN_OPTIONS, GALLERY_SORT_COLUMN_OPTIONS, GALLERY_FIRST_SORT_COLUMN_OPTIONS, SORT_TYPE, GALLERY_SORT_PRIVATE_COLUMN_KEYS, GALLERY_FIRST_SORT_PRIVATE_COLUMN_KEYS, -} from './sort'; +} from '../sort'; + +export * from './gallery'; +export * from './kanban'; +export * from './map'; +export * from './table'; export const METADATA_VIEWS_KEY = 'sf-metadata-views'; diff --git a/frontend/src/metadata/constants/view/kanban.js b/frontend/src/metadata/constants/view/kanban.js new file mode 100644 index 00000000000..d9d7085b5e0 --- /dev/null +++ b/frontend/src/metadata/constants/view/kanban.js @@ -0,0 +1 @@ +export const UNCATEGORIZED = '_uncategorized'; diff --git a/frontend/src/metadata/constants/view/map.js b/frontend/src/metadata/constants/view/map.js new file mode 100644 index 00000000000..d9511628ffa --- /dev/null +++ b/frontend/src/metadata/constants/view/map.js @@ -0,0 +1,15 @@ +export const MAP_TYPE = { + MAP: 'map', + SATELLITE: 'satellite', +}; + +export const STORAGE_MAP_TYPE_KEY = 'map_type'; + +export const STORAGE_MAP_CENTER_KEY = 'map_center'; + +export const STORAGE_MAP_ZOOM_KEY = 'map_zoom'; + +export const MAP_VIEW_TOOLBAR_MODE = { + MAP: 'map', + GALLERY: 'gallery', +}; diff --git a/frontend/src/metadata/constants/view/table.js b/frontend/src/metadata/constants/view/table.js new file mode 100644 index 00000000000..aab12d26102 --- /dev/null +++ b/frontend/src/metadata/constants/view/table.js @@ -0,0 +1,101 @@ +import { CellType } from '../column'; + +export const CELL_NAVIGATION_MODE = { + NONE: 'none', + CHANGE_ROW: 'changeRow', + LOOP_OVER_ROW: 'loopOverRow', +}; + +export const SEQUENCE_COLUMN_WIDTH = 80; + +export const ROW_HEIGHT = 32; + +export const GRID_HEADER_DEFAULT_HEIGHT = 32; + +export const GRID_HEADER_DOUBLE_HEIGHT = 56; + +export const GROUP_VIEW_OFFSET = 16; + +export const GROUP_HEADER_HEIGHT = 48; + +export const TABLE_LEFT_MARGIN = 10; + +export const TABLE_BORDER_WIDTH = 1; + +export const UNABLE_TO_CALCULATE = '--'; + +export const FROZEN_COLUMN_SHADOW = '2px 0 5px -2px hsla(0,0%,53.3%,.3)'; + +export const TABLE_NOT_SUPPORT_EDIT_TYPE_MAP = { + [CellType.CREATOR]: true, + [CellType.LAST_MODIFIER]: true, + [CellType.CTIME]: true, + [CellType.MTIME]: true, + [CellType.FILE_NAME]: true, +}; + +export const TABLE_SUPPORT_EDIT_TYPE_MAP = { + [CellType.TEXT]: true, + [CellType.DATE]: true, + [CellType.NUMBER]: true, + [CellType.SINGLE_SELECT]: true, + [CellType.MULTIPLE_SELECT]: true, + [CellType.COLLABORATOR]: true, + [CellType.CHECKBOX]: true, + [CellType.LONG_TEXT]: true, + [CellType.LINK]: true, + [CellType.TAGS]: true, +}; + +export const TABLE_MOBILE_SUPPORT_EDIT_CELL_TYPE_MAP = { + [CellType.TEXT]: true, +}; + +export const CANVAS_RIGHT_INTERVAL = 44; + +export const LEFT_NAV = 280; +export const ROW_DETAIL_PADDING = 40 * 2; +export const ROW_DETAIL_MARGIN = 20 * 2; +export const EDITOR_PADDING = 1.5 * 16; // 1.5: 0.75 * 2 + +export const GROUP_ROW_TYPE = { + GROUP_CONTAINER: 'group_container', + ROW: 'row', + BTN_INSERT_ROW: 'btn_insert_row', +}; + +export const INSERT_ROW_HEIGHT = 32; + +export const CHANGE_HEADER_WIDTH = 'CHANGE_HEADER_WIDTH'; + +export const NOT_SUPPORT_DRAG_COPY_COLUMN_TYPES = [ +]; + +export const SUPPORT_PREVIEW_COLUMN_TYPES = []; + +export const OVER_SCAN_COLUMNS = 10; + +export const DELETED_OPTION_BACKGROUND_COLOR = '#eaeaea'; + +export const DELETED_OPTION_TIPS = 'deleted_option'; + +export const SUPPORT_BATCH_DOWNLOAD_TYPES = []; + +export const PER_LOAD_NUMBER = 1000; + +export const DEFAULT_RETRY_TIMES = 4; + +export const DEFAULT_RETRY_INTERVAL = 1000; + +export const MAX_LOAD_NUMBER = 10000; + +export const EDITOR_TYPE = { + PREVIEWER: 'previewer', + ADDITION: 'addition', +}; + +export const PASTE_SOURCE = { + COPY: 'copy', + CUT: 'cut', +}; + diff --git a/frontend/src/metadata/hooks/metadata-view.js b/frontend/src/metadata/hooks/metadata-view.js index f7397091ba8..1b0ee43808e 100644 --- a/frontend/src/metadata/hooks/metadata-view.js +++ b/frontend/src/metadata/hooks/metadata-view.js @@ -340,6 +340,8 @@ export const MetadataViewProvider = ({ { const errorMessage = Utils.getErrorMsg(error); @@ -164,7 +164,7 @@ const PeoplePhotos = ({ view, people, onClose, onDeletePeoplePhotos, onRemovePeo }, []); useEffect(() => { - const unsubscribeViewChange = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.FACE_RECOGNITION_VIEW_CHANGE, onViewChange); + const unsubscribeViewChange = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_SERVER_VIEW, onViewChange); return () => { unsubscribeViewChange && unsubscribeViewChange(); }; diff --git a/frontend/src/metadata/views/gallery/context-menu/index.js b/frontend/src/metadata/views/gallery/context-menu/index.js index 307d39d8c7c..756989506b6 100644 --- a/frontend/src/metadata/views/gallery/context-menu/index.js +++ b/frontend/src/metadata/views/gallery/context-menu/index.js @@ -30,17 +30,17 @@ const GalleryContextMenu = ({ metadata, selectedImages, onDelete, onDuplicate, a const options = useMemo(() => { let validOptions = [{ value: CONTEXT_MENU_KEY.DOWNLOAD, label: gettext('Download') }]; - if (checkCanDeleteRow) { + if (onDelete && checkCanDeleteRow) { validOptions.push({ value: CONTEXT_MENU_KEY.DELETE, label: selectedImages.length > 1 ? gettext('Delete') : gettext('Delete file') }); } - if (canDuplicateRow && selectedImages.length === 1) { + if (onDuplicate && canDuplicateRow && selectedImages.length === 1) { validOptions.push({ value: CONTEXT_MENU_KEY.DUPLICATE, label: gettext('Duplicate') }); } - if (canRemovePhotoFromPeople) { + if (onRemoveImage && canRemovePhotoFromPeople) { validOptions.push({ value: CONTEXT_MENU_KEY.REMOVE, label: gettext('Remove from this group') }); } return validOptions; - }, [checkCanDeleteRow, canDuplicateRow, canRemovePhotoFromPeople, selectedImages]); + }, [checkCanDeleteRow, canDuplicateRow, canRemovePhotoFromPeople, selectedImages, onDuplicate, onDelete, onRemoveImage]); const closeZipDialog = () => { setIsZipDialogOpen(false); diff --git a/frontend/src/metadata/views/gallery/main.js b/frontend/src/metadata/views/gallery/main.js index 1b82a10dc78..76ba2b1f59b 100644 --- a/frontend/src/metadata/views/gallery/main.js +++ b/frontend/src/metadata/views/gallery/main.js @@ -8,7 +8,7 @@ import { useMetadataView } from '../../hooks/metadata-view'; import { Utils } from '../../../utils/utils'; import { getDateDisplayString, getFileNameFromRecord, getParentDirFromRecord, getRecordIdFromRecord } from '../../utils/cell'; import { siteRoot, fileServerRoot, thumbnailSizeForGrid, thumbnailSizeForOriginal } from '../../../utils/constants'; -import { EVENT_BUS_TYPE, GALLERY_DATE_MODE, DATE_TAG_HEIGHT, GALLERY_IMAGE_GAP } from '../../constants'; +import { EVENT_BUS_TYPE, GALLERY_DATE_MODE, DATE_TAG_HEIGHT, GALLERY_IMAGE_GAP, STORAGE_GALLERY_DATE_MODE_KEY } from '../../constants'; import { getRowById } from '../../utils/table'; import { getEventClassName } from '../../utils/common'; import GalleryContextmenu from './context-menu'; @@ -129,14 +129,14 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, const gear = window.sfMetadataContext.localStorage.getItem('zoom-gear', 0) || 0; setZoomGear(gear); - const mode = window.sfMetadataContext.localStorage.getItem('gallery-group-by', GALLERY_DATE_MODE.DAY) || GALLERY_DATE_MODE.DAY; + const mode = window.sfMetadataContext.localStorage.getItem(STORAGE_GALLERY_DATE_MODE_KEY, GALLERY_DATE_MODE.DAY) || GALLERY_DATE_MODE.DAY; setMode(mode); const switchGalleryModeSubscribe = window.sfMetadataContext.eventBus.subscribe( EVENT_BUS_TYPE.SWITCH_GALLERY_GROUP_BY, (mode) => { setMode(mode); - window.sfMetadataContext.localStorage.setItem('gallery-group-by', mode); + window.sfMetadataContext.localStorage.setItem(STORAGE_GALLERY_DATE_MODE_KEY, mode); } ); @@ -193,7 +193,7 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, if (!containerRef.current) return; const { scrollTop, scrollHeight, clientHeight } = containerRef.current; if (scrollTop + clientHeight >= scrollHeight - 10) { - onLoadMore(); + onLoadMore && onLoadMore(); } else { const { scrollTop, clientHeight } = containerRef.current; const overScanTop = Math.max(0, scrollTop - (imageSize + GALLERY_IMAGE_GAP) * OVER_SCAN_ROWS); diff --git a/frontend/src/metadata/views/map/cluster-photos/index.css b/frontend/src/metadata/views/map/cluster-photos/index.css index b2f844b5374..e190c9727a2 100644 --- a/frontend/src/metadata/views/map/cluster-photos/index.css +++ b/frontend/src/metadata/views/map/cluster-photos/index.css @@ -1,54 +1,4 @@ .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); + 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 index 4fc69e70c2d..c69fe7c3526 100644 --- a/frontend/src/metadata/views/map/cluster-photos/index.js +++ b/frontend/src/metadata/views/map/cluster-photos/index.js @@ -1,67 +1,113 @@ -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, 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 } 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 './index.css'; -const ClusterPhotos = ({ metadata, markerIds, onClose }) => { - const { store, duplicateRecord, addFolder } = useMetadataView(); +const ClusterPhotos = ({ markerIds, onClose }) => { + const [isLoading, setLoading] = useState(true); + const [metadata, setMetadata] = useState({ rows: [] }); - const repoID = window.sfMetadataContext.getSetting('repoID'); + const { repoID, viewID, metadata: allMetadata, store, addFolder, deleteRecords } = useMetadataView(); + const { collaborators } = useCollaborators(); - const clusterMetadata = useMemo(() => { - const filteredRows = metadata.rows.filter(row => markerIds.includes(row._id)); + const rows = useMemo(() => getRowsByIds(allMetadata, markerIds), [allMetadata, markerIds]); + const columns = useMemo(() => allMetadata?.columns || [], [allMetadata]); + + const loadData = useCallback((view) => { + setLoading(true); + const orderRows = sortTableRows({ columns }, rows, view?.sorts || [], { collaborators }); + let metadata = new Metadata({ rows, columns, view }); + metadata.hasMore = false; + metadata.row_ids = orderRows; + metadata.view.rows = orderRows; + setMetadata(metadata); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.RESET_VIEW, metadata.view); + setLoading(false); + }, [rows, columns, collaborators]); + + const deletedByIds = useCallback((ids) => { + if (!Array.isArray(ids) || ids.length === 0) return; const newMetadata = deepCopy(metadata); - newMetadata.rows = filteredRows; - return newMetadata; - }, [metadata, markerIds]); + 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) => recordIds.push(record.id)); - store.deleteRecords(recordIds, { success_callback }); - }, [store]); - - const handleViewChange = useCallback((update) => { - metadataAPI.modifyView(repoID, metadata.view._id, update).then(res => { + 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,]); + }, [metadata, repoID, viewID, store, loadData]); useEffect(() => { - const unsubscribeViewChange = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.MAP_GALLERY_VIEW_CHANGE, handleViewChange); + const unsubscribeViewChange = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_SERVER_VIEW, onViewChange); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, true); return () => { unsubscribeViewChange(); + window?.sfMetadataContext?.eventBus?.dispatch(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, false); }; + }, [onViewChange]); + + useEffect(() => { + loadData({ sorts: allMetadata.view.sorts }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + if (isLoading) return (); + return (
-
-
- -
-
{gettext('Location')}
-
- +
); }; ClusterPhotos.propTypes = { - metadata: PropTypes.object, markerIds: PropTypes.array, onClose: PropTypes.func, }; diff --git a/frontend/src/metadata/views/map/index.js b/frontend/src/metadata/views/map/index.js index a4d51bf9eb7..9e55604ec16 100644 --- a/frontend/src/metadata/views/map/index.js +++ b/frontend/src/metadata/views/map/index.js @@ -1,24 +1,26 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, 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 MapView from './map'; +import { PREDEFINED_FILE_TYPE_OPTION_KEY } from '../../constants'; import { useMetadataView } from '../../hooks/metadata-view'; import { Utils } from '../../../utils/utils'; -import { siteRoot, thumbnailSizeForGrid } from '../../../utils/constants'; +import { gettext, siteRoot, thumbnailSizeForGrid } 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 './index.css'; const Map = () => { - const [showGallery, setShowGallery] = useState(false); - const [markerIds, setMarkerIds] = useState([]); - const { metadata } = useMetadataView(); + const [showCluster, setShowCluster] = useState(false); + const { metadata, viewID, updateCurrentPath } = useMetadataView(); + + const clusterRef = useRef([]); const repoID = window.sfMetadataContext.getSetting('repoID'); - const validImages = useMemo(() => { + const images = useMemo(() => { return metadata.rows .map(record => { const recordType = getFileTypeFromRecord(record); @@ -39,31 +41,28 @@ const Map = () => { .filter(Boolean); }, [repoID, metadata.rows]); - 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 openCluster = useCallback((clusterIds) => { + clusterRef.current = clusterIds; + updateCurrentPath(`/${PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES}/${viewID}/${gettext('Location')}`); + setShowCluster(true); + }, [viewID, updateCurrentPath]); - const closeGallery = useCallback(() => { - setShowGallery(false); - window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_MAP_VIEW_TOOLBAR, false); - }, []); + const closeCluster = useCallback(() => { + clusterRef.current = []; + updateCurrentPath(`/${PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES}/${viewID}`); + setShowCluster(false); + }, [viewID, updateCurrentPath]); useEffect(() => { - window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MAP_VIEW, metadata.view); - }, [metadata.view]); + updateCurrentPath(`/${PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES}/${viewID}`); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [viewID]); + + if (showCluster) { + return (); + } - return ( - <> - {showGallery ? ( - - ) : ( -
- )} - - ); + return (); }; export default Map; diff --git a/frontend/src/metadata/views/map/main.js b/frontend/src/metadata/views/map/map.js similarity index 75% rename from frontend/src/metadata/views/map/main.js rename to frontend/src/metadata/views/map/map.js index e9770b9fc48..89a956035c9 100644 --- a/frontend/src/metadata/views/map/main.js +++ b/frontend/src/metadata/views/map/map.js @@ -5,7 +5,7 @@ import { appAvatarURL, baiduMapKey, gettext, googleMapKey, mediaUrl } from '../. 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 { 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 toaster from '../../../components/toast'; @@ -14,7 +14,7 @@ const DEFAULT_POSITION = { lng: 104.195, lat: 35.861 }; const DEFAULT_ZOOM = 4; const BATCH_SIZE = 500; -const Main = ({ validImages, onOpen }) => { +const Main = ({ images, onOpenCluster }) => { const mapInfo = useMemo(() => initMapInfo({ baiduMapKey, googleMapKey }), []); const mapRef = useRef(null); @@ -61,13 +61,14 @@ const Main = ({ validImages, onOpen }) => { ); }, []); - const getMapType = useCallback((type) => { - if (!mapRef.current) return; + const getBMapType = useCallback((type) => { switch (type) { - case MAP_TYPE.SATELLITE: + case MAP_TYPE.SATELLITE: { return window.BMAP_SATELLITE_MAP; - default: + } + default: { return window.BMAP_NORMAL_MAP; + } } }, []); @@ -75,13 +76,13 @@ const Main = ({ validImages, onOpen }) => { if (!mapRef.current) return; const point = mapRef.current.getCenter && mapRef.current.getCenter(); const zoom = mapRef.current.getZoom && mapRef.current.getZoom(); - window.sfMetadataContext.localStorage.setItem('map-center', point); - window.sfMetadataContext.localStorage.setItem('map-zoom', zoom); + window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_CENTER_KEY, point); + window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_ZOOM_KEY, zoom); }, []); const loadMapState = useCallback(() => { - const savedCenter = window.sfMetadataContext.localStorage.getItem('map-center') || DEFAULT_POSITION; - const savedZoom = window.sfMetadataContext.localStorage.getItem('map-zoom') || DEFAULT_ZOOM; + 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 }; }, []); @@ -89,18 +90,18 @@ const Main = ({ validImages, onOpen }) => { saveMapState(); const imageIds = markers.map(marker => marker._imageId); - onOpen(imageIds); - }, [onOpen, saveMapState]); + onOpenCluster(imageIds); + }, [onOpenCluster, saveMapState]); const renderMarkersBatch = useCallback(() => { - if (!validImages.length || !clusterRef.current) return; + if (!images.length || !clusterRef.current) return; const startIndex = batchIndexRef.current * BATCH_SIZE; - const endIndex = Math.min(startIndex + BATCH_SIZE, validImages.length); + const endIndex = Math.min(startIndex + BATCH_SIZE, images.length); const batchMarkers = []; for (let i = startIndex; i < endIndex; i++) { - const image = validImages[i]; + const image = images[i]; const { lng, lat } = image; const point = new window.BMap.Point(lng, lat); const marker = customImageOverlay(point, image, { @@ -110,15 +111,15 @@ const Main = ({ validImages, onOpen }) => { } clusterRef.current.addMarkers(batchMarkers); - if (endIndex < validImages.length) { + if (endIndex < images.length) { batchIndexRef.current += 1; setTimeout(renderMarkersBatch, 20); // Schedule the next batch } - }, [validImages, onClickMarker]); + }, [images, onClickMarker]); - const initializeClusterer = useCallback(() => { + const initializeCluster = useCallback(() => { if (mapRef.current && !clusterRef.current) { - clusterRef.current = new window.BMapLib.MarkerClusterer(mapRef.current, { + clusterRef.current = new window.BMapLib.MarkerCluster(mapRef.current, { callback: (e, markers) => onClickMarker(e, markers) }); } @@ -131,7 +132,7 @@ const Main = ({ validImages, onOpen }) => { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition((userInfo) => { center = { lng: userInfo.coords.longitude, lat: userInfo.coords.latitude }; - window.sfMetadataContext.localStorage.setItem('map-center', center); + window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_CENTER_KEY, center); }); } if (!isValidPosition(center?.lng, center?.lat)) return; @@ -145,28 +146,30 @@ const Main = ({ validImages, onOpen }) => { mapRef.current.centerAndZoom(point, zoom); mapRef.current.enableScrollWheelZoom(true); - const savedValue = window.sfMetadataContext.localStorage.getItem('map-type'); - mapRef.current && mapRef.current.setMapType(getMapType(savedValue)); + + const savedValue = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_TYPE_KEY); + mapRef.current && mapRef.current.setMapType(getBMapType(savedValue)); addMapController(); initializeUserMarker(); - initializeClusterer(); + initializeCluster(); batchIndexRef.current = 0; renderMarkersBatch(); - }, [addMapController, initializeClusterer, initializeUserMarker, renderMarkersBatch, getMapType, loadMapState]); + }, [addMapController, initializeCluster, initializeUserMarker, renderMarkersBatch, getBMapType, loadMapState]); 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)); + const modifyMapTypeSubscribe = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.MODIFY_MAP_TYPE, (newType) => { + window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_TYPE_KEY, newType); + mapRef.current && mapRef.current.setMapType(getBMapType(newType)); }); return () => { - switchMapTypeSubscribe(); + modifyMapTypeSubscribe(); }; - }, [getMapType, saveMapState]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); useEffect(() => { if (mapInfo.type === MAP_PROVIDER.B_MAP) { @@ -186,8 +189,8 @@ const Main = ({ validImages, onOpen }) => { }; Main.propTypes = { - validImages: PropTypes.array, - onOpen: PropTypes.func, + images: PropTypes.array, + onOpenCluster: PropTypes.func, }; export default Main; diff --git a/frontend/src/metadata/views/table/utils/grid-utils.js b/frontend/src/metadata/views/table/utils/grid-utils.js index a835e80223b..3e0d4d9857f 100644 --- a/frontend/src/metadata/views/table/utils/grid-utils.js +++ b/frontend/src/metadata/views/table/utils/grid-utils.js @@ -2,8 +2,7 @@ import dayjs from 'dayjs'; import { getCellValueByColumn, getFileNameFromRecord, getRecordIdFromRecord, isCellValueChanged } from '../../../utils/cell'; import { getColumnByIndex, getColumnOriginName } from '../../../utils/column'; import { CellType, NOT_SUPPORT_DRAG_COPY_COLUMN_TYPES, PRIVATE_COLUMN_KEY, TRANSFER_TYPES, - REG_NUMBER_DIGIT, REG_STRING_NUMBER_PARTS, COLUMN_RATE_MAX_NUMBER, - PASTE_SOURCE, + REG_NUMBER_DIGIT, REG_STRING_NUMBER_PARTS, RATE_MAX_NUMBER, PASTE_SOURCE, } from '../../../constants'; import { getGroupRecordByIndex } from './group-metrics'; import { convertCellValue } from './convert-utils'; @@ -594,7 +593,7 @@ class GridUtils { } _getRatingLeastSquares(numberList, data) { - const { rate_max_number = COLUMN_RATE_MAX_NUMBER[4].name } = data || {}; + const { rate_max_number = RATE_MAX_NUMBER[4].name } = data || {}; let slope; let intercept; let xAverage; diff --git a/media/js/map/marker-clusterer.js b/media/js/map/marker-clusterer.js index 41f1aca44f1..57f58edc6fd 100644 --- a/media/js/map/marker-clusterer.js +++ b/media/js/map/marker-clusterer.js @@ -1,6 +1,6 @@ /** * @fileoverview MarkerClusterer标记聚合器用来解决加载大量点要素到地图上产生覆盖现象的问题,并提高性能。 - * 主入口类是MarkerClusterer, + * 主入口类是MarkerCluster, * 基于Baidu Map API 1.2。 * * @author Baidu Map Api Group @@ -97,11 +97,11 @@ var BMapLib = window.BMapLib = BMapLib || {}; }; /** - *@exports MarkerClusterer as BMapLib.MarkerClusterer + *@exports MarkerCluster as BMapLib.MarkerCluster */ - var MarkerClusterer = + var MarkerCluster = /** - * MarkerClusterer + * MarkerCluster * @class 用来解决加载大量点要素到地图上产生覆盖现象的问题,并提高性能 * @constructor * @param {Map} map 地图的一个实例。 @@ -113,7 +113,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * isAverangeCenter {Boolean} 聚合点的落脚位置是否是所有聚合在内点的平均值,默认为否,落脚在聚合内的第一个点
* styles {Array} 自定义聚合后的图标风格,请参考TextIconOverlay类
*/ - BMapLib.MarkerClusterer = function(map, options){ + BMapLib.MarkerCluster = function(map, options){ if (!map){ return; } @@ -151,7 +151,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * * @return 无返回值。 */ - MarkerClusterer.prototype.addMarkers = function(markers){ + MarkerCluster.prototype.addMarkers = function(markers){ for(var i = 0, len = markers.length; i } 聚合的样式风格集合 */ - MarkerClusterer.prototype.getStyles = function() { + MarkerCluster.prototype.getStyles = function() { return this._styles; }; @@ -385,7 +385,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * @param {Array} styles 样式风格数组 * @return 无返回值 */ - MarkerClusterer.prototype.setStyles = function(styles) { + MarkerCluster.prototype.setStyles = function(styles) { this._styles = styles; this._redraw(); }; @@ -394,7 +394,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * 获取单个聚合的最小数量。 * @return {Number} 单个聚合的最小数量。 */ - MarkerClusterer.prototype.getMinClusterSize = function() { + MarkerCluster.prototype.getMinClusterSize = function() { return this._minClusterSize; }; @@ -403,7 +403,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * @param {Number} size 单个聚合的最小数量。 * @return 无返回值。 */ - MarkerClusterer.prototype.setMinClusterSize = function(size) { + MarkerCluster.prototype.setMinClusterSize = function(size) { this._minClusterSize = size; this._redraw(); }; @@ -412,7 +412,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * 获取单个聚合的落脚点是否是聚合内所有标记的平均中心。 * @return {Boolean} true或false。 */ - MarkerClusterer.prototype.isAverageCenter = function() { + MarkerCluster.prototype.isAverageCenter = function() { return this._isAverageCenter; }; @@ -420,7 +420,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * 获取聚合的Map实例。 * @return {Map} Map的示例。 */ - MarkerClusterer.prototype.getMap = function() { + MarkerCluster.prototype.getMap = function() { return this._map; }; @@ -428,7 +428,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * 获取所有的标记数组。 * @return {Array} 标记数组。 */ - MarkerClusterer.prototype.getMarkers = function() { + MarkerCluster.prototype.getMarkers = function() { return this._markers; }; @@ -436,7 +436,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * 获取聚合的总数量。 * @return {Number} 聚合的总数量。 */ - MarkerClusterer.prototype.getClustersCount = function() { + MarkerCluster.prototype.getClustersCount = function() { var count = 0; for(var i = 0, cluster; cluster = this._clusters[i]; i++){ cluster.isReal() && count++; @@ -444,7 +444,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; return count; }; - MarkerClusterer.prototype.getCallback = function() { + MarkerCluster.prototype.getCallback = function() { return this._callback; } @@ -453,7 +453,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * Cluster * @class 表示一个聚合对象,该聚合,包含有N个标记,这N个标记组成的范围,并有予以显示在Map上的TextIconOverlay等。 * @constructor - * @param {MarkerClusterer} markerClusterer 一个标记聚合器示例。 + * @param {MarkerCluster} markerClusterer 一个标记聚合器示例。 */ function Cluster(markerClusterer){ this._markerClusterer = markerClusterer; From e8f261e0a9bb97ef2992246fd223513351f95ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E5=9B=BD=E7=92=87?= Date: Wed, 8 Jan 2025 15:10:50 +0800 Subject: [PATCH 5/9] feat: optimize code --- .../metadata/components/radio-group/index.css | 1 - .../metadata/components/radio-group/index.js | 1 - frontend/src/metadata/constants/view/table.js | 1 - media/js/map/marker-clusterer.js | 18 +++++++++--------- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/frontend/src/metadata/components/radio-group/index.css b/frontend/src/metadata/components/radio-group/index.css index 5bc9682ddb0..75c6e8705f1 100644 --- a/frontend/src/metadata/components/radio-group/index.css +++ b/frontend/src/metadata/components/radio-group/index.css @@ -51,4 +51,3 @@ .sf-metadata-radio-group .sf-metadata-radio-group-option.active + .sf-metadata-radio-group-option::before { opacity: 0; } - diff --git a/frontend/src/metadata/components/radio-group/index.js b/frontend/src/metadata/components/radio-group/index.js index 4718e3409a1..910f0cd136f 100644 --- a/frontend/src/metadata/components/radio-group/index.js +++ b/frontend/src/metadata/components/radio-group/index.js @@ -1,4 +1,3 @@ - import React, { useCallback, useMemo } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; diff --git a/frontend/src/metadata/constants/view/table.js b/frontend/src/metadata/constants/view/table.js index aab12d26102..aa056116516 100644 --- a/frontend/src/metadata/constants/view/table.js +++ b/frontend/src/metadata/constants/view/table.js @@ -98,4 +98,3 @@ export const PASTE_SOURCE = { COPY: 'copy', CUT: 'cut', }; - diff --git a/media/js/map/marker-clusterer.js b/media/js/map/marker-clusterer.js index 57f58edc6fd..1ab237d3dc4 100644 --- a/media/js/map/marker-clusterer.js +++ b/media/js/map/marker-clusterer.js @@ -1,5 +1,5 @@ /** - * @fileoverview MarkerClusterer标记聚合器用来解决加载大量点要素到地图上产生覆盖现象的问题,并提高性能。 + * @fileoverview MarkerCluster标记聚合器用来解决加载大量点要素到地图上产生覆盖现象的问题,并提高性能。 * 主入口类是MarkerCluster, * 基于Baidu Map API 1.2。 * @@ -583,14 +583,14 @@ var BMapLib = window.BMapLib = BMapLib || {}; let clickTimeout; this._clusterMarker.addEventListener("click", (event) => { if (clickTimeout) { - clearTimeout(clickTimeout); - clickTimeout = null; - return; + clearTimeout(clickTimeout); + clickTimeout = null; + return; } clickTimeout = setTimeout(() => { - if (this._markerClusterer && typeof this._markerClusterer.getCallback() === 'function') { - const markers = this._markers; - this._markerClusterer.getCallback()(event, markers); + if (this._markerClusterer && typeof this._markerClusterer.getCallback() === 'function') { + const markers = this._markers; + this._markerClusterer.getCallback()(event, markers); } clickTimeout = null; }, 300); // Delay to differentiate between single and double click @@ -598,8 +598,8 @@ var BMapLib = window.BMapLib = BMapLib || {}; this._clusterMarker.addEventListener("dblclick", (event) => { if (clickTimeout) { - clearTimeout(clickTimeout); - clickTimeout = null; + clearTimeout(clickTimeout); + clickTimeout = null; } // Do nothing on double click }); From e63fd157859c35c93d7caf1405d6c9db4cb6f2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E5=9B=BD=E7=92=87?= Date: Thu, 9 Jan 2025 16:28:50 +0800 Subject: [PATCH 6/9] feat: optimize code --- .../basic-filters/file-folder-filter.js | 2 +- .../basic-filters/gallery-file-type-filter.js | 2 +- .../basic-filters/table-file-type-filter.js | 2 +- .../basic-filters/tags-filter.js | 2 +- .../view-toolbar/map-view-toolbar/index.js | 5 +- .../views/map/cluster-photos/index.js | 9 +- .../views/map/control/geolocation-control.js | 8 +- .../views/map/control/zoom-control.js | 19 ++-- frontend/src/metadata/views/map/index.css | 3 +- frontend/src/metadata/views/map/index.js | 5 +- frontend/src/metadata/views/map/map.js | 105 ++++++++---------- .../map/overlay/custom-avatar-overlay.js | 2 +- .../views/map/overlay/custom-image-overlay.js | 10 +- .../lib-content-view/lib-content-view.js | 1 + frontend/src/utils/map-utils.js | 8 +- media/js/map/marker-clusterer.js | 49 ++++---- media/js/map/text-icon-overlay.js | 24 ++-- 17 files changed, 125 insertions(+), 131 deletions(-) diff --git a/frontend/src/metadata/components/popover/filter-popover/basic-filters/file-folder-filter.js b/frontend/src/metadata/components/popover/filter-popover/basic-filters/file-folder-filter.js index 1bf69397271..7a7652b7117 100644 --- a/frontend/src/metadata/components/popover/filter-popover/basic-filters/file-folder-filter.js +++ b/frontend/src/metadata/components/popover/filter-popover/basic-filters/file-folder-filter.js @@ -47,7 +47,7 @@ const FileOrFolderFilter = ({ readOnly, value = 'all', onChange: onChangeAPI }) return ( { return ( { readOnly={readOnly} searchable={true} supportMultipleSelect={true} - className="sf-metadata-basic-filters-select sf-metadata-table-view-basic-filter-file-type-select ml-4" + className="sf-metadata-basic-filters-select sf-metadata-table-view-basic-filter-file-type-select mr-4" value={displayValue} options={options} onSelectOption={onChange} 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 7996708c1a9..58eca8719e6 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 @@ -38,6 +38,7 @@ const MapViewToolBar = ({ }, []); 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 () => { @@ -45,10 +46,6 @@ const MapViewToolBar = ({ unsubscribeView && unsubscribeView(); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - setShowGalleryToolbar(false); }, [viewID]); if (showGalleryToolbar) { diff --git a/frontend/src/metadata/views/map/cluster-photos/index.js b/frontend/src/metadata/views/map/cluster-photos/index.js index c69fe7c3526..d829ebb3111 100644 --- a/frontend/src/metadata/views/map/cluster-photos/index.js +++ b/frontend/src/metadata/views/map/cluster-photos/index.js @@ -85,12 +85,17 @@ const ClusterPhotos = ({ markerIds, onClose }) => { }, [metadata, repoID, viewID, store, loadData]); useEffect(() => { - const unsubscribeViewChange = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_SERVER_VIEW, onViewChange); window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, true); return () => { - unsubscribeViewChange(); window?.sfMetadataContext?.eventBus?.dispatch(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, false); }; + }, []); + + useEffect(() => { + const unsubscribeViewChange = window?.sfMetadataContext?.eventBus?.subscribe(EVENT_BUS_TYPE.UPDATE_SERVER_VIEW, onViewChange); + return () => { + unsubscribeViewChange && unsubscribeViewChange(); + }; }, [onViewChange]); useEffect(() => { diff --git a/frontend/src/metadata/views/map/control/geolocation-control.js b/frontend/src/metadata/views/map/control/geolocation-control.js index 8ce5e38053f..d067348c819 100644 --- a/frontend/src/metadata/views/map/control/geolocation-control.js +++ b/frontend/src/metadata/views/map/control/geolocation-control.js @@ -1,12 +1,12 @@ import { mediaUrl } from '../../../../utils/constants'; import { Utils } from '../../../../utils/utils'; -export function createBMapGeolocationControl(BMap, callback) { +export function createBMapGeolocationControl(BMapGL, callback) { function GeolocationControl() { this.defaultAnchor = window.BMAP_ANCHOR_BOTTOM_RIGHT; - this.defaultOffset = new BMap.Size(30, Utils.isDesktop() ? 30 : 90); + this.defaultOffset = new BMapGL.Size(30, Utils.isDesktop() ? 30 : 90); } - GeolocationControl.prototype = new window.BMap.Control(); + GeolocationControl.prototype = new BMapGL.Control(); GeolocationControl.prototype.initialize = function (map) { const div = document.createElement('div'); div.className = 'sf-BMap-geolocation-control'; @@ -24,7 +24,7 @@ export function createBMapGeolocationControl(BMap, callback) { } div.onclick = (e) => { e.preventDefault(); - const geolocation = new BMap.Geolocation(); + const geolocation = new BMapGL.Geolocation(); div.className = 'sf-BMap-geolocation-control sf-BMap-geolocation-control-loading'; geolocation.getCurrentPosition((result) => { div.className = 'sf-BMap-geolocation-control'; diff --git a/frontend/src/metadata/views/map/control/zoom-control.js b/frontend/src/metadata/views/map/control/zoom-control.js index 995590dc316..de799d28408 100644 --- a/frontend/src/metadata/views/map/control/zoom-control.js +++ b/frontend/src/metadata/views/map/control/zoom-control.js @@ -1,11 +1,14 @@ import { Utils } from '../../../../utils/utils'; -export function createBMapZoomControl(BMap, callback) { +const maxZoom = 18; +const minZoom = 3; + +export function createBMapZoomControl(BMapGL, callback) { function ZoomControl() { this.defaultAnchor = window.BMAP_ANCHOR_BOTTOM_RIGHT; - this.defaultOffset = new BMap.Size(80, Utils.isDesktop() ? 30 : 90); + this.defaultOffset = new BMapGL.Size(80, Utils.isDesktop() ? 30 : 90); } - ZoomControl.prototype = new window.BMap.Control(); + ZoomControl.prototype = new BMapGL.Control(); ZoomControl.prototype.initialize = function (map) { const div = document.createElement('div'); div.className = 'sf-BMap-zoom-control'; @@ -35,21 +38,21 @@ export function createBMapZoomControl(BMap, callback) { const updateButtonStates = () => { const zoomLevel = map.getZoom(); - const maxZoom = map.getMapType().getMaxZoom(); - const minZoom = map.getMapType().getMinZoom(); - zoomInButton.disabled = zoomLevel >= maxZoom; zoomOutButton.disabled = zoomLevel <= minZoom; + callback && callback(zoomLevel); }; zoomInButton.onclick = (e) => { e.preventDefault(); - map.zoomTo(map.getZoom() + 2); + const nextZoom = map.getZoom() + 2; + map.zoomTo(Math.min(nextZoom, maxZoom)); }; zoomOutButton.onclick = (e) => { e.preventDefault(); - map.zoomTo(map.getZoom() - 2); + const nextZoom = map.getZoom() - 2; + map.zoomTo(Math.max(nextZoom, minZoom)); }; map.addEventListener('zoomend', updateButtonStates); diff --git a/frontend/src/metadata/views/map/index.css b/frontend/src/metadata/views/map/index.css index 98cbfca6d2d..b6afc61a495 100644 --- a/frontend/src/metadata/views/map/index.css +++ b/frontend/src/metadata/views/map/index.css @@ -28,7 +28,7 @@ .sf-metadata-view-map .custom-image-number { position: absolute; - right: -15px; + right: -16px; top: -16px; width: 32px; height: 32px; @@ -39,6 +39,7 @@ text-align: center; font-size: 16px; line-height: 20px; + font-weight: 400; } .sf-metadata-view-map .custom-image-container:active::before, diff --git a/frontend/src/metadata/views/map/index.js b/frontend/src/metadata/views/map/index.js index 9e55604ec16..4df75e4c95e 100644 --- a/frontend/src/metadata/views/map/index.js +++ b/frontend/src/metadata/views/map/index.js @@ -45,6 +45,7 @@ const Map = () => { clusterRef.current = clusterIds; updateCurrentPath(`/${PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES}/${viewID}/${gettext('Location')}`); setShowCluster(true); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [viewID, updateCurrentPath]); const closeCluster = useCallback(() => { @@ -55,8 +56,8 @@ const Map = () => { useEffect(() => { updateCurrentPath(`/${PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES}/${viewID}`); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [viewID]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); if (showCluster) { return (); diff --git a/frontend/src/metadata/views/map/map.js b/frontend/src/metadata/views/map/map.js index 89a956035c9..9ed2d35de35 100644 --- a/frontend/src/metadata/views/map/map.js +++ b/frontend/src/metadata/views/map/map.js @@ -1,30 +1,37 @@ 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 { 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 toaster from '../../../components/toast'; const DEFAULT_POSITION = { lng: 104.195, lat: 35.861 }; const DEFAULT_ZOOM = 4; const BATCH_SIZE = 500; -const Main = ({ images, onOpenCluster }) => { +const Map = ({ images, onOpenCluster }) => { const mapInfo = useMemo(() => initMapInfo({ baiduMapKey, googleMapKey }), []); const mapRef = useRef(null); const clusterRef = useRef(null); const batchIndexRef = useRef(0); + 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.BMap); + const ZoomControl = createBMapZoomControl(window.BMapGL, saveMapState); const zoomControl = new ZoomControl(); - const GeolocationControl = createBMapGeolocationControl(window.BMap, (err, point) => { + const GeolocationControl = createBMapGeolocationControl(window.BMapGL, (err, point) => { if (!err && point) { mapRef.current.setCenter({ lng: point.lng, lat: point.lat }); } @@ -34,37 +41,20 @@ const Main = ({ images, onOpenCluster }) => { 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')); - } - ); + }, [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); + setTimeout(() => mapRef.current.showOverlayContainer(), 3000); }, []); const getBMapType = useCallback((type) => { switch (type) { case MAP_TYPE.SATELLITE: { - return window.BMAP_SATELLITE_MAP; + return window.BMAP_EARTH_MAP; } default: { return window.BMAP_NORMAL_MAP; @@ -72,14 +62,6 @@ const Main = ({ images, onOpenCluster }) => { } }, []); - 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 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; @@ -88,8 +70,7 @@ const Main = ({ images, onOpenCluster }) => { const onClickMarker = useCallback((e, markers) => { saveMapState(); - - const imageIds = markers.map(marker => marker._imageId); + const imageIds = markers.map(marker => marker._id); onOpenCluster(imageIds); }, [onOpenCluster, saveMapState]); @@ -103,7 +84,7 @@ const Main = ({ images, onOpenCluster }) => { for (let i = startIndex; i < endIndex; i++) { const image = images[i]; const { lng, lat } = image; - const point = new window.BMap.Point(lng, lat); + const point = new window.BMapGL.Point(lng, lat); const marker = customImageOverlay(point, image, { callback: (e, markers) => onClickMarker(e, markers) }); @@ -126,42 +107,45 @@ const Main = ({ images, onOpenCluster }) => { }, [onClickMarker]); const renderBaiduMap = useCallback(() => { - if (!mapRef.current || !window.BMap.Map) return; + if (!mapRef.current || !window.BMapGL.Map) return; let { center, zoom } = loadMapState(); // ask for user location if (navigator.geolocation) { navigator.geolocation.getCurrentPosition((userInfo) => { center = { lng: userInfo.coords.longitude, lat: userInfo.coords.latitude }; + const gcPosition = wgs84_to_gcj02(center.lng, center.lat); + const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat); + const { lng, lat } = bdPosition; + center = new window.BMapGL.Point(lng, lat); window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_CENTER_KEY, center); }); } - if (!isValidPosition(center?.lng, center?.lat)) return; + const mapTypeValue = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_TYPE_KEY); + mapRef.current = new window.BMapGL.Map('sf-metadata-map-container', { + enableMapClick: false, + mapType: getBMapType(mapTypeValue) + }); - const gcPosition = wgs84_to_gcj02(center.lng, center.lat); - const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat); - const { lng, lat } = bdPosition; + if (isValidPosition(center?.lng, center?.lat)) { + mapRef.current.centerAndZoom(center, zoom); + } - mapRef.current = new window.BMap.Map('sf-metadata-map-container', { enableMapClick: false }); - const point = new window.BMap.Point(lng, lat); - mapRef.current.centerAndZoom(point, zoom); mapRef.current.enableScrollWheelZoom(true); - - const savedValue = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_TYPE_KEY); - mapRef.current && mapRef.current.setMapType(getBMapType(savedValue)); - - addMapController(); - initializeUserMarker(); + initializeUserMarker(center); initializeCluster(); batchIndexRef.current = 0; renderMarkersBatch(); + + addMapController(); }, [addMapController, initializeCluster, initializeUserMarker, renderMarkersBatch, getBMapType, loadMapState]); useEffect(() => { const modifyMapTypeSubscribe = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.MODIFY_MAP_TYPE, (newType) => { window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_TYPE_KEY, newType); - mapRef.current && mapRef.current.setMapType(getBMapType(newType)); + const mapType = getBMapType(newType); + mapRef.current && mapRef.current.setMapType(mapType); }); return () => { @@ -179,7 +163,8 @@ const Main = ({ images, onOpenCluster }) => { }; } return; - }, [mapInfo, renderBaiduMap]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return (
@@ -188,9 +173,9 @@ const Main = ({ images, onOpenCluster }) => { ); }; -Main.propTypes = { +Map.propTypes = { images: PropTypes.array, onOpenCluster: PropTypes.func, }; -export default Main; +export default Map; diff --git a/frontend/src/metadata/views/map/overlay/custom-avatar-overlay.js b/frontend/src/metadata/views/map/overlay/custom-avatar-overlay.js index 72d97808da9..9b21e978342 100644 --- a/frontend/src/metadata/views/map/overlay/custom-avatar-overlay.js +++ b/frontend/src/metadata/views/map/overlay/custom-avatar-overlay.js @@ -1,5 +1,5 @@ const customAvatarOverlay = (point, avatarUrl, bgUrl, width = 20, height = 25) => { - class AvatarOverlay extends window.BMap.Overlay { + class AvatarOverlay extends window.BMapGL.Overlay { constructor(point, avatarUrl, bgUrl, width, height) { super(); this._point = point; diff --git a/frontend/src/metadata/views/map/overlay/custom-image-overlay.js b/frontend/src/metadata/views/map/overlay/custom-image-overlay.js index b882bfbe114..583d6def107 100644 --- a/frontend/src/metadata/views/map/overlay/custom-image-overlay.js +++ b/frontend/src/metadata/views/map/overlay/custom-image-overlay.js @@ -5,8 +5,8 @@ const customImageOverlay = (center, image, callback) => { constructor(center, image, { callback } = {}) { super(center, '', { styles: [] }); this._center = center; - this._imageUrl = image.src; - this._imageId = image.id; + this._URL = image.src; + this._id = image.id; this._callback = callback; } @@ -18,11 +18,11 @@ const customImageOverlay = (center, image, callback) => { map.getPanes().markerPane.appendChild(div); this._div = div; - const imageElement = ``; + const imageElement = ``; const htmlString = `
- ${this._imageUrl ? imageElement : '
'} + ${this._URL ? imageElement : '
'}
`; const labelDocument = new DOMParser().parseFromString(htmlString, 'text/html'); @@ -31,7 +31,7 @@ const customImageOverlay = (center, image, callback) => { const eventHandler = (event) => { event.preventDefault(); - this._callback && this._callback(event, [{ _imageId: this._imageId }]); + this._callback && this._callback(event, [{ _id: this._id }]); }; if (Utils.isDesktop()) { diff --git a/frontend/src/pages/lib-content-view/lib-content-view.js b/frontend/src/pages/lib-content-view/lib-content-view.js index ba90c75833c..f11e9fad8cd 100644 --- a/frontend/src/pages/lib-content-view/lib-content-view.js +++ b/frontend/src/pages/lib-content-view/lib-content-view.js @@ -2141,6 +2141,7 @@ class LibContentView extends React.Component { }; updatePath = (path) => { + if (this.state.path === path) return; this.setState({ path }); }; diff --git a/frontend/src/utils/map-utils.js b/frontend/src/utils/map-utils.js index 1b68b04acd6..fa2c07f3952 100644 --- a/frontend/src/utils/map-utils.js +++ b/frontend/src/utils/map-utils.js @@ -39,16 +39,16 @@ export default function loadBMap(ak) { export function asyncLoadBaiduJs(ak) { return new Promise((resolve, reject) => { - if (typeof window.BMap !== 'undefined') { - resolve(window.BMap); + if (typeof window.BMapGL !== 'undefined') { + resolve(window.BMapGL); return; } window.renderMap = function () { - resolve(window.BMap); + resolve(window.BMapGL); }; let script = document.createElement('script'); script.type = 'text/javascript'; - script.src = `https://api.map.baidu.com/api?v=3.0&ak=${ak}&callback=renderMap`; + script.src = `https://api.map.baidu.com/api?type=webgl&v=1.0&ak=${ak}&callback=renderMap`; script.onerror = reject; document.body.appendChild(script); }); diff --git a/media/js/map/marker-clusterer.js b/media/js/map/marker-clusterer.js index 1ab237d3dc4..283af7b3fff 100644 --- a/media/js/map/marker-clusterer.js +++ b/media/js/map/marker-clusterer.js @@ -15,11 +15,11 @@ var BMapLib = window.BMapLib = BMapLib || {}; /** * 获取一个扩展的视图范围,把上下左右都扩大一样的像素值。 - * @param {Map} map BMap.Map的实例化对象 - * @param {BMap.Bounds} bounds BMap.Bounds的实例化对象 + * @param {Map} map BMapGL.Map的实例化对象 + * @param {BMapGL.Bounds} bounds BMapGL.Bounds的实例化对象 * @param {Number} gridSize 要扩大的像素值 * - * @return {BMap.Bounds} 返回扩大后的视图范围。 + * @return {BMapGL.Bounds} 返回扩大后的视图范围。 */ var getExtendedBounds = function(map, bounds, gridSize){ bounds = cutBoundsInRange(bounds); @@ -31,21 +31,22 @@ var BMapLib = window.BMapLib = BMapLib || {}; pixelSW.y += gridSize; var newNE = map.pixelToPoint(pixelNE); var newSW = map.pixelToPoint(pixelSW); - return new BMap.Bounds(newSW, newNE); + return new BMapGL.Bounds(newSW, newNE); }; /** * 按照百度地图支持的世界范围对bounds进行边界处理 - * @param {BMap.Bounds} bounds BMap.Bounds的实例化对象 + * @param {BMapGL.Bounds} bounds BMapGL.Bounds的实例化对象 * - * @return {BMap.Bounds} 返回不越界的视图范围 + * @return {BMapGL.Bounds} 返回不越界的视图范围 */ var cutBoundsInRange = function (bounds) { + console.log(bounds); var maxX = getRange(bounds.getNorthEast().lng, -180, 180); var minX = getRange(bounds.getSouthWest().lng, -180, 180); - var maxY = getRange(bounds.getNorthEast().lat, -74, 74); - var minY = getRange(bounds.getSouthWest().lat, -74, 74); - return new BMap.Bounds(new BMap.Point(minX, minY), new BMap.Point(maxX, maxY)); + var maxY = getRange(bounds.getNorthEast().lat, -90, 90); + var minY = getRange(bounds.getSouthWest().lat, -90, 90); + return new BMapGL.Bounds(new BMapGL.Point(minX, minY), new BMapGL.Point(maxX, maxY)); }; /** @@ -137,9 +138,9 @@ var BMapLib = window.BMapLib = BMapLib || {}; that._redraw(); }); - // this._map.addEventListener("moveend",function(){ - // that._redraw(); - // }); + this._map.addEventListener("moveend",function(){ + that._redraw(); + }); var mkrs = opts["markers"]; isArray(mkrs) && this.addMarkers(mkrs); @@ -160,7 +161,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; /** * 把一个标记添加到要聚合的标记数组中 - * @param {BMap.Marker} marker 要添加的标记 + * @param {BMapGL.Marker} marker 要添加的标记 * * @return 无返回值。 */ @@ -174,7 +175,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; /** * 添加一个聚合的标记。 - * @param {BMap.Marker} marker 要聚合的单个标记。 + * @param {BMapGL.Marker} marker 要聚合的单个标记。 * @return 无返回值。 */ MarkerCluster.prototype.addMarker = function(marker) { @@ -190,7 +191,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; var mapBounds = this._map.getBounds(); var extendedBounds = getExtendedBounds(this._map, mapBounds, this._gridSize); for(var i = 0, marker; marker = this._markers[i]; i++){ - if(!marker.isInCluster && extendedBounds.containsPoint(marker.getPosition()) ){ + if(!marker.isInCluster && extendedBounds.containsPoint(marker._position) ){ this._addToClosestCluster(marker); } } @@ -205,7 +206,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; /** * 根据标记的位置,把它添加到最近的聚合中 - * @param {BMap.Marker} marker 要进行聚合的单个标记 + * @param {BMapGL.Marker} marker 要进行聚合的单个标记 * * @return 无返回值。 */ @@ -268,7 +269,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; /** * 删除单个标记 - * @param {BMap.Marker} marker 需要被删除的marker + * @param {BMapGL.Marker} marker 需要被删除的marker * * @return {Boolean} 删除成功返回true,否则返回false */ @@ -284,7 +285,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; /** * 删除单个标记 - * @param {BMap.Marker} marker 需要被删除的marker + * @param {BMapGL.Marker} marker 需要被删除的marker * * @return {Boolean} 删除成功返回true,否则返回false */ @@ -299,7 +300,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; /** * 删除一组标记 - * @param {Array} markers 需要被删除的marker数组 + * @param {Array} markers 需要被删除的marker数组 * * @return {Boolean} 删除成功返回true,否则返回false */ @@ -487,7 +488,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; var l = this._markers.length + 1; var lat = (this._center.lat * (l - 1) + marker.getPosition().lat) / l; var lng = (this._center.lng * (l - 1) + marker.getPosition().lng) / l; - this._center = new BMap.Point(lng, lat); + this._center = new BMapGL.Point(lng, lat); this.updateGridBounds(); }//计算新的Center } @@ -550,7 +551,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * @return 无返回值。 */ Cluster.prototype.updateGridBounds = function() { - var bounds = new BMap.Bounds(this._center, this._center); + var bounds = new BMapGL.Bounds(this._center, this._center); this._gridBounds = getExtendedBounds(this._map, bounds, this._markerClusterer.getGridSize()); }; @@ -620,10 +621,10 @@ var BMapLib = window.BMapLib = BMapLib || {}; /** * 获取该聚合所包含的所有标记的最小外接矩形的范围。 - * @return {BMap.Bounds} 计算出的范围。 + * @return {BMapGL.Bounds} 计算出的范围。 */ Cluster.prototype.getBounds = function() { - var bounds = new BMap.Bounds(this._center,this._center); + var bounds = new BMapGL.Bounds(this._center,this._center); for (var i = 0, marker; marker = this._markers[i]; i++) { bounds.extend(marker.getPosition()); } @@ -632,7 +633,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; /** * 获取该聚合的落脚点。 - * @return {BMap.Point} 该聚合的落脚点。 + * @return {BMapGL.Point} 该聚合的落脚点。 */ Cluster.prototype.getCenter = function() { return this._center; diff --git a/media/js/map/text-icon-overlay.js b/media/js/map/text-icon-overlay.js index 9c4ead85a00..963f8116175 100644 --- a/media/js/map/text-icon-overlay.js +++ b/media/js/map/text-icon-overlay.js @@ -754,21 +754,21 @@ var BMapLib = window.BMapLib = BMapLib || {}; (!this._styles.length) && this._setupDefaultStyles(); }; - T.lang.inherits(TextIconOverlay, BMap.Overlay, "TextIconOverlay"); + T.lang.inherits(TextIconOverlay, BMapGL.Overlay, "TextIconOverlay"); TextIconOverlay.prototype._setupDefaultStyles = function(){ var sizes = [53, 56, 66, 78, 90]; for(var i = 0, size; size = sizes[i]; i++){ this._styles.push({ url:_IMAGE_PATH + i + '.' + _IMAGE_EXTENSION, - size: new BMap.Size(size, size) + size: new BMapGL.Size(size, size) }); }//for循环的简洁写法 }; /** *继承Overlay的intialize方法,自定义覆盖物时必须。 - *@param {Map} map BMap.Map的实例化对象。 + *@param {Map} map BMapGL.Map的实例化对象。 *@return {HTMLElement} 返回覆盖物对应的HTML元素。 */ TextIconOverlay.prototype.initialize = function(map){ @@ -944,12 +944,12 @@ var BMapLib = window.BMapLib = BMapLib || {}; } else { csstext.push('background-image:url(' + url + ');'); var backgroundPosition = '0 0'; - (offset instanceof BMap.Size) && (backgroundPosition = offset.width + 'px' + ' ' + offset.height + 'px'); + (offset instanceof BMapGL.Size) && (backgroundPosition = offset.width + 'px' + ' ' + offset.height + 'px'); csstext.push('background-position:' + backgroundPosition + ';'); } - if (size instanceof BMap.Size){ - if (anchor instanceof BMap.Size) { + if (size instanceof BMapGL.Size){ + if (anchor instanceof BMapGL.Size) { if (anchor.height > 0 && anchor.height < size.height) { csstext.push('height:' + (size.height - anchor.height) + 'px; padding-top:' + anchor.height + 'px;'); } @@ -999,9 +999,9 @@ var BMapLib = window.BMapLib = BMapLib || {}; *
"target:{BMapLib.TextIconOverlay} 事件目标 - *
"point : {BMap.Point} 最新添加上的节点BMap.Point对象 + *
"point : {BMapGL.Point} 最新添加上的节点BMap.Point对象 - *
"pixel:{BMap.pixel} 最新添加上的节点BMap.Pixel对象 + *
"pixel:{BMapGL.pixel} 最新添加上的节点BMap.Pixel对象 * @@ -1025,9 +1025,9 @@ var BMapLib = window.BMapLib = BMapLib || {}; *
"target:{BMapLib.TextIconOverlay} 事件目标 - *
"point : {BMap.Point} 最新添加上的节点BMap.Point对象 + *
"point : {BMapGL.Point} 最新添加上的节点BMap.Point对象 - *
"pixel:{BMap.pixel} 最新添加上的节点BMap.Pixel对象 + *
"pixel:{BMapGL.pixel} 最新添加上的节点BMap.Pixel对象 * @@ -1058,7 +1058,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; var y = e.clientY || e.pageY; if (e && be && x && y && elem){ var offset = T.dom.getPosition(map.getContainer()); - be.pixel = new BMap.Pixel(x - offset.left, y - offset.top); + be.pixel = new BMapGL.Pixel(x - offset.left, y - offset.top); be.point = map.pixelToPoint(be.pixel); } return be; @@ -1075,4 +1075,4 @@ var BMapLib = window.BMapLib = BMapLib || {}; }); }; -})(); \ No newline at end of file +})(); From ed7f501d4dd567f1941be3c36661b322c9fac74f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E5=9B=BD=E7=92=87?= Date: Thu, 9 Jan 2025 17:42:29 +0800 Subject: [PATCH 7/9] feat: optimize code --- frontend/src/metadata/views/map/index.css | 4 ++++ frontend/src/metadata/views/map/map.js | 22 ++++++++++--------- .../map/overlay/custom-avatar-overlay.js | 1 + media/js/map/marker-clusterer.js | 14 ++++++------ 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/frontend/src/metadata/views/map/index.css b/frontend/src/metadata/views/map/index.css index b6afc61a495..ca7ed525896 100644 --- a/frontend/src/metadata/views/map/index.css +++ b/frontend/src/metadata/views/map/index.css @@ -5,6 +5,10 @@ flex-direction: column; } +.sf-metadata-view-map #platform div:nth-child(2) div:has(.custom-avatar-overlay) { + display: block !important; +} + .sf-metadata-view-map .sf-metadata-map-container { width: 100%; height: 100%; diff --git a/frontend/src/metadata/views/map/map.js b/frontend/src/metadata/views/map/map.js index 9ed2d35de35..4dbef7eadd9 100644 --- a/frontend/src/metadata/views/map/map.js +++ b/frontend/src/metadata/views/map/map.js @@ -33,7 +33,7 @@ const Map = ({ images, onOpenCluster }) => { const zoomControl = new ZoomControl(); const GeolocationControl = createBMapGeolocationControl(window.BMapGL, (err, point) => { if (!err && point) { - mapRef.current.setCenter({ lng: point.lng, lat: point.lat }); + mapRef.current.setCenter(point); } }); @@ -48,7 +48,6 @@ const Map = ({ images, onOpenCluster }) => { const imageUrl = `${mediaUrl}img/marker.png`; const avatarMarker = customAvatarOverlay(centerPoint, appAvatarURL, imageUrl); mapRef.current.addOverlay(avatarMarker); - setTimeout(() => mapRef.current.showOverlayContainer(), 3000); }, []); const getBMapType = useCallback((type) => { @@ -101,7 +100,8 @@ const Map = ({ images, onOpenCluster }) => { const initializeCluster = useCallback(() => { if (mapRef.current && !clusterRef.current) { clusterRef.current = new window.BMapLib.MarkerCluster(mapRef.current, { - callback: (e, markers) => onClickMarker(e, markers) + callback: (e, markers) => onClickMarker(e, markers), + maxZoom: 21, }); } }, [onClickMarker]); @@ -109,21 +109,24 @@ const Map = ({ images, onOpenCluster }) => { 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) => { - center = { lng: userInfo.coords.longitude, lat: userInfo.coords.latitude }; - const gcPosition = wgs84_to_gcj02(center.lng, center.lat); + const gcPosition = wgs84_to_gcj02(userInfo.coords.longitude, userInfo.coords.latitude); const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat); const { lng, lat } = bdPosition; - center = new window.BMapGL.Point(lng, lat); + 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, - mapType: getBMapType(mapTypeValue) + minZoom: 3, + maxZoom: 21, + mapType: getBMapType(mapTypeValue), }); if (isValidPosition(center?.lng, center?.lat)) { @@ -131,14 +134,13 @@ const Map = ({ images, onOpenCluster }) => { } mapRef.current.enableScrollWheelZoom(true); + addMapController(); - initializeUserMarker(center); + initializeUserMarker(userPosition); initializeCluster(); batchIndexRef.current = 0; renderMarkersBatch(); - - addMapController(); }, [addMapController, initializeCluster, initializeUserMarker, renderMarkersBatch, getBMapType, loadMapState]); useEffect(() => { diff --git a/frontend/src/metadata/views/map/overlay/custom-avatar-overlay.js b/frontend/src/metadata/views/map/overlay/custom-avatar-overlay.js index 9b21e978342..27b0545e70a 100644 --- a/frontend/src/metadata/views/map/overlay/custom-avatar-overlay.js +++ b/frontend/src/metadata/views/map/overlay/custom-avatar-overlay.js @@ -13,6 +13,7 @@ const customAvatarOverlay = (point, avatarUrl, bgUrl, width = 20, height = 25) = this._map = map; const divBox = document.createElement('div'); const divImg = new Image(); + divBox.className = 'custom-avatar-overlay'; divBox.style.position = 'absolute'; divBox.style.width = `${this._width}px`; divBox.style.height = `${this._height}px`; diff --git a/media/js/map/marker-clusterer.js b/media/js/map/marker-clusterer.js index 283af7b3fff..e29c8710248 100644 --- a/media/js/map/marker-clusterer.js +++ b/media/js/map/marker-clusterer.js @@ -31,6 +31,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; pixelSW.y += gridSize; var newNE = map.pixelToPoint(pixelNE); var newSW = map.pixelToPoint(pixelSW); + if (!newSW || !newNE) return null; return new BMapGL.Bounds(newSW, newNE); }; @@ -41,7 +42,6 @@ var BMapLib = window.BMapLib = BMapLib || {}; * @return {BMapGL.Bounds} 返回不越界的视图范围 */ var cutBoundsInRange = function (bounds) { - console.log(bounds); var maxX = getRange(bounds.getNorthEast().lng, -180, 180); var minX = getRange(bounds.getSouthWest().lng, -180, 180); var maxY = getRange(bounds.getNorthEast().lat, -90, 90); @@ -124,7 +124,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; var opts = options || {}; this._gridSize = opts["gridSize"] || 60; - this._maxZoom = opts["maxZoom"] || 18; + this._maxZoom = opts["maxZoom"] || 21; this._minClusterSize = opts["minClusterSize"] || 2; this._isAverageCenter = false; if (opts['isAverageCenter'] != undefined) { @@ -138,9 +138,9 @@ var BMapLib = window.BMapLib = BMapLib || {}; that._redraw(); }); - this._map.addEventListener("moveend",function(){ - that._redraw(); - }); + // this._map.addEventListener("moveend",function(){ + // that._redraw(); + // }); var mkrs = opts["markers"]; isArray(mkrs) && this.addMarkers(mkrs); @@ -191,7 +191,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; var mapBounds = this._map.getBounds(); var extendedBounds = getExtendedBounds(this._map, mapBounds, this._gridSize); for(var i = 0, marker; marker = this._markers[i]; i++){ - if(!marker.isInCluster && extendedBounds.containsPoint(marker._position) ){ + if(!marker.isInCluster && extendedBounds && extendedBounds.containsPoint(marker._position) ){ this._addToClosestCluster(marker); } } @@ -624,7 +624,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * @return {BMapGL.Bounds} 计算出的范围。 */ Cluster.prototype.getBounds = function() { - var bounds = new BMapGL.Bounds(this._center,this._center); + var bounds = new BMapGL.Bounds(this._center, this._center); for (var i = 0, marker; marker = this._markers[i]; i++) { bounds.extend(marker.getPosition()); } From 125ce2430c7a7d6d2a6bf68609d4bd87a506634e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E5=9B=BD=E7=92=87?= Date: Thu, 9 Jan 2025 17:45:44 +0800 Subject: [PATCH 8/9] feat: optimize code --- frontend/src/metadata/views/map/index.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/metadata/views/map/index.css b/frontend/src/metadata/views/map/index.css index ca7ed525896..cf9cd9ce286 100644 --- a/frontend/src/metadata/views/map/index.css +++ b/frontend/src/metadata/views/map/index.css @@ -5,7 +5,11 @@ flex-direction: column; } -.sf-metadata-view-map #platform div:nth-child(2) div:has(.custom-avatar-overlay) { +.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; } From 13efff8eb04ee707916b90400f2f7e79aa07206c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E5=9B=BD=E7=92=87?= Date: Fri, 10 Jan 2025 14:02:03 +0800 Subject: [PATCH 9/9] fix: ui --- frontend/src/metadata/utils/sort/sort-row.js | 4 +- .../face-recognition/person-photos/index.js | 50 ++++++- .../views/map/cluster-photos/index.js | 81 ++++++++--- .../views/map/control/geolocation-control.js | 50 ------- .../views/map/control/zoom-control.js | 68 --------- frontend/src/metadata/views/map/index.css | 133 ------------------ frontend/src/metadata/views/map/index.js | 4 +- .../control/geolocation-control/index.css | 4 + .../control/geolocation-control/index.js | 44 ++++++ .../views/map/map-view/control/index.css | 63 +++++++++ .../views/map/{ => map-view}/control/index.js | 2 + .../map-view/control/zoom-control/index.css | 3 + .../map-view/control/zoom-control/index.js | 59 ++++++++ .../src/metadata/views/map/map-view/index.css | 82 +++++++++++ .../views/map/{map.js => map-view/index.js} | 35 ++--- .../overlay/custom-avatar-overlay.js | 0 .../overlay/custom-image-overlay.js | 2 +- .../views/map/{ => map-view}/overlay/index.js | 0 frontend/src/utils/map-utils.js | 6 +- media/css/sf_font3/iconfont.css | 40 ++++-- media/css/sf_font3/iconfont.eot | Bin 29884 -> 30864 bytes media/css/sf_font3/iconfont.svg | 18 ++- media/css/sf_font3/iconfont.ttf | Bin 29716 -> 30696 bytes media/css/sf_font3/iconfont.woff | Bin 17600 -> 18208 bytes media/css/sf_font3/iconfont.woff2 | Bin 14836 -> 15388 bytes ...{marker-clusterer.js => marker-cluster.js} | 28 ++-- media/js/map/text-icon-overlay.js | 53 ++++--- 27 files changed, 475 insertions(+), 354 deletions(-) delete mode 100644 frontend/src/metadata/views/map/control/geolocation-control.js delete mode 100644 frontend/src/metadata/views/map/control/zoom-control.js create mode 100644 frontend/src/metadata/views/map/map-view/control/geolocation-control/index.css create mode 100644 frontend/src/metadata/views/map/map-view/control/geolocation-control/index.js create mode 100644 frontend/src/metadata/views/map/map-view/control/index.css rename frontend/src/metadata/views/map/{ => map-view}/control/index.js (89%) create mode 100644 frontend/src/metadata/views/map/map-view/control/zoom-control/index.css create mode 100644 frontend/src/metadata/views/map/map-view/control/zoom-control/index.js create mode 100644 frontend/src/metadata/views/map/map-view/index.css rename frontend/src/metadata/views/map/{map.js => map-view/index.js} (88%) rename frontend/src/metadata/views/map/{ => map-view}/overlay/custom-avatar-overlay.js (100%) rename frontend/src/metadata/views/map/{ => map-view}/overlay/custom-image-overlay.js (97%) rename frontend/src/metadata/views/map/{ => map-view}/overlay/index.js (100%) rename media/js/map/{marker-clusterer.js => marker-cluster.js} (95%) diff --git a/frontend/src/metadata/utils/sort/sort-row.js b/frontend/src/metadata/utils/sort/sort-row.js index b3b3b714e36..376abb654b7 100644 --- a/frontend/src/metadata/utils/sort/sort-row.js +++ b/frontend/src/metadata/utils/sort/sort-row.js @@ -59,13 +59,13 @@ const sortRowsWithMultiSorts = (tableRows, sorts, { collaborators }) => { * @param {object} value e.g. { collaborators, ... } * @returns sorted rows ids, array */ -const sortTableRows = (table, rows, sorts, { collaborators }) => { +const sortTableRows = (table, rows, sorts, { collaborators, isReturnID = true } = {}) => { const { columns } = table; if (!Array.isArray(rows) || rows.length === 0) return []; const sortRows = rows.slice(0); const validSorts = deleteInvalidSort(sorts, columns); sortRowsWithMultiSorts(sortRows, validSorts, { collaborators }); - return sortRows.map((row) => row._id); + return isReturnID ? sortRows.map((row) => row._id) : sortRows; }; export { diff --git a/frontend/src/metadata/views/face-recognition/person-photos/index.js b/frontend/src/metadata/views/face-recognition/person-photos/index.js index ce4169499cb..f8692af4258 100644 --- a/frontend/src/metadata/views/face-recognition/person-photos/index.js +++ b/frontend/src/metadata/views/face-recognition/person-photos/index.js @@ -1,6 +1,8 @@ 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 metadataAPI from '../../../api'; import Metadata from '../../../model/metadata'; @@ -10,11 +12,16 @@ import { Utils } from '../../../../utils/utils'; import toaster from '../../../../components/toast'; import Gallery from '../../gallery/main'; import { useMetadataView } from '../../../hooks/metadata-view'; -import { PER_LOAD_NUMBER, EVENT_BUS_TYPE, FACE_RECOGNITION_VIEW_ID } from '../../../constants'; +import { PER_LOAD_NUMBER, EVENT_BUS_TYPE, FACE_RECOGNITION_VIEW_ID, UTC_FORMAT_DEFAULT } from '../../../constants'; +import { getRecordIdFromRecord, getParentDirFromRecord, getFileNameFromRecord } from '../../../utils/cell'; +import { sortTableRows } from '../../../utils/sort'; +import { useCollaborators } from '../../../hooks/collaborators'; import './index.css'; import '../../gallery/index.css'; +dayjs.extend(utc); + const PeoplePhotos = ({ view, people, onClose, onDeletePeoplePhotos, onRemovePeoplePhotos }) => { const [isLoading, setLoading] = useState(true); const [isLoadingMore, setLoadingMore] = useState(false); @@ -22,6 +29,7 @@ const PeoplePhotos = ({ view, people, onClose, onDeletePeoplePhotos, onRemovePeo const repoID = window.sfMetadataContext.getSetting('repoID'); const { deleteFilesCallback, store } = useMetadataView(); + const { collaborators } = useCollaborators(); const onLoadMore = useCallback(async () => { if (isLoadingMore) return; @@ -150,6 +158,38 @@ const PeoplePhotos = ({ view, people, onClose, onDeletePeoplePhotos, onRemovePeo }); }, [repoID, metadata, 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(() => { loadData({ sorts: view.sorts }); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -164,11 +204,15 @@ const PeoplePhotos = ({ view, people, onClose, onDeletePeoplePhotos, onRemovePeo }, []); useEffect(() => { - const unsubscribeViewChange = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_SERVER_VIEW, onViewChange); + 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]); + }, [onViewChange, onRecordChange]); if (isLoading) return (); diff --git a/frontend/src/metadata/views/map/cluster-photos/index.js b/frontend/src/metadata/views/map/cluster-photos/index.js index d829ebb3111..4678e616fc7 100644 --- a/frontend/src/metadata/views/map/cluster-photos/index.js +++ b/frontend/src/metadata/views/map/cluster-photos/index.js @@ -1,9 +1,11 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +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 } from '../../../constants'; +import { EVENT_BUS_TYPE, UTC_FORMAT_DEFAULT } from '../../../constants'; import metadataAPI from '../../../api'; import { Utils } from '../../../../utils/utils'; import toaster from '../../../../components/toast'; @@ -12,30 +14,29 @@ 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'; -const ClusterPhotos = ({ markerIds, onClose }) => { - const [isLoading, setLoading] = useState(true); - const [metadata, setMetadata] = useState({ rows: [] }); +dayjs.extend(utc); +const ClusterPhotos = ({ photoIds, onClose }) => { const { repoID, viewID, metadata: allMetadata, store, addFolder, deleteRecords } = useMetadataView(); const { collaborators } = useCollaborators(); - const rows = useMemo(() => getRowsByIds(allMetadata, markerIds), [allMetadata, markerIds]); - const columns = useMemo(() => allMetadata?.columns || [], [allMetadata]); + const [isLoading, setLoading] = useState(true); + const [metadata, setMetadata] = useState({ rows: getRowsByIds(allMetadata, photoIds), columns: allMetadata?.columns || [] }); const loadData = useCallback((view) => { setLoading(true); - const orderRows = sortTableRows({ columns }, rows, view?.sorts || [], { collaborators }); - let metadata = new Metadata({ rows, columns, view }); - metadata.hasMore = false; - metadata.row_ids = orderRows; - metadata.view.rows = orderRows; - setMetadata(metadata); - window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.RESET_VIEW, metadata.view); + 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); - }, [rows, columns, collaborators]); + }, [metadata, collaborators]); const deletedByIds = useCallback((ids) => { if (!Array.isArray(ids) || ids.length === 0) return; @@ -84,23 +85,61 @@ const ClusterPhotos = ({ markerIds, onClose }) => { }); }, [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(() => { - window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, true); + const eventBus = window?.sfMetadataContext?.eventBus; + if (!eventBus) return; + eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, true); return () => { - window?.sfMetadataContext?.eventBus?.dispatch(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, false); + eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, false); }; }, []); useEffect(() => { - const unsubscribeViewChange = window?.sfMetadataContext?.eventBus?.subscribe(EVENT_BUS_TYPE.UPDATE_SERVER_VIEW, onViewChange); + 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]); + }, [onViewChange, onRecordChange]); useEffect(() => { loadData({ sorts: allMetadata.view.sorts }); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); if (isLoading) return (); @@ -113,7 +152,7 @@ const ClusterPhotos = ({ markerIds, onClose }) => { }; ClusterPhotos.propTypes = { - markerIds: PropTypes.array, + photoIds: PropTypes.array, onClose: PropTypes.func, }; diff --git a/frontend/src/metadata/views/map/control/geolocation-control.js b/frontend/src/metadata/views/map/control/geolocation-control.js deleted file mode 100644 index d067348c819..00000000000 --- a/frontend/src/metadata/views/map/control/geolocation-control.js +++ /dev/null @@ -1,50 +0,0 @@ -import { mediaUrl } from '../../../../utils/constants'; -import { Utils } from '../../../../utils/utils'; - -export function createBMapGeolocationControl(BMapGL, callback) { - function GeolocationControl() { - this.defaultAnchor = window.BMAP_ANCHOR_BOTTOM_RIGHT; - this.defaultOffset = new BMapGL.Size(30, Utils.isDesktop() ? 30 : 90); - } - GeolocationControl.prototype = new BMapGL.Control(); - GeolocationControl.prototype.initialize = function (map) { - const div = document.createElement('div'); - div.className = 'sf-BMap-geolocation-control'; - div.style = 'display: flex; justify-content: center; align-items: center;'; - - const icon = document.createElement('img'); - icon.className = 'sf-BMap-icon-current-location'; - icon.src = `${mediaUrl}/img/current-location.svg`; - icon.style = 'width: 18px; height: 18px; display: block;'; - div.appendChild(icon); - if (Utils.isDesktop()) { - setNodeStyle(div, 'height: 40px; width: 40px; line-height: 40px'); - } else { - setNodeStyle(div, 'height: 35px; width: 35px; line-height: 35px; opacity: 0.75'); - } - div.onclick = (e) => { - e.preventDefault(); - const geolocation = new BMapGL.Geolocation(); - div.className = 'sf-BMap-geolocation-control sf-BMap-geolocation-control-loading'; - geolocation.getCurrentPosition((result) => { - div.className = 'sf-BMap-geolocation-control'; - if (result) { - const point = result.point; - map.setCenter(point); - callback(null, point); - } else { - // Positioning failed - callback(true); - } - }); - }; - map.getContainer().appendChild(div); - return div; - }; - - return GeolocationControl; -} - -function setNodeStyle(dom, styleText) { - dom.style.cssText += styleText; -} diff --git a/frontend/src/metadata/views/map/control/zoom-control.js b/frontend/src/metadata/views/map/control/zoom-control.js deleted file mode 100644 index de799d28408..00000000000 --- a/frontend/src/metadata/views/map/control/zoom-control.js +++ /dev/null @@ -1,68 +0,0 @@ -import { Utils } from '../../../../utils/utils'; - -const maxZoom = 18; -const minZoom = 3; - -export function createBMapZoomControl(BMapGL, callback) { - function ZoomControl() { - this.defaultAnchor = window.BMAP_ANCHOR_BOTTOM_RIGHT; - this.defaultOffset = new BMapGL.Size(80, Utils.isDesktop() ? 30 : 90); - } - ZoomControl.prototype = new BMapGL.Control(); - ZoomControl.prototype.initialize = function (map) { - const div = document.createElement('div'); - div.className = 'sf-BMap-zoom-control'; - div.style = 'display: flex; justify-content: center; align-items: center;'; - - const zoomInButton = document.createElement('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); - - const divider = document.createElement('div'); - divider.style = 'height: 22px; width: 1px; background-color: #ccc;'; - div.appendChild(divider); - - const zoomOutButton = document.createElement('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); - - if (Utils.isDesktop()) { - setNodeStyle(div, 'height: 40px; width: 111px; line-height: 40px'); - } else { - setNodeStyle(div, 'height: 35px; width: 80px; line-height: 35px; opacity: 0.75'); - } - - const updateButtonStates = () => { - const zoomLevel = map.getZoom(); - zoomInButton.disabled = zoomLevel >= maxZoom; - zoomOutButton.disabled = zoomLevel <= minZoom; - callback && callback(zoomLevel); - }; - - zoomInButton.onclick = (e) => { - e.preventDefault(); - const nextZoom = map.getZoom() + 2; - map.zoomTo(Math.min(nextZoom, maxZoom)); - }; - - zoomOutButton.onclick = (e) => { - e.preventDefault(); - const nextZoom = map.getZoom() - 2; - map.zoomTo(Math.max(nextZoom, minZoom)); - }; - - map.addEventListener('zoomend', updateButtonStates); - map.getContainer().appendChild(div); - return div; - }; - - return ZoomControl; -} - -function setNodeStyle(dom, styleText) { - dom.style.cssText += styleText; -} diff --git a/frontend/src/metadata/views/map/index.css b/frontend/src/metadata/views/map/index.css index cf9cd9ce286..22ff37c9430 100644 --- a/frontend/src/metadata/views/map/index.css +++ b/frontend/src/metadata/views/map/index.css @@ -4,136 +4,3 @@ 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; - 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: -16px; - top: -16px; - width: 32px; - height: 32px; - padding: 6px; - background: #007bff; - color: #fff; - border-radius: 50%; - text-align: center; - font-size: 16px; - line-height: 20px; - 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); -} - -.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; - box-shadow: -2px -2px 4px 2px rgba(0, 0, 0, 0.1); -} - -.sf-metadata-view-map .sf-BMap-geolocation-control-loading { - opacity: 0.7; -} - -.sf-metadata-view-map .sf-BMap-geolocation-control:hover, -.sf-metadata-view-map .sf-BMap-zoom-button:not(.disabled):hover { - background-color: #f5f5f5; - cursor: pointer; -} - -.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; - box-shadow: none; -} - -.sf-metadata-view-map .sf-BMap-zoom-button .zoom-in-icon, -.sf-metadata-view-map .sf-BMap-zoom-button .zoom-out-icon { - width: 18px; - height: 18px; - fill: currentColor; -} - -.sf-metadata-view-map .sf-BMap-zoom-button:not(:disabled):active:focus { - box-shadow: none; -} - -.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 4df75e4c95e..df78a5a9914 100644 --- a/frontend/src/metadata/views/map/index.js +++ b/frontend/src/metadata/views/map/index.js @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { getFileNameFromRecord, getFileTypeFromRecord, getImageLocationFromRecord, getParentDirFromRecord, getRecordIdFromRecord } from '../../utils/cell'; import ClusterPhotos from './cluster-photos'; -import MapView from './map'; +import MapView from './map-view'; import { PREDEFINED_FILE_TYPE_OPTION_KEY } from '../../constants'; import { useMetadataView } from '../../hooks/metadata-view'; import { Utils } from '../../../utils/utils'; @@ -60,7 +60,7 @@ const Map = () => { }, []); if (showCluster) { - return (); + return (); } return (); diff --git a/frontend/src/metadata/views/map/map-view/control/geolocation-control/index.css b/frontend/src/metadata/views/map/map-view/control/geolocation-control/index.css new file mode 100644 index 00000000000..49359bb5bd7 --- /dev/null +++ b/frontend/src/metadata/views/map/map-view/control/geolocation-control/index.css @@ -0,0 +1,4 @@ +.sf-map-control-container.sf-map-geolocation-control { + width: 40px; + line-height: 40px; +} diff --git a/frontend/src/metadata/views/map/map-view/control/geolocation-control/index.js b/frontend/src/metadata/views/map/map-view/control/geolocation-control/index.js new file mode 100644 index 00000000000..62e05f53c3f --- /dev/null +++ b/frontend/src/metadata/views/map/map-view/control/geolocation-control/index.js @@ -0,0 +1,44 @@ +import classnames from 'classnames'; +import { Utils } from '../../../../../../utils/utils'; + +import './index.css'; + +export function createBMapGeolocationControl(BMapGL, callback) { + function GeolocationControl() { + this.defaultAnchor = window.BMAP_ANCHOR_BOTTOM_RIGHT; + this.defaultOffset = new BMapGL.Size(30, Utils.isDesktop() ? 30 : 90); + } + GeolocationControl.prototype = new BMapGL.Control(); + GeolocationControl.prototype.initialize = function (map) { + const div = document.createElement('div'); + let className = classnames('sf-map-control-container sf-map-geolocation-control d-flex align-items-center justify-content-center', { + 'sf-map-geolocation-control-mobile': !Utils.isDesktop() + }); + + const locationButton = document.createElement('div'); + locationButton.className = 'sf-map-control d-flex align-items-center justify-content-center'; + locationButton.innerHTML = ''; + div.appendChild(locationButton); + + div.className = className; + div.onclick = (e) => { + e.preventDefault(); + const geolocation = new BMapGL.Geolocation(); + div.className = classnames(className, 'sf-map-control-loading'); + geolocation.getCurrentPosition((result) => { + div.className = className; + if (result) { + const point = result.point; + callback(point); + } else { + // Positioning failed + callback(); + } + }); + }; + map.getContainer().appendChild(div); + return div; + }; + + return GeolocationControl; +} diff --git a/frontend/src/metadata/views/map/map-view/control/index.css b/frontend/src/metadata/views/map/map-view/control/index.css new file mode 100644 index 00000000000..c9157de9611 --- /dev/null +++ b/frontend/src/metadata/views/map/map-view/control/index.css @@ -0,0 +1,63 @@ +.sf-map-control-container { + height: 40px; + 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; +} + +.sf-map-control-container.sf-map-control-loading { + opacity: 0.7; +} + +.sf-map-control-container.sf-map-control-container-mobile { + height: 35px; + line-height: 35px; + opacity: .75; +} + +.sf-map-control-container .sf-map-control-divider { + height: 100%; + width: 1px; + position: relative; + background-color: inherit; +} + +.sf-map-control-container .sf-map-control-divider::before { + content: ''; + position: absolute; + top: 9px; + height: 22px; + width: 1px; + background-color: #ccc; +} + +.sf-map-control-container .sf-map-control { + height: 40px; + width: 40px; + color: #666; + background-color: inherit; + text-align: center; +} + +.sf-map-control-container.sf-map-control-container-mobile .sf-map-control { + height: 35px; + width: 35px; + opacity: .75; +} + +.sf-map-control-container .sf-map-control .sf-map-control-icon { + font-size: 18px; +} + +.sf-map-control-container .sf-map-control:not(.disabled):hover { + cursor: pointer; + color: #212529; +} + +.sf-map-control-container .sf-map-control.disabled { + color: #ccc; +} diff --git a/frontend/src/metadata/views/map/control/index.js b/frontend/src/metadata/views/map/map-view/control/index.js similarity index 89% rename from frontend/src/metadata/views/map/control/index.js rename to frontend/src/metadata/views/map/map-view/control/index.js index 46577bee520..8cd156d393c 100644 --- a/frontend/src/metadata/views/map/control/index.js +++ b/frontend/src/metadata/views/map/map-view/control/index.js @@ -1,6 +1,8 @@ import { createBMapGeolocationControl } from './geolocation-control'; import { createBMapZoomControl } from './zoom-control'; +import './index.css'; + export { createBMapGeolocationControl, createBMapZoomControl diff --git a/frontend/src/metadata/views/map/map-view/control/zoom-control/index.css b/frontend/src/metadata/views/map/map-view/control/zoom-control/index.css new file mode 100644 index 00000000000..98deb1eabd0 --- /dev/null +++ b/frontend/src/metadata/views/map/map-view/control/zoom-control/index.css @@ -0,0 +1,3 @@ +.sf-map-control-container.sf-map-zoom-control-container .sf-map-control { + width: 55px; +} diff --git a/frontend/src/metadata/views/map/map-view/control/zoom-control/index.js b/frontend/src/metadata/views/map/map-view/control/zoom-control/index.js new file mode 100644 index 00000000000..79057f567e0 --- /dev/null +++ b/frontend/src/metadata/views/map/map-view/control/zoom-control/index.js @@ -0,0 +1,59 @@ +import classnames from 'classnames'; +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); + } + ZoomControl.prototype = new BMapGL.Control(); + ZoomControl.prototype.initialize = function (map) { + const zoomLevel = map.getZoom(); + const div = document.createElement('div'); + div.className = classnames('sf-map-control-container sf-map-zoom-control-container d-flex align-items-center justify-content-center', { + 'sf-map-control-container-mobile': !Utils.isDesktop() + }); + + const buttonClassName = 'sf-map-control d-flex align-items-center justify-content-center'; + const zoomInButton = document.createElement('div'); + zoomInButton.className = classnames(buttonClassName, { 'disabled': zoomLevel >= maxZoom }); + zoomInButton.innerHTML = ''; + div.appendChild(zoomInButton); + + const divider = document.createElement('div'); + divider.className = 'sf-map-control-divider'; + div.appendChild(divider); + + const zoomOutButton = document.createElement('div'); + zoomOutButton.className = classnames(buttonClassName, { 'disabled': zoomLevel <= minZoom }); + zoomOutButton.innerHTML = ''; + div.appendChild(zoomOutButton); + + const updateButtonStates = () => { + const zoomLevel = map.getZoom(); + zoomInButton.className = classnames(buttonClassName, { 'disabled': zoomLevel >= maxZoom }); + zoomOutButton.className = classnames(buttonClassName, { 'disabled': zoomLevel <= minZoom }); + callback && callback(zoomLevel); + }; + + zoomInButton.onclick = (e) => { + e.preventDefault(); + const nextZoom = map.getZoom() + 1; + map.zoomTo(Math.min(nextZoom, maxZoom)); + }; + + zoomOutButton.onclick = (e) => { + e.preventDefault(); + const nextZoom = map.getZoom() - 1; + map.zoomTo(Math.max(nextZoom, minZoom)); + }; + + map.addEventListener('zoomend', updateButtonStates); + map.getContainer().appendChild(div); + return div; + }; + + return ZoomControl; +} diff --git a/frontend/src/metadata/views/map/map-view/index.css b/frontend/src/metadata/views/map/map-view/index.css new file mode 100644 index 00000000000..58f97737709 --- /dev/null +++ b/frontend/src/metadata/views/map/map-view/index.css @@ -0,0 +1,82 @@ +.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.js b/frontend/src/metadata/views/map/map-view/index.js similarity index 88% rename from frontend/src/metadata/views/map/map.js rename to frontend/src/metadata/views/map/map-view/index.js index 4dbef7eadd9..9b07c82a032 100644 --- a/frontend/src/metadata/views/map/map.js +++ b/frontend/src/metadata/views/map/map-view/index.js @@ -1,19 +1,23 @@ import React, { useCallback, useEffect, useMemo, useRef } 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 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 './index.css'; + const DEFAULT_POSITION = { lng: 104.195, lat: 35.861 }; const DEFAULT_ZOOM = 4; const BATCH_SIZE = 500; +const MAX_ZOOM = 21; +const MIN_ZOOM = 3; -const Map = ({ images, onOpenCluster }) => { +const MapView = ({ images, onOpenCluster }) => { const mapInfo = useMemo(() => initMapInfo({ baiduMapKey, googleMapKey }), []); const mapRef = useRef(null); @@ -29,16 +33,13 @@ const Map = ({ images, onOpenCluster }) => { }, []); const addMapController = useCallback(() => { - const ZoomControl = createBMapZoomControl(window.BMapGL, saveMapState); + const ZoomControl = createBMapZoomControl(window.BMapGL, { maxZoom: MAX_ZOOM, minZoom: MIN_ZOOM }, saveMapState); const zoomControl = new ZoomControl(); - const GeolocationControl = createBMapGeolocationControl(window.BMapGL, (err, point) => { - if (!err && point) { - mapRef.current.setCenter(point); - } + 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]); @@ -124,8 +125,8 @@ const Map = ({ images, onOpenCluster }) => { const mapTypeValue = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_TYPE_KEY); mapRef.current = new window.BMapGL.Map('sf-metadata-map-container', { enableMapClick: false, - minZoom: 3, - maxZoom: 21, + minZoom: MIN_ZOOM, + maxZoom: MAX_ZOOM, mapType: getBMapType(mapTypeValue), }); @@ -175,9 +176,9 @@ const Map = ({ images, onOpenCluster }) => { ); }; -Map.propTypes = { +MapView.propTypes = { images: PropTypes.array, onOpenCluster: PropTypes.func, }; -export default Map; +export default MapView; diff --git a/frontend/src/metadata/views/map/overlay/custom-avatar-overlay.js b/frontend/src/metadata/views/map/map-view/overlay/custom-avatar-overlay.js similarity index 100% rename from frontend/src/metadata/views/map/overlay/custom-avatar-overlay.js rename to frontend/src/metadata/views/map/map-view/overlay/custom-avatar-overlay.js diff --git a/frontend/src/metadata/views/map/overlay/custom-image-overlay.js b/frontend/src/metadata/views/map/map-view/overlay/custom-image-overlay.js similarity index 97% rename from frontend/src/metadata/views/map/overlay/custom-image-overlay.js rename to frontend/src/metadata/views/map/map-view/overlay/custom-image-overlay.js index 583d6def107..871e0046179 100644 --- a/frontend/src/metadata/views/map/overlay/custom-image-overlay.js +++ b/frontend/src/metadata/views/map/map-view/overlay/custom-image-overlay.js @@ -1,4 +1,4 @@ -import { Utils } from '../../../../utils/utils'; +import { Utils } from '../../../../../utils/utils'; const customImageOverlay = (center, image, callback) => { class ImageOverlay extends window.BMapLib.TextIconOverlay { diff --git a/frontend/src/metadata/views/map/overlay/index.js b/frontend/src/metadata/views/map/map-view/overlay/index.js similarity index 100% rename from frontend/src/metadata/views/map/overlay/index.js rename to frontend/src/metadata/views/map/map-view/overlay/index.js diff --git a/frontend/src/utils/map-utils.js b/frontend/src/utils/map-utils.js index fa2c07f3952..dfa09822257 100644 --- a/frontend/src/utils/map-utils.js +++ b/frontend/src/utils/map-utils.js @@ -1,6 +1,8 @@ import { MAP_TYPE } from '../constants'; import { mediaUrl } from './constants'; +const STATIC_RESOURCE_VERSION = 0.1; + export const initMapInfo = ({ baiduMapKey, googleMapKey, mineMapKey }) => { if (baiduMapKey) return { type: MAP_TYPE.B_MAP, key: baiduMapKey }; if (googleMapKey) return { type: MAP_TYPE.G_MAP, key: googleMapKey }; @@ -30,8 +32,8 @@ export const loadMapSource = (type, key, callback) => { export default function loadBMap(ak) { return new Promise((resolve, reject) => { asyncLoadBaiduJs(ak) - .then(() => asyncLoadJs(`${mediaUrl}/js/map/text-icon-overlay.js`)) - .then(() => asyncLoadJs(`${mediaUrl}/js/map/marker-clusterer.js`)) + .then(() => asyncLoadJs(`${mediaUrl}/js/map/text-icon-overlay.js?v=${STATIC_RESOURCE_VERSION}`)) + .then(() => asyncLoadJs(`${mediaUrl}/js/map/marker-cluster.js?v=${STATIC_RESOURCE_VERSION}`)) .then(() => resolve(true)) .catch((err) => reject(err)); }); diff --git a/media/css/sf_font3/iconfont.css b/media/css/sf_font3/iconfont.css index def6c7fe635..b6650f591a4 100644 --- a/media/css/sf_font3/iconfont.css +++ b/media/css/sf_font3/iconfont.css @@ -1,11 +1,11 @@ @font-face { font-family: "sf3-font"; /* Project id 1230969 */ - src: url('./iconfont.eot?t=1733301127109'); /* IE9 */ - src: url('./iconfont.eot?t=1733301127109#iefix') format('embedded-opentype'), /* IE6-IE8 */ - url('./iconfont.woff2?t=1733301127109') format('woff2'), - url('./iconfont.woff?t=1733301127109') format('woff'), - url('./iconfont.ttf?t=1733301127109') format('truetype'), - url('./iconfont.svg?t=1733301127109#sf3-font') format('svg'); + src: url('./iconfont.eot?t=1736476800596'); /* IE9 */ + src: url('./iconfont.eot?t=1736476800596#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('./iconfont.woff2?t=1736476800596') format('woff2'), + url('./iconfont.woff?t=1736476800596') format('woff'), + url('./iconfont.ttf?t=1736476800596') format('truetype'), + url('./iconfont.svg?t=1736476800596#sf3-font') format('svg'); } .sf3-font { @@ -16,6 +16,30 @@ -moz-osx-font-smoothing: grayscale; } +.sf3-font-zoom-out:before { + content: "\e630"; +} + +.sf3-font-current-location:before { + content: "\e62e"; +} + +.sf3-font-zoom-in:before { + content: "\e62f"; +} + +.sf3-font-ai:before { + content: "\e854"; +} + +.sf3-font-time:before { + content: "\e852"; +} + +.sf3-font-description:before { + content: "\e853"; +} + .sf3-font-hi:before { content: "\e603"; } @@ -24,10 +48,6 @@ content: "\e851"; } -.sf3-font-current-location:before { - content: "\e850"; -} - .sf3-font-ai_generated:before { content: "\e84f"; } diff --git a/media/css/sf_font3/iconfont.eot b/media/css/sf_font3/iconfont.eot index 4bb9fed7ba7e370a5b1157415b64ee9ccc8538c0..bfefade4d6916ac19d5c155bf0ffe19c9ed55d4a 100644 GIT binary patch delta 3785 zcmZ9Pdu$xl8O6`t*@s`5U3*`)leO1t@2@DJtPy%fuv+lWjfrifL zH#2+Z-kE#9?|f(MGyf0|d@P&~90ugK9&{${A5QnSJlJ=Cx(*O00E~=HkIvrde|Q~W zNT4vWXYZZ6FYVd;OThF2qi;`6jE*Pv|0>76?~>#s8(e=kmUw-J)J;y$A6{Cy{2rh8 z5n0+hGdB9WSI$R(6&x=xJ$iUnJd8i^x}5#P`$nfHoChnP-~(njz_w?0=D_?-Pu=w= zU~VhmcqlV2YNKh6RHM<-JN@CXd-bO%wzC2F;K`n^&Y7o0g-B;!6VHpSnZve-ZFxD< zXRpfi+gnfn)%IJ_TA6Ejb3mxc03tG{PCe)(=2{a;vDwLv{v077u2x?J>D9YeL3~^LqC03yv z6$ruyKfEyE#tM|6n4vC20UU5L$Pi9Oa4FN~aHUIB2Av*t4SAp!$iV*M#GfCdRD{a!g@}F^dVmSM#GH5dPT$B!}^PcDuDH- zhI&BOt)ViI9hWT%lo=L7E!Y8zj@DVC6x2G^|0$QVpvSGF`?4 zVPQggHR$8nr(v-|`ZX+DNak9>0)`A~RCr~%hD8lop<#JLR%%%2kSjGTdB|0oG^-yn zq@fQ$R%z%6kkuM`17ui3mw>F%&@Uh(8afALt%e=~S*M|!Kt?t6708%|4g(oy{&@h+ z2C`m5+ktG*(10Kl8d?!#qlTsg*`%R8L4E;^3R3kWv@A%~kI=jz+cdN>$aW1HnSZHX zNa$^l>ViEs&?+J68w#2x z$gLXnoyczid=D>*Iq{awX&bSdNZ3 z!uQm~YVM8%B9pb7>xSwsMq8ru(SOJ4Vu$1X@rUBM`s4MlH+ULOHoTE2N(?VH-re|C zQ=;i`bA9tQ&6}H#G{4wV)N-Qbv(_tGFSLCG{v88`LcGjaFwCW$H68wTUu{jKyS=N& zIN8&)G}j|u?%}4oVR^w0V}aRMA9sd2^ITbF=At|97M<<+f$eIb4VlHQf%Ffn5>*3_ z?kb-2S5^5Zi+4Rb@I}LrY8YZe?&Ae+#zKAkk0I@VQLkayE*H%bDLz zQ5%WHW2t0{$L?f0wN|8(!TepnFBqt1XO0La*&*54kw0~J#o5;tF$E_VxKhtE#OF9e zLjCr$Uw3I&iwM?6*yBpk!C&!?WLGTW^aX6j>vT9Clk@D3)riS(jOTbzQtCH8lE0(+y1y$Bt-Q zbg;--TrjxPa3=b5yDhEYvB{0X4P%%!IaCk+s z!zElP;dKR5M)*EYak1y~U*taV*apkj{W>?|JS0xVH_vW165n+6G!_hzSvypTyvh_Y zo^7UT5-|}gZM0qZ@@~A;F!T4#Rr;no!G8INl;ZRYLx1$}zp*1T7jG0t#W8VQq{RXX zj?BzVH_sfL_lzB!o154-->m*YoS&N6=gQxi+GiV`vd>RVPZW<&92lFMnpO8~lT)s_ RiQRJ(2PRM7Kih5>_%HjPVdww= delta 2727 zcmX|@du&^08OEPuJ9ZLZ8#~U`iEAfz;v~J3u3ZV~D5Z2;leAmXbzSBxm!^;#X}VzSZ$u`h zhlgJH_Fy#-xXJGyOb*S@SXc3De#bc^r-mj+y_ai$^j~1&1mL`ur0fhpUX>fuC1-& z>)yQui%UE4(8t>m=++IhJ@WtUBu?TKdU2W?Fpn2`(RQ?92VTJQIF5rn$7Rgn5@s=j zFJl^0n8XCWgfHSE#xaU9T)+s1@hqN39z%EzT{w?t3K-X_Kx!QK8s`c91h`AIDo_0&xhHAz1WQo>_RJY zXh9ZfG@%(8RHFs~)S?dcNT30YtkLcu{Md;UDiKE&qKKgaVMGu@84FqpAG|1GDcx|f z++Nmfqu5(gRtOl4N@%F<*H=tw6;=l}*BV$S*wn;eWU?C-)(&=3VHIJg6xI{=8HJUF zJ)p40u+J&1HteShX2+8rZ=lX_!U`3K6IZA`oTNe*!1=vGKfqa4=nOc2R_GBp?<#Z) zoHd2Mf%6xI4uZorESMdg1Lp&Uwu18y1$mAiDzqA$2MSFGhkb3Jhv582p%LM1D0qe= zTh~Bu!r4@|uf+~>2>n`sa5T`m5Nd0ni6PY8Ks!UIyMe}ru)PhmIHW|O*&*z918ooS zDW=P$RADPX$`m#Qgl#V6)SvwU@hj{UNI+rFK!OUp2NF`)M-a+nU`IhB3VRDe;~Cgx zkP3zU28k-{JV;Do4?+r++!3}TgnAp;n2;*Plvk=1HY=n?K{v-*g$)d;Q`pLodWB65 zNhoY@NQ1&ghcqe$_B0m&##4oFsEhCo^r zrU|4~VXi=OM*ke2*i5p24GbGdyTaIkn1cud2x1N*j3S6RF<~e{%*6@g31Tiz7*r5* zgM^Ui9dJ<+T#OO(wt`MUqVa`H~o`i`D zIj%5!A;z49DGV{@B+O%oF(+X%qaene#Qf@saW-LGLyY4IgB$X=!U%_)QW)lt9)+iIf{Us(sNE3R7CcU^C}kGL1yE1n~sCC~dMVEL7*^Tl*`AxseKOTq&-Uzya-NEa@jnJ{sS3`dYSBJ;KH^M8Cvypo{Qaj$P z*i|u6u@ptLB|0Cy75#UtBDOEKR2iy#sB*mWhw)JSO8nKT@~VZZcdGN%Qj@Bgs6AZ! z+q#CjnYuf5>;3g7>%UfSCl(qWX;^PuYW#ch>&ca!w^Dc0=hLsG-)YJ>z1+0g{PX6G z%vi?G?#a$)AGCC~%(k|)zMt#LE#+3*uC~3}cB}2(_WJhO_K!LSJFf3KyZeddyA_L8 z6HBpm-M#7_qDtK+TC9Cl@v3;l=keGOJDWTkdl2f8d5zA-)7 zee`Je3)gk-&4Hbx4(Gk zbbImI>2?dt-#qhV$UW>@Ee;;aoH`Q@Z?0RBpex`GWkZ|mo9m%$B5XxA*8^6$JCqAq z5i1yIb-k_Mei+Dv+@X*a+*}VQvK*~QD8oaofH_NGx&OJ}MEK{d;nGJ5xW+H~W=F?n dM=y;>hA+>~j!w;WnE#1$7pJF|2j}*B{s*o9b};|| diff --git a/media/css/sf_font3/iconfont.svg b/media/css/sf_font3/iconfont.svg index d40bdac5381..e3a48dd6bf4 100644 --- a/media/css/sf_font3/iconfont.svg +++ b/media/css/sf_font3/iconfont.svg @@ -14,12 +14,22 @@ /> + + + + + + + + + + + + - - @@ -188,7 +198,7 @@ - + @@ -224,7 +234,7 @@ - + diff --git a/media/css/sf_font3/iconfont.ttf b/media/css/sf_font3/iconfont.ttf index 0a1dae1a5d6a7ba21ac1b533cd4fb8f47919ded5..6e2143aaa5473b9cce8c82dc2c7a0744a2d3e30a 100644 GIT binary patch delta 3779 zcmZXXdu$xl8O6`t*@vIAYwyc;yzBMa-nG}S__d2oNCG7^51Ka#3ASU$c4CwG5yzn+ zC0Rpx^p7Y477(H>O{t&~YJt|2fLo`{TJV-Wtm3{lzh4i%uZP+1fxdY$9 zCO+qCoWSikj&I`_Zo?doVird*gK11*61UQIjcl*5A<;;6z3RHFiw2*L+HyfEQL zDN0bxP!}>$4me?7IPWMCX{YD2q|K`UYZx0?Os0ZSf@NqJFIY7iMh;d)!x+MfY8Xve zZ5kyAt6jqg!&2jjO1!lrPZ|t6tlb(W0M=d&QvvH14U+<^U&Hjk8rCpTux2$(8LVR( zCJ)v#8m1A}vl=E8)^i%B7S@Y=zx5(#G{^JH8s;0;pEb-ptT!~&0J3fk)qpHls-P~A zWh4|-2(oW!s1;=2(NHzWUeZuM$iAyV`Yt29+DoRp!^{n8WsYiQ^S&gq>dCUjx=Nee-PFQWTA!?14&mWVwR*XE8#0HCW5DPs0L*^lMnE zkj%A$MGG0!sPM`P4GS5vQp1vltkST!Ay;Tv=8)AI7CdA+!~txnmVLXHle*kZq}%8s@$TX%flyXs%qYh++7>1y(=1sj@NCiAFRLJ z(AF^5@b6fC?09@*{K0sx@l@mMO`fJRP3IFuiN)r-n%_*fBwCKQHnv{Zy0!I0>+@|z zZKvBlX}_xdgO%@tf46|45HB$p40BmmZKuD(S63VD>FDk?&h+*!&Gw3ydbz1#Sf01T zm}mCY2c4l7ox!jO27E44xSY{=n~2A{Q>#TP8T9*{(bzS@w7I%6yWQ1p(bbWEa))}d z4H>aBkobN$5$=C%fAP3K9QKbF?|-cSvxY&{Fvy17hx6Qw`OI@W0_j^vRP)G}#(ljZ z_putzr5K#*lg8V|OnzAuwD-#k6J_1W&VawnXNqW>@FtDHWOC_=qJvk`;%Rr`BP7KoI_nvQ*d&DtMxoXe2gXRo#mYG_bcjbuzKOIk#vy#SIk=4HX+)h1V1o z{m@|-Yn#_6zU$4s=Jkp`Z$!QDP%MyL~}#xTvLRei8TK4STxGN!>mr;29k|MjSK z>hd|69?DYXpCM&LI|IgiPuK1}7xwJ#>iOa#vu!k(UJQ8`n^Nl4-ut~FF+cv5A!_Wn zUgVbok-vPv8%i~Oz%Y3idC+QHG+s5<(!D&&76q%u=@JgFNOrn}DnSew zeEQSeM;_Zi#k%;fb3@MC#M$`PnXN|RK6gvP{PZVYQDhtPFXaYqD->sii(OcFy!1Y? zQ0O_ky76)2U4|yCzH6*ZhQsfp$XK7wXuD8yRPyRhB{MbU6$6WT4O9f1&$&Z!^IXr} z8}kh+{YQ3h+>mH#No?5o%;y)zP`+dR_17lPnemYOTuYm3cvtCY=qn@hw=AsQa+7Jy z8zNKH6W*2V3>I>!#*As^E{d2bN({5rox7O3=x(hxMT}>gshUJg#LAj&AAH^$FEh-1 zzgew2-3gA%Kcox`MT5Whh;-&pQC yRh*lco^s`TCZ=q|6ZW}@$+6~fLXrkI z?XrOIv>(ZzO={z5zmTZ-sH#oc29Z!TiiBVSgr+KD`+?8|p@rjzX#&Hx0tF@9Z=S(9 zy3T9Ae$Ur)-_M=MU#*wLx|Awz1*{hVJnhT2l(GRGxglW@dNg^fY24- z)V0a+{ONS$om#G4BZHHisQ#1v4}QKuIwz;+=B0V#Hm~;~k*RZI`Db4`)dU2u@%KB^ z`T1Gv5`MSXZ~hLLc@wA_yL=2Ci2?LuJ9?2u54w>;C$^yr zO=w0CEoem>;%G+)Ycvo-09%nnJsQx6C}OBX7!f$AVL|<?oD zkeI>_L{aMbg|HbR)Z4(egfuFqywaqwRUyp^MmV-8Y+p#L!bXO)DQsy-Tw!xV+7-4r zq+=7-|2}?5DC~X6R)r}5Nh-_(NJ?QcKsps>1Z11Sw19Le%neAl!UTczD9jQ_ufkM; zq>cV*zOk8P{~8!Ikc`66ftZsB;|F3+A`BvkxiDcQLCnnw!wF(;P8d@V^MHhb1u+j! z7+nzaas)-AwRvm8JcA4>Og6|p3NsF3Bq2;Yh^ZK1?m89Lk=qpb;uFL{Na?W zVuGIA3H;RBV=Z`EJTH5GZSS=g?N#qy@3QxUs)=INuY6wLE54i64b{2oxBPy8&c9mo zaLv`)VC}VlComao2)-TighoPFLU)}9ov%B;4L5}+!dJtqk>in@TasJesoP#RRks{P zv?n?ry&nB%tS+`Iwp{Pj-&;Rf|J??sVWHvG#@fck#`l|YP12lfo@yCt`DJT+>ul?d z*3nAa!M1O-x$(vJ``arW%N>77ypUMkdOi8`)XCHlG^!@&s+C+gY!ySDfy6*ov%If>y2Vq#Y|_g@V1FclFngf?ba7I96!A5{`Fsv?5Ly zPql*PD#4Z4^S_8#3#E&NkK^!6o~$W9ey8`p Dv-@h~ diff --git a/media/css/sf_font3/iconfont.woff b/media/css/sf_font3/iconfont.woff index 77a4a0b7a7fc85ca681a18600395cd7729161962..0d71528c2b12f3767a872d00e6c9f9a6612af2b9 100644 GIT binary patch delta 17818 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 delta 17169 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%0AD*~yLJVoD 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 diff --git a/media/css/sf_font3/iconfont.woff2 b/media/css/sf_font3/iconfont.woff2 index 48dbc69dbe0b1b331fba4fe32aa88fcbd53f65d6..f576b8fab31e3c427d8442fd3f522fee0b3b9998 100644 GIT binary patch literal 15388 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 aQhJ)WG^8oM9+9(DWkWb4F60jY0001eTjN0h diff --git a/media/js/map/marker-clusterer.js b/media/js/map/marker-cluster.js similarity index 95% rename from media/js/map/marker-clusterer.js rename to media/js/map/marker-cluster.js index e29c8710248..42fc3695092 100644 --- a/media/js/map/marker-clusterer.js +++ b/media/js/map/marker-cluster.js @@ -111,7 +111,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * girdSize {Number} 聚合计算时网格的像素大小,默认60
* maxZoom {Number} 最大的聚合级别,大于该级别就不进行相应的聚合
* minClusterSize {Number} 最小的聚合数量,小于该数量的不能成为一个聚合,默认为2
- * isAverangeCenter {Boolean} 聚合点的落脚位置是否是所有聚合在内点的平均值,默认为否,落脚在聚合内的第一个点
+ * isAvgCenter {Boolean} 聚合点的落脚位置是否是所有聚合在内点的平均值,默认为否,落脚在聚合内的第一个点
* styles {Array} 自定义聚合后的图标风格,请参考TextIconOverlay类
*/ BMapLib.MarkerCluster = function(map, options){ @@ -142,8 +142,8 @@ var BMapLib = window.BMapLib = BMapLib || {}; // that._redraw(); // }); - var mkrs = opts["markers"]; - isArray(mkrs) && this.addMarkers(mkrs); + var markers = opts["markers"]; + isArray(markers) && this.addMarkers(markers); }; /** @@ -454,19 +454,19 @@ var BMapLib = window.BMapLib = BMapLib || {}; * Cluster * @class 表示一个聚合对象,该聚合,包含有N个标记,这N个标记组成的范围,并有予以显示在Map上的TextIconOverlay等。 * @constructor - * @param {MarkerCluster} markerClusterer 一个标记聚合器示例。 + * @param {MarkerCluster} markerCluster 一个标记聚合器示例。 */ - function Cluster(markerClusterer){ - this._markerClusterer = markerClusterer; - this._map = markerClusterer.getMap(); - this._minClusterSize = markerClusterer.getMinClusterSize(); - this._isAverageCenter = markerClusterer.isAverageCenter(); + function Cluster(markerCluster){ + this._markerCluster = markerCluster; + this._map = markerCluster.getMap(); + this._minClusterSize = markerCluster.getMinClusterSize(); + this._isAverageCenter = markerCluster.isAverageCenter(); this._center = null;//落脚位置 this._markers = [];//这个Cluster中所包含的markers this._gridBounds = null;//以中心点为准,向四边扩大gridSize个像素的范围,也即网格范围 this._isReal = false; //真的是个聚合 - this._clusterMarker = new BMapLib.TextIconOverlay(this._center, this._markers.length, {"styles":this._markerClusterer.getStyles()}); + this._clusterMarker = new BMapLib.TextIconOverlay(this._center, this._markers.length, {"styles":this._markerCluster.getStyles()}); //this._map.addOverlay(this._clusterMarker); } @@ -552,7 +552,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; */ Cluster.prototype.updateGridBounds = function() { var bounds = new BMapGL.Bounds(this._center, this._center); - this._gridBounds = getExtendedBounds(this._map, bounds, this._markerClusterer.getGridSize()); + this._gridBounds = getExtendedBounds(this._map, bounds, this._markerCluster.getGridSize()); }; /** @@ -560,7 +560,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * @return 无返回值。 */ Cluster.prototype.updateClusterMarker = function () { - if (this._map.getZoom() > this._markerClusterer.getMaxZoom()) { + if (this._map.getZoom() > this._markerCluster.getMaxZoom()) { this._clusterMarker && this._map.removeOverlay(this._clusterMarker); for (var i = 0, marker; marker = this._markers[i]; i++) { this._map.addOverlay(marker); @@ -589,9 +589,9 @@ var BMapLib = window.BMapLib = BMapLib || {}; return; } clickTimeout = setTimeout(() => { - if (this._markerClusterer && typeof this._markerClusterer.getCallback() === 'function') { + if (this._markerCluster && typeof this._markerCluster.getCallback() === 'function') { const markers = this._markers; - this._markerClusterer.getCallback()(event, markers); + this._markerCluster.getCallback()(event, markers); } clickTimeout = null; }, 300); // Delay to differentiate between single and double click diff --git a/media/js/map/text-icon-overlay.js b/media/js/map/text-icon-overlay.js index 963f8116175..c713ea3670d 100644 --- a/media/js/map/text-icon-overlay.js +++ b/media/js/map/text-icon-overlay.js @@ -211,7 +211,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * @returns {string} 驼峰化处理后的字符串 */ baidu.string.toCamelCase = function (source) { - //提前判断,提高getStyle等的效率 thanks xianwei + //提前判断,提高getStyle等的效率 if (source.indexOf('-') < 0 && source.indexOf('_') < 0) { return source; } @@ -649,7 +649,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * @grammar obj.dispatchEvent(event, options) * @param {baidu.lang.Event|String} event Event对象,或事件名称(1.1.1起支持) * @param {Object} options 扩展参数,所含属性键值会扩展到Event对象上(1.2起支持) - * @remark 处理会调用通过addEventListenr绑定的自定义事件回调函数之外,还会调用直接绑定到对象上面的自定义事件。例如:
+ * @remark 处理会调用通过addEventListener绑定的自定义事件回调函数之外,还会调用直接绑定到对象上面的自定义事件。例如:
myobj.onMyEvent = function(){}
myobj.addEventListener("onMyEvent", function(){}); */ @@ -767,7 +767,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; }; /** - *继承Overlay的intialize方法,自定义覆盖物时必须。 + *继承Overlay的initialize方法,自定义覆盖物时必须。 *@param {Map} map BMapGL.Map的实例化对象。 *@return {HTMLElement} 返回覆盖物对应的HTML元素。 */ @@ -860,21 +860,20 @@ var BMapLib = window.BMapLib = BMapLib || {}; var style = this.getStyleByText(this._text, this._styles); var newStyle = { url: imageUrl, - size: {width: 72, height: 72} + size: { width: 86, height: 86 } } if (imageUrl) { - style = Object.assign(style, {url: imageUrl, size: {width: 72, height: 72}}) + style = Object.assign(style, { url: imageUrl, size: { width: 86, height: 86 } }) } - const customImageNumber = `${this._text}`; + const customImageNumber = `${this._text < 1000 ? this._text : '1k+'}`; this._domElement.style.cssText = this.buildImageCssText(newStyle); - const imageElement = `` + const imageElement = `` const htmlString = ` -
- ${this._text > 1 ? customImageNumber : ''} - ${imageUrl ? imageElement : '
'} - -
+
+ ${this._text > 1 ? customImageNumber : ''} + ${imageUrl ? imageElement : '
'} +
` const labelDocument = new DOMParser().parseFromString(htmlString, 'text/html'); const label = labelDocument.body.firstElementChild; @@ -888,14 +887,14 @@ var BMapLib = window.BMapLib = BMapLib || {}; var textColor = style['textColor'] || 'black'; var textSize = style['textSize'] || 10; - var csstext = []; + var cssText = []; - csstext.push('height:' + size.height + 'px; line-height:' + size.height + 'px;'); - csstext.push('width:' + size.width + 'px; text-align:center;'); + cssText.push('height:' + size.height + 'px; line-height:' + size.height + 'px;'); + cssText.push('width:' + size.width + 'px; text-align:center;'); - csstext.push('cursor:pointer; color:' + textColor + '; position:absolute; font-size:' + + cssText.push('cursor:pointer; color:' + textColor + '; position:absolute; font-size:' + textSize + 'px; font-family:Arial,sans-serif; font-weight:bold'); - return csstext.join(''); + return cssText.join(''); }; /** @@ -937,34 +936,34 @@ var BMapLib = window.BMapLib = BMapLib || {}; var textColor = style['textColor'] || 'black'; var textSize = style['textSize'] || 10; - var csstext = []; + var cssText = []; if (T.browser["ie"] < 7) { - csstext.push('filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(' + + cssText.push('filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(' + 'sizingMethod=scale,src="' + url + '");'); } else { - csstext.push('background-image:url(' + url + ');'); + cssText.push('background-image:url(' + url + ');'); var backgroundPosition = '0 0'; (offset instanceof BMapGL.Size) && (backgroundPosition = offset.width + 'px' + ' ' + offset.height + 'px'); - csstext.push('background-position:' + backgroundPosition + ';'); + cssText.push('background-position:' + backgroundPosition + ';'); } if (size instanceof BMapGL.Size){ if (anchor instanceof BMapGL.Size) { if (anchor.height > 0 && anchor.height < size.height) { - csstext.push('height:' + (size.height - anchor.height) + 'px; padding-top:' + anchor.height + 'px;'); + cssText.push('height:' + (size.height - anchor.height) + 'px; padding-top:' + anchor.height + 'px;'); } if(anchor.width > 0 && anchor.width < size.width){ - csstext.push('width:' + (size.width - anchor.width) + 'px; padding-left:' + anchor.width + 'px;'); + cssText.push('width:' + (size.width - anchor.width) + 'px; padding-left:' + anchor.width + 'px;'); } } else { - csstext.push('height:' + size.height + 'px; line-height:' + size.height + 'px;'); - csstext.push('width:' + size.width + 'px; text-align:center;'); + cssText.push('height:' + size.height + 'px; line-height:' + size.height + 'px;'); + cssText.push('width:' + size.width + 'px; text-align:center;'); } } - csstext.push('cursor:pointer; color:' + textColor + '; position:absolute; font-size:' + + cssText.push('cursor:pointer; color:' + textColor + '; position:absolute; font-size:' + textSize + 'px; font-family:Arial,sans-serif; font-weight:bold'); - return csstext.join(''); + return cssText.join(''); };