1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-07-15 16:04:01 +00:00

Optimize/map cluster (#7426)

* optimize cluster position, clean up map view toolbar

* fix cluster position, optimize controller ui

---------

Co-authored-by: zhouwenxuan <aries@Mac.local>
This commit is contained in:
Aries 2025-01-26 15:54:20 +08:00 committed by GitHub
parent f8112c0306
commit 94e19c58c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 325 additions and 554 deletions

View File

@ -114,11 +114,9 @@ const ViewToolBar = ({ viewId, isCustomPermission, onToggleDetail, onCloseDetail
{viewType === VIEW_TYPE.MAP && ( {viewType === VIEW_TYPE.MAP && (
<MapViewToolBar <MapViewToolBar
readOnly={readOnly} readOnly={readOnly}
isCustomPermission={isCustomPermission}
view={view} view={view}
collaborators={collaborators} collaborators={collaborators}
modifyFilters={modifyFilters} modifyFilters={modifyFilters}
onToggleDetail={onToggleDetail}
/> />
)} )}
</div> </div>

View File

@ -1,20 +1,14 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useMemo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY, VIEW_TYPE } from '../../../constants'; import { PRIVATE_COLUMN_KEY, VIEW_TYPE } from '../../../constants';
import { FilterSetter, GalleryGroupBySetter, MapTypeSetter, SortSetter } from '../../data-process-setter'; import { FilterSetter, MapTypeSetter } from '../../data-process-setter';
import { gettext } from '../../../../utils/constants';
const MapViewToolBar = ({ const MapViewToolBar = ({
isCustomPermission,
readOnly, readOnly,
view: oldView, view: oldView,
collaborators, collaborators,
modifyFilters, modifyFilters,
onToggleDetail,
}) => { }) => {
const [showGalleryToolbar, setShowGalleryToolbar] = useState(false);
const [view, setView] = useState(oldView);
const viewType = useMemo(() => VIEW_TYPE.MAP, []); const viewType = useMemo(() => VIEW_TYPE.MAP, []);
const viewColumns = useMemo(() => { const viewColumns = useMemo(() => {
if (!oldView) return []; if (!oldView) return [];
@ -26,54 +20,6 @@ const MapViewToolBar = ({
return viewColumns && viewColumns.filter(c => c.key !== PRIVATE_COLUMN_KEY.FILE_TYPE); return viewColumns && viewColumns.filter(c => c.key !== PRIVATE_COLUMN_KEY.FILE_TYPE);
}, [viewColumns]); }, [viewColumns]);
const onToggle = useCallback((value) => {
setShowGalleryToolbar(value);
}, []);
const modifySorts = useCallback((sorts) => {
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.UPDATE_SERVER_VIEW, { sorts });
}, []);
const resetView = useCallback(view => {
setView(view);
}, []);
useEffect(() => {
setShowGalleryToolbar(false);
const unsubscribeToggle = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, onToggle);
const unsubscribeView = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.RESET_VIEW, resetView);
return () => {
unsubscribeToggle && unsubscribeToggle();
unsubscribeView && unsubscribeView();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [viewID]);
if (showGalleryToolbar) {
return (
<>
<div className="sf-metadata-tool-left-operations">
<GalleryGroupBySetter viewID={viewID} />
<SortSetter
wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-sort"
target="sf-metadata-sort-popover"
readOnly={readOnly}
sorts={view.sorts}
type={VIEW_TYPE.MAP}
columns={viewColumns}
modifySorts={modifySorts}
/>
{!isCustomPermission && (
<div className="cur-view-path-btn ml-2" onClick={onToggleDetail}>
<span className="sf3-font sf3-font-info" aria-label={gettext('Properties')} title={gettext('Properties')}></span>
</div>
)}
</div>
<div className="sf-metadata-tool-right-operations"></div>
</>
);
}
return ( return (
<> <>
<div className="sf-metadata-tool-left-operations"> <div className="sf-metadata-tool-left-operations">
@ -99,11 +45,10 @@ const MapViewToolBar = ({
}; };
MapViewToolBar.propTypes = { MapViewToolBar.propTypes = {
isCustomPermission: PropTypes.bool,
readOnly: PropTypes.bool, readOnly: PropTypes.bool,
view: PropTypes.object,
collaborators: PropTypes.array, collaborators: PropTypes.array,
modifyFilters: PropTypes.func, modifyFilters: PropTypes.func,
onToggleDetail: PropTypes.func,
}; };
export default MapViewToolBar; export default MapViewToolBar;

View File

@ -1,4 +0,0 @@
.sf-metadata-map-photos-container {
padding: 0 !important;
overflow: hidden !important;
}

View File

@ -1,159 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import deepCopy from 'deep-copy';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import { CenteredLoading } from '@seafile/sf-metadata-ui-component';
import Gallery from '../../gallery/main';
import { EVENT_BUS_TYPE, UTC_FORMAT_DEFAULT } from '../../../constants';
import metadataAPI from '../../../api';
import { Utils } from '../../../../utils/utils';
import toaster from '../../../../components/toast';
import { useMetadataView } from '../../../hooks/metadata-view';
import { getRowsByIds } from '../../../utils/table';
import Metadata from '../../../model/metadata';
import { sortTableRows } from '../../../utils/sort';
import { useCollaborators } from '../../../hooks/collaborators';
import { getRecordIdFromRecord, getParentDirFromRecord, getFileNameFromRecord } from '../../../utils/cell';
import './index.css';
dayjs.extend(utc);
const ClusterPhotos = ({ photoIds, onClose }) => {
const { repoID, viewID, metadata: allMetadata, store, addFolder, deleteRecords } = useMetadataView();
const { collaborators } = useCollaborators();
const [isLoading, setLoading] = useState(true);
const [metadata, setMetadata] = useState({ rows: getRowsByIds(allMetadata, photoIds), columns: allMetadata?.columns || [] });
const loadData = useCallback((view) => {
setLoading(true);
const columns = metadata.columns;
const orderRows = sortTableRows({ columns }, metadata.rows, view?.sorts || [], { collaborators, isReturnID: false });
let newMetadata = new Metadata({ rows: orderRows, columns, view });
newMetadata.hasMore = false;
setMetadata(newMetadata);
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.RESET_VIEW, newMetadata.view);
setLoading(false);
}, [metadata, collaborators]);
const deletedByIds = useCallback((ids) => {
if (!Array.isArray(ids) || ids.length === 0) return;
const newMetadata = deepCopy(metadata);
const idNeedDeletedMap = ids.reduce((currIdNeedDeletedMap, rowId) => ({ ...currIdNeedDeletedMap, [rowId]: true }), {});
newMetadata.rows = newMetadata.rows.filter((row) => !idNeedDeletedMap[row._id]);
newMetadata.row_ids = newMetadata.row_ids.filter((id) => !idNeedDeletedMap[id]);
// delete rows in id_row_map
ids.forEach(rowId => {
delete newMetadata.id_row_map[rowId];
});
newMetadata.recordsCount = newMetadata.row_ids.length;
setMetadata(newMetadata);
if (newMetadata.rows.length === 0) {
onClose && onClose();
}
}, [metadata, onClose]);
const handelDelete = useCallback((deletedImages, { success_callback } = {}) => {
if (!deletedImages.length) return;
let recordIds = [];
deletedImages.forEach((record) => {
const { id, parentDir, name } = record || {};
if (parentDir && name) {
recordIds.push(id);
}
});
deleteRecords(recordIds, {
success_callback: () => {
success_callback();
deletedByIds(recordIds);
}
});
}, [deleteRecords, deletedByIds]);
const onViewChange = useCallback((update) => {
metadataAPI.modifyView(repoID, viewID, update).then(res => {
store.modifyLocalView(update);
const newView = { ...metadata.view, ...update };
loadData(newView);
}).catch(error => {
const errorMessage = Utils.getErrorMsg(error);
toaster.danger(errorMessage);
});
}, [metadata, repoID, viewID, store, loadData]);
const onRecordChange = useCallback(({ recordId, parentDir, fileName }, update) => {
const modifyTime = dayjs().utc().format(UTC_FORMAT_DEFAULT);
const modifier = window.sfMetadataContext.getUsername();
const { rows, columns, view } = metadata;
let newRows = [...rows];
newRows.forEach((row, index) => {
const _rowId = getRecordIdFromRecord(row);
const _parentDir = getParentDirFromRecord(row);
const _fileName = getFileNameFromRecord(row);
if ((_rowId === recordId || (_parentDir === parentDir && _fileName === fileName)) && update) {
const updatedRow = Object.assign({}, row, update, {
'_mtime': modifyTime,
'_last_modifier': modifier,
});
newRows[index] = updatedRow;
}
});
let updatedColumnKeyMap = {
'_mtime': true,
'_last_modifier': true
};
Object.keys(update).forEach(key => {
updatedColumnKeyMap[key] = true;
});
if (view.sorts.some(sort => updatedColumnKeyMap[sort.column_key])) {
newRows = sortTableRows({ columns }, newRows, view?.sorts || [], { collaborators, isReturnID: false });
}
let newMetadata = new Metadata({ rows: newRows, columns, view });
newMetadata.hasMore = false;
setMetadata(newMetadata);
}, [metadata, collaborators]);
useEffect(() => {
const eventBus = window?.sfMetadataContext?.eventBus;
if (!eventBus) return;
eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, true);
return () => {
eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, false);
};
}, []);
useEffect(() => {
const eventBus = window?.sfMetadataContext?.eventBus;
if (!eventBus) return;
const unsubscribeViewChange = eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_SERVER_VIEW, onViewChange);
const localRecordChangedSubscribe = eventBus.subscribe(EVENT_BUS_TYPE.LOCAL_RECORD_CHANGED, onRecordChange);
return () => {
unsubscribeViewChange && unsubscribeViewChange();
localRecordChangedSubscribe && localRecordChangedSubscribe();
};
}, [onViewChange, onRecordChange]);
useEffect(() => {
loadData({ sorts: allMetadata.view.sorts });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (isLoading) return (<CenteredLoading />);
return (
<div className="sf-metadata-view-map sf-metadata-map-photos-container">
<Gallery metadata={metadata} onDelete={handelDelete} onAddFolder={addFolder} />
</div>
);
};
ClusterPhotos.propTypes = {
photoIds: PropTypes.array,
onClose: PropTypes.func,
};
export default ClusterPhotos;

View File

@ -1,4 +1,4 @@
.sf-map-control-container.sf-map-geolocation-control { .sf-map-control-container.sf-map-geolocation-control {
width: 40px; width: 30px;
line-height: 40px; line-height: 30px;
} }

View File

@ -1,5 +1,5 @@
import classnames from 'classnames'; import classnames from 'classnames';
import { Utils } from '../../../../../../utils/utils'; import { Utils } from '../../../../../utils/utils';
import './index.css'; import './index.css';

View File

@ -1,12 +1,12 @@
.sf-map-control-container { .sf-map-control-container {
height: 40px; height: 30px;
width: fit-content; width: fit-content;
background-color: rgba(255, 255, 255, .9); background-color: rgba(255, 255, 255, .9);
opacity: 1; opacity: 1;
overflow: hidden; overflow: hidden;
border-radius: 6px; border-radius: 6px;
box-shadow: -2px -2px 4px 2px rgba(0, 0, 0, 0.1); box-shadow: -2px -2px 4px 2px rgba(0, 0, 0, 0.1);
line-height: 40px; line-height: 30px;
} }
.sf-map-control-container.sf-map-control-loading { .sf-map-control-container.sf-map-control-loading {
@ -14,8 +14,8 @@
} }
.sf-map-control-container.sf-map-control-container-mobile { .sf-map-control-container.sf-map-control-container-mobile {
height: 35px; height: 25px;
line-height: 35px; line-height: 25px;
opacity: .75; opacity: .75;
} }
@ -36,21 +36,21 @@
} }
.sf-map-control-container .sf-map-control { .sf-map-control-container .sf-map-control {
height: 40px; height: 30px;
width: 40px; width: 30px;
color: #666; color: #666;
background-color: inherit; background-color: inherit;
text-align: center; text-align: center;
} }
.sf-map-control-container.sf-map-control-container-mobile .sf-map-control { .sf-map-control-container.sf-map-control-container-mobile .sf-map-control {
height: 35px; height: 25px;
width: 35px; width: 25px;
opacity: .75; opacity: .75;
} }
.sf-map-control-container .sf-map-control .sf-map-control-icon { .sf-map-control-container .sf-map-control .sf-map-control-icon {
font-size: 18px; font-size: 14px;
} }
.sf-map-control-container .sf-map-control:not(.disabled):hover { .sf-map-control-container .sf-map-control:not(.disabled):hover {

View File

@ -1,3 +1,3 @@
.sf-map-control-container.sf-map-zoom-control-container .sf-map-control { .sf-map-control-container.sf-map-zoom-control-container .sf-map-control {
width: 55px; width: 40px;
} }

View File

@ -1,12 +1,12 @@
import classnames from 'classnames'; import classnames from 'classnames';
import { Utils } from '../../../../../../utils/utils'; import { Utils } from '../../../../../utils/utils';
import './index.css'; import './index.css';
export function createBMapZoomControl(BMapGL, { maxZoom, minZoom }, callback) { export function createBMapZoomControl(BMapGL, { maxZoom, minZoom }, callback) {
function ZoomControl() { function ZoomControl() {
this.defaultAnchor = window.BMAP_ANCHOR_BOTTOM_RIGHT; this.defaultAnchor = window.BMAP_ANCHOR_BOTTOM_RIGHT;
this.defaultOffset = new BMapGL.Size(80, Utils.isDesktop() ? 30 : 90); this.defaultOffset = new BMapGL.Size(66, Utils.isDesktop() ? 30 : 90);
} }
ZoomControl.prototype = new BMapGL.Control(); ZoomControl.prototype = new BMapGL.Control();
ZoomControl.prototype.initialize = function (map) { ZoomControl.prototype.initialize = function (map) {

View File

@ -4,3 +4,86 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.sf-metadata-view-map #platform div:has(.custom-avatar-overlay) {
display: block !important;
}
.sf-metadata-view-map #platform div:has(.custom-image-overlay) {
display: block !important;
}
.sf-metadata-view-map .sf-metadata-map-container {
width: 100%;
height: 100%;
}
.sf-metadata-view-map .custom-image-container {
width: 86px;
height: 86px;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
padding: 3px;
border-radius: 6px;
position: relative;
cursor: default;
}
.sf-metadata-view-map .custom-image-container img {
border-radius: 6px;
}
.sf-metadata-view-map .custom-image-number {
position: absolute;
right: -16px;
top: -16px;
width: 32px;
height: 32px;
line-height: 32px;
background: #007bff;
color: #fff;
border-radius: 50%;
text-align: center;
font-size: 16px;
font-weight: 400;
}
.sf-metadata-view-map .custom-image-container:active::before,
.sf-metadata-view-map .custom-image-container:active .custom-image-number::before,
.sf-metadata-view-map .custom-image-number:active::before,
.sf-metadata-view-map .custom-image-number:active .custom-image-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
border-radius: 6px;
}
.sf-metadata-view-map .custom-image-container:active .custom-image-number::before,
.sf-metadata-view-map .custom-image-number:active::before {
border-radius: 50%;
}
.sf-metadata-view-map .custom-image-container::after {
content: '';
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid #fff;
border-radius: 2px;
}
.sf-metadata-view-map .custom-image-container:active::after,
.sf-metadata-view-map .custom-image-number:active .custom-image-container::after {
border-top: 10px solid rgba(0, 0, 0, 0.4);
}

View File

@ -1,17 +1,32 @@
import React, { useEffect, useMemo } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { getFileNameFromRecord, getFileTypeFromRecord, getImageLocationFromRecord, getParentDirFromRecord, getRecordIdFromRecord } from '../../utils/cell'; import { getFileNameFromRecord, getFileTypeFromRecord, getImageLocationFromRecord, getParentDirFromRecord, getRecordIdFromRecord } from '../../utils/cell';
import MapView from './map-view';
import { PREDEFINED_FILE_TYPE_OPTION_KEY } from '../../constants';
import { useMetadataView } from '../../hooks/metadata-view'; import { useMetadataView } from '../../hooks/metadata-view';
import { Utils } from '../../../utils/utils'; import { Utils } from '../../../utils/utils';
import { fileServerRoot, siteRoot, thumbnailSizeForGrid, thumbnailSizeForOriginal } from '../../../utils/constants';
import { isValidPosition } from '../../utils/validate'; import { isValidPosition } from '../../utils/validate';
import { gcj02_to_bd09, wgs84_to_gcj02 } from '../../../utils/coord-transform'; import { gcj02_to_bd09, wgs84_to_gcj02 } from '../../../utils/coord-transform';
import { PRIVATE_FILE_TYPE } from '../../../constants'; import loadBMap, { initMapInfo } from '../../../utils/map-utils';
import { appAvatarURL, baiduMapKey, fileServerRoot, googleMapKey, mediaUrl, siteRoot, thumbnailSizeForGrid, thumbnailSizeForOriginal } from '../../../utils/constants';
import { MAP_TYPE as MAP_PROVIDER, PRIVATE_FILE_TYPE } from '../../../constants';
import { EVENT_BUS_TYPE, MAP_TYPE, PREDEFINED_FILE_TYPE_OPTION_KEY, STORAGE_MAP_CENTER_KEY, STORAGE_MAP_TYPE_KEY, STORAGE_MAP_ZOOM_KEY } from '../../constants';
import { createBMapGeolocationControl, createBMapZoomControl } from './control';
import { customAvatarOverlay, customImageOverlay } from './overlay';
import ModalPortal from '../../../components/modal-portal';
import ImageDialog from '../../../components/dialog/image-dialog';
import './index.css'; import './index.css';
const DEFAULT_POSITION = { lng: 104.195, lat: 35.861 };
const DEFAULT_ZOOM = 4;
const MAX_ZOOM = 21;
const MIN_ZOOM = 3;
const Map = () => { const Map = () => {
const [imageIndex, setImageIndex] = useState(0);
const [clusterLeaveIds, setClusterLeaveIds] = useState([]);
const mapRef = useRef(null);
const clusterRef = useRef(null);
const clickTimeoutRef = useRef(null);
const { metadata, viewID, updateCurrentPath } = useMetadataView(); const { metadata, viewID, updateCurrentPath } = useMetadataView();
const repoID = window.sfMetadataContext.getSetting('repoID'); const repoID = window.sfMetadataContext.getSetting('repoID');
@ -52,18 +67,219 @@ const Map = () => {
downloadURL: `${fileServerRoot}repos/${repoID}/files${path}?op=download`, downloadURL: `${fileServerRoot}repos/${repoID}/files${path}?op=download`,
thumbnail, thumbnail,
parentDir, parentDir,
location: { lng: bdPosition.lng, lat: bdPosition.lat } location: bdPosition
}; };
}) })
.filter(Boolean); .filter(Boolean);
}, [repoID, repoInfo.encrypted, metadata]); }, [repoID, repoInfo.encrypted, metadata]);
const mapInfo = useMemo(() => initMapInfo({ baiduMapKey, googleMapKey }), []);
const clusterLeaves = useMemo(() => images.filter(image => clusterLeaveIds.includes(image.id)), [images, clusterLeaveIds]);
const getPoints = useCallback(() => {
if (!window.Cluster || !images) return [];
return window.Cluster.pointTransformer(images, (data) => ({
point: [data.location.lng, data.location.lat],
properties: {
id: data.id,
src: data.src,
}
}));
}, [images]);
const saveMapState = useCallback(() => {
if (!mapRef.current) return;
const point = mapRef.current.getCenter && mapRef.current.getCenter();
const zoom = mapRef.current.getZoom && mapRef.current.getZoom();
window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_CENTER_KEY, point);
window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_ZOOM_KEY, zoom);
}, []);
const addMapController = useCallback(() => {
const ZoomControl = createBMapZoomControl(window.BMapGL, { maxZoom: MAX_ZOOM, minZoom: MIN_ZOOM }, saveMapState);
const zoomControl = new ZoomControl();
const GeolocationControl = createBMapGeolocationControl(window.BMapGL, (point) => {
point && mapRef.current && mapRef.current.setCenter(point);
});
const geolocationControl = new GeolocationControl();
mapRef.current.addControl(zoomControl);
mapRef.current.addControl(geolocationControl);
}, [saveMapState]);
const initializeUserMarker = useCallback((centerPoint) => {
if (!window.BMapGL || !mapRef.current) return;
const imageUrl = `${mediaUrl}img/marker.png`;
const avatarMarker = customAvatarOverlay(centerPoint, appAvatarURL, imageUrl);
mapRef.current.addOverlay(avatarMarker);
}, []);
const getBMapType = useCallback((type) => {
switch (type) {
case MAP_TYPE.SATELLITE: {
return window.BMAP_EARTH_MAP;
}
default: {
return window.BMAP_NORMAL_MAP;
}
}
}, []);
const loadMapState = useCallback(() => {
const savedCenter = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_CENTER_KEY) || DEFAULT_POSITION;
const savedZoom = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_ZOOM_KEY) || DEFAULT_ZOOM;
return { center: savedCenter, zoom: savedZoom };
}, []);
const initializeCluster = useCallback(() => {
clusterRef.current = new window.Cluster.View(mapRef.current, {
clusterRadius: 60,
updateRealTime: true,
fitViewOnClick: false,
isAnimation: true,
clusterMap: (properties) => ({ src: properties.src, id: properties.id }),
clusterReduce: (acc, properties) => {
if (!acc.properties) {
acc.properties = [];
}
acc.properties.push(properties);
},
renderClusterStyle: {
type: window.Cluster.ClusterRender.DOM,
style: { offsetX: -40, offsetY: -80 },
inject: (props) => customImageOverlay(props),
},
});
clusterRef.current.setData(getPoints());
clusterRef.current.on(window.Cluster.ClusterEvent.CLICK, (element) => {
if (clickTimeoutRef.current) {
clearTimeout(clickTimeoutRef.current);
clickTimeoutRef.current = null;
return;
} else {
clickTimeoutRef.current = setTimeout(() => {
let imageIds = [];
if (element.isCluster) {
imageIds = clusterRef.current.getLeaves(element.id).map(item => item.properties.id).filter(Boolean);
} else {
imageIds = [element.properties.id];
}
clickTimeoutRef.current = null;
setClusterLeaveIds(imageIds);
}, 300);
}
});
window.BMapCluster = clusterRef.current;
}, [getPoints]);
const renderBaiduMap = useCallback(() => {
if (!window.BMapGL.Map) return;
let { center, zoom } = loadMapState();
let userPosition = { lng: 116.40396418840683, lat: 39.915106021711345 };
// ask for user location
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition((userInfo) => {
const gcPosition = wgs84_to_gcj02(userInfo.coords.longitude, userInfo.coords.latitude);
const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat);
const { lng, lat } = bdPosition;
userPosition = new window.BMapGL.Point(lng, lat);
center = userPosition;
window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_CENTER_KEY, center);
});
}
const mapTypeValue = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_TYPE_KEY);
if (!window.BMapInstance) {
mapRef.current = new window.BMapGL.Map('sf-metadata-map-container', {
enableMapClick: false,
minZoom: MIN_ZOOM,
maxZoom: MAX_ZOOM,
mapType: getBMapType(mapTypeValue),
});
window.BMapInstance = mapRef.current;
if (isValidPosition(center?.lng, center?.lat)) {
mapRef.current.centerAndZoom(center, zoom);
}
mapRef.current.enableScrollWheelZoom(true);
addMapController();
initializeUserMarker(userPosition);
initializeCluster();
} else {
const viewDom = document.getElementById('sf-metadata-view-map');
const container = window.BMapInstance.getContainer();
viewDom.replaceChild(container, mapRef.current);
mapRef.current = window.BMapInstance;
clusterRef.current = window.BMapCluster;
clusterRef.current.setData(getPoints());
}
}, [addMapController, initializeCluster, initializeUserMarker, getBMapType, loadMapState, getPoints]);
const handleClose = useCallback(() => {
setImageIndex(0);
setClusterLeaveIds([]);
}, []);
const moveToPrevImage = useCallback(() => {
setImageIndex((imageIndex + clusterLeaves.length - 1) % clusterLeaves.length);
}, [imageIndex, clusterLeaves.length]);
const moveToNextImage = useCallback(() => {
setImageIndex((imageIndex + 1) % clusterLeaves.length);
}, [imageIndex, clusterLeaves.length]);
useEffect(() => { useEffect(() => {
updateCurrentPath(`/${PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES}/${viewID}`); updateCurrentPath(`/${PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES}/${viewID}`);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
return (<MapView images={images} />); useEffect(() => {
const modifyMapTypeSubscribe = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.MODIFY_MAP_TYPE, (newType) => {
window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_TYPE_KEY, newType);
const mapType = getBMapType(newType);
mapRef.current && mapRef.current.setMapType(mapType);
mapRef.current.setCenter(mapRef.current.getCenter());
});
return () => {
modifyMapTypeSubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (mapInfo.type === MAP_PROVIDER.B_MAP) {
loadBMap(mapInfo.key).then(() => renderBaiduMap());
return () => {
window.renderMap = null;
};
}
return;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="sf-metadata-view-map" id="sf-metadata-view-map">
<div ref={mapRef} className="sf-metadata-map-container" id="sf-metadata-map-container"></div>
{clusterLeaveIds.length > 0 && (
<ModalPortal>
<ImageDialog
imageItems={clusterLeaves}
imageIndex={imageIndex}
closeImagePopup={handleClose}
moveToPrevImage={moveToPrevImage}
moveToNextImage={moveToNextImage}
/>
</ModalPortal>
)}
</div>
);
}; };
export default Map; export default Map;

View File

@ -1,82 +0,0 @@
.sf-metadata-view-map #platform div:has(.custom-avatar-overlay) {
display: block !important;
}
.sf-metadata-view-map #platform div:has(.custom-image-overlay) {
display: block !important;
}
.sf-metadata-view-map .sf-metadata-map-container {
width: 100%;
height: 100%;
}
.sf-metadata-view-map .custom-image-container {
width: 86px;
height: 86px;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
padding: 3px;
border-radius: 6px;
position: relative;
cursor: default;
}
.sf-metadata-view-map .custom-image-container img {
border-radius: 6px;
}
.sf-metadata-view-map .custom-image-number {
position: absolute;
right: -16px;
top: -16px;
width: 32px;
height: 32px;
line-height: 32px;
background: #007bff;
color: #fff;
border-radius: 50%;
text-align: center;
font-size: 16px;
font-weight: 400;
}
.sf-metadata-view-map .custom-image-container:active::before,
.sf-metadata-view-map .custom-image-container:active .custom-image-number::before,
.sf-metadata-view-map .custom-image-number:active::before,
.sf-metadata-view-map .custom-image-number:active .custom-image-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
border-radius: 6px;
}
.sf-metadata-view-map .custom-image-container:active .custom-image-number::before,
.sf-metadata-view-map .custom-image-number:active::before {
border-radius: 50%;
}
.sf-metadata-view-map .custom-image-container::after {
content: '';
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid #fff;
border-radius: 2px;
}
.sf-metadata-view-map .custom-image-container:active::after,
.sf-metadata-view-map .custom-image-number:active .custom-image-container::after {
border-top: 10px solid rgba(0, 0, 0, 0.4);
}

View File

@ -1,226 +0,0 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import loadBMap, { initMapInfo } from '../../../../utils/map-utils';
import { appAvatarURL, baiduMapKey, googleMapKey, mediaUrl } from '../../../../utils/constants';
import { isValidPosition } from '../../../utils/validate';
import { wgs84_to_gcj02, gcj02_to_bd09 } from '../../../../utils/coord-transform';
import { MAP_TYPE as MAP_PROVIDER } from '../../../../constants';
import { EVENT_BUS_TYPE, MAP_TYPE, STORAGE_MAP_CENTER_KEY, STORAGE_MAP_TYPE_KEY, STORAGE_MAP_ZOOM_KEY } from '../../../constants';
import { createBMapGeolocationControl, createBMapZoomControl } from './control';
import { customAvatarOverlay, customImageOverlay } from './overlay';
import ModalPortal from '../../../../components/modal-portal';
import ImageDialog from '../../../../components/dialog/image-dialog';
import './index.css';
const DEFAULT_POSITION = { lng: 104.195, lat: 35.861 };
const DEFAULT_ZOOM = 4;
const MAX_ZOOM = 21;
const MIN_ZOOM = 3;
const MapView = ({ images }) => {
const [imageIndex, setImageIndex] = useState(0);
const [clusterLeaveIds, setClusterLeaveIds] = useState([]);
const mapInfo = useMemo(() => initMapInfo({ baiduMapKey, googleMapKey }), []);
const clusterLeaves = useMemo(() => images.filter(image => clusterLeaveIds.includes(image.id)), [images, clusterLeaveIds]);
const mapRef = useRef(null);
const clusterRef = useRef(null);
const batchIndexRef = useRef(0);
const clickTimeoutRef = useRef(null);
const saveMapState = useCallback(() => {
if (!mapRef.current) return;
const point = mapRef.current.getCenter && mapRef.current.getCenter();
const zoom = mapRef.current.getZoom && mapRef.current.getZoom();
window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_CENTER_KEY, point);
window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_ZOOM_KEY, zoom);
}, []);
const addMapController = useCallback(() => {
const ZoomControl = createBMapZoomControl(window.BMapGL, { maxZoom: MAX_ZOOM, minZoom: MIN_ZOOM }, saveMapState);
const zoomControl = new ZoomControl();
const GeolocationControl = createBMapGeolocationControl(window.BMapGL, (point) => {
point && mapRef.current && mapRef.current.setCenter(point);
});
const geolocationControl = new GeolocationControl();
mapRef.current.addControl(zoomControl);
mapRef.current.addControl(geolocationControl);
}, [saveMapState]);
const initializeUserMarker = useCallback((centerPoint) => {
if (!window.BMapGL || !mapRef.current) return;
const imageUrl = `${mediaUrl}img/marker.png`;
const avatarMarker = customAvatarOverlay(centerPoint, appAvatarURL, imageUrl);
mapRef.current.addOverlay(avatarMarker);
}, []);
const getBMapType = useCallback((type) => {
switch (type) {
case MAP_TYPE.SATELLITE: {
return window.BMAP_EARTH_MAP;
}
default: {
return window.BMAP_NORMAL_MAP;
}
}
}, []);
const loadMapState = useCallback(() => {
const savedCenter = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_CENTER_KEY) || DEFAULT_POSITION;
const savedZoom = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_ZOOM_KEY) || DEFAULT_ZOOM;
return { center: savedCenter, zoom: savedZoom };
}, []);
const getPoints = useCallback((images) => {
if (!window.Cluster || !images) return [];
return window.Cluster.pointTransformer(images, (data) => ({
point: [data.location.lng, data.location.lat],
properties: {
id: data.id,
src: data.src,
}
}));
}, []);
const initializeCluster = useCallback(() => {
clusterRef.current = new window.Cluster.View(mapRef.current, {
clusterRadius: 80,
updateRealTime: true,
fitViewOnClick: false,
isAnimation: true,
clusterMap: (properties) => ({ src: properties.src, id: properties.id }),
clusterReduce: (acc, properties) => {
if (!acc.properties) {
acc.properties = [];
}
acc.properties.push(properties);
},
renderClusterStyle: {
type: window.Cluster.ClusterRender.DOM,
inject: (props) => customImageOverlay(props),
},
});
clusterRef.current.setData(getPoints(images));
clusterRef.current.on(window.Cluster.ClusterEvent.CLICK, (element) => {
if (clickTimeoutRef.current) {
clearTimeout(clickTimeoutRef.current);
clickTimeoutRef.current = null;
return;
} else {
clickTimeoutRef.current = setTimeout(() => {
let imageIds = [];
if (element.isCluster) {
imageIds = clusterRef.current.getLeaves(element.id).map(item => item.properties.id).filter(Boolean);
} else {
imageIds = [element.properties.id];
}
clickTimeoutRef.current = null;
setClusterLeaveIds(imageIds);
}, 300);
}
});
}, [images, getPoints]);
const renderBaiduMap = useCallback(() => {
if (!mapRef.current || !window.BMapGL.Map) return;
let { center, zoom } = loadMapState();
let userPosition = { lng: 116.40396418840683, lat: 39.915106021711345 };
// ask for user location
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition((userInfo) => {
const gcPosition = wgs84_to_gcj02(userInfo.coords.longitude, userInfo.coords.latitude);
const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat);
const { lng, lat } = bdPosition;
userPosition = new window.BMapGL.Point(lng, lat);
center = userPosition;
window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_CENTER_KEY, center);
});
}
const mapTypeValue = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_TYPE_KEY);
mapRef.current = new window.BMapGL.Map('sf-metadata-map-container', {
enableMapClick: false,
minZoom: MIN_ZOOM,
maxZoom: MAX_ZOOM,
mapType: getBMapType(mapTypeValue),
});
if (isValidPosition(center?.lng, center?.lat)) {
mapRef.current.centerAndZoom(center, zoom);
}
mapRef.current.enableScrollWheelZoom(true);
addMapController();
initializeUserMarker(userPosition);
initializeCluster();
batchIndexRef.current = 0;
}, [addMapController, initializeCluster, initializeUserMarker, getBMapType, loadMapState]);
const handleClose = useCallback(() => {
setImageIndex(0);
setClusterLeaveIds([]);
}, []);
const moveToPrevImage = useCallback(() => {
setImageIndex((imageIndex + clusterLeaves.length - 1) % clusterLeaves.length);
}, [imageIndex, clusterLeaves.length]);
const moveToNextImage = useCallback(() => {
setImageIndex((imageIndex + 1) % clusterLeaves.length);
}, [imageIndex, clusterLeaves.length]);
useEffect(() => {
const modifyMapTypeSubscribe = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.MODIFY_MAP_TYPE, (newType) => {
window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_TYPE_KEY, newType);
const mapType = getBMapType(newType);
mapRef.current && mapRef.current.setMapType(mapType);
});
return () => {
modifyMapTypeSubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (mapInfo.type === MAP_PROVIDER.B_MAP) {
loadBMap(mapInfo.key).then(() => renderBaiduMap());
return () => {
window.renderMap = null;
};
}
return;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="sf-metadata-view-map">
<div className="sf-metadata-map-container" ref={mapRef} id="sf-metadata-map-container"></div>
{clusterLeaveIds.length > 0 && (
<ModalPortal>
<ImageDialog
imageItems={clusterLeaves}
imageIndex={imageIndex}
closeImagePopup={handleClose}
moveToPrevImage={moveToPrevImage}
moveToNextImage={moveToNextImage}
/>
</ModalPortal>
)}
</div>
);
};
MapView.propTypes = {
images: PropTypes.array,
onOpenCluster: PropTypes.func,
};
export default MapView;