mirror of
https://github.com/haiwen/seahub.git
synced 2025-07-15 07:52:14 +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:
parent
f8112c0306
commit
94e19c58c0
@ -114,11 +114,9 @@ const ViewToolBar = ({ viewId, isCustomPermission, onToggleDetail, onCloseDetail
|
||||
{viewType === VIEW_TYPE.MAP && (
|
||||
<MapViewToolBar
|
||||
readOnly={readOnly}
|
||||
isCustomPermission={isCustomPermission}
|
||||
view={view}
|
||||
collaborators={collaborators}
|
||||
modifyFilters={modifyFilters}
|
||||
onToggleDetail={onToggleDetail}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -1,20 +1,14 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY, VIEW_TYPE } from '../../../constants';
|
||||
import { FilterSetter, GalleryGroupBySetter, MapTypeSetter, SortSetter } from '../../data-process-setter';
|
||||
import { gettext } from '../../../../utils/constants';
|
||||
import { PRIVATE_COLUMN_KEY, VIEW_TYPE } from '../../../constants';
|
||||
import { FilterSetter, MapTypeSetter } from '../../data-process-setter';
|
||||
|
||||
const MapViewToolBar = ({
|
||||
isCustomPermission,
|
||||
readOnly,
|
||||
view: oldView,
|
||||
collaborators,
|
||||
modifyFilters,
|
||||
onToggleDetail,
|
||||
}) => {
|
||||
const [showGalleryToolbar, setShowGalleryToolbar] = useState(false);
|
||||
const [view, setView] = useState(oldView);
|
||||
|
||||
const viewType = useMemo(() => VIEW_TYPE.MAP, []);
|
||||
const viewColumns = useMemo(() => {
|
||||
if (!oldView) return [];
|
||||
@ -26,54 +20,6 @@ const MapViewToolBar = ({
|
||||
return viewColumns && viewColumns.filter(c => c.key !== PRIVATE_COLUMN_KEY.FILE_TYPE);
|
||||
}, [viewColumns]);
|
||||
|
||||
const onToggle = useCallback((value) => {
|
||||
setShowGalleryToolbar(value);
|
||||
}, []);
|
||||
|
||||
const modifySorts = useCallback((sorts) => {
|
||||
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.UPDATE_SERVER_VIEW, { sorts });
|
||||
}, []);
|
||||
|
||||
const resetView = useCallback(view => {
|
||||
setView(view);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setShowGalleryToolbar(false);
|
||||
const unsubscribeToggle = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, onToggle);
|
||||
const unsubscribeView = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.RESET_VIEW, resetView);
|
||||
return () => {
|
||||
unsubscribeToggle && unsubscribeToggle();
|
||||
unsubscribeView && unsubscribeView();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [viewID]);
|
||||
|
||||
if (showGalleryToolbar) {
|
||||
return (
|
||||
<>
|
||||
<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 (
|
||||
<>
|
||||
<div className="sf-metadata-tool-left-operations">
|
||||
@ -99,11 +45,10 @@ const MapViewToolBar = ({
|
||||
};
|
||||
|
||||
MapViewToolBar.propTypes = {
|
||||
isCustomPermission: PropTypes.bool,
|
||||
readOnly: PropTypes.bool,
|
||||
view: PropTypes.object,
|
||||
collaborators: PropTypes.array,
|
||||
modifyFilters: PropTypes.func,
|
||||
onToggleDetail: PropTypes.func,
|
||||
};
|
||||
|
||||
export default MapViewToolBar;
|
||||
|
@ -1,4 +0,0 @@
|
||||
.sf-metadata-map-photos-container {
|
||||
padding: 0 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
@ -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;
|
@ -1,4 +1,4 @@
|
||||
.sf-map-control-container.sf-map-geolocation-control {
|
||||
width: 40px;
|
||||
line-height: 40px;
|
||||
width: 30px;
|
||||
line-height: 30px;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import classnames from 'classnames';
|
||||
import { Utils } from '../../../../../../utils/utils';
|
||||
import { Utils } from '../../../../../utils/utils';
|
||||
|
||||
import './index.css';
|
||||
|
@ -1,12 +1,12 @@
|
||||
.sf-map-control-container {
|
||||
height: 40px;
|
||||
height: 30px;
|
||||
width: fit-content;
|
||||
background-color: rgba(255, 255, 255, .9);
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
box-shadow: -2px -2px 4px 2px rgba(0, 0, 0, 0.1);
|
||||
line-height: 40px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.sf-map-control-container.sf-map-control-loading {
|
||||
@ -14,8 +14,8 @@
|
||||
}
|
||||
|
||||
.sf-map-control-container.sf-map-control-container-mobile {
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
@ -36,21 +36,21 @@
|
||||
}
|
||||
|
||||
.sf-map-control-container .sf-map-control {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
color: #666;
|
||||
background-color: inherit;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sf-map-control-container.sf-map-control-container-mobile .sf-map-control {
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
.sf-map-control-container .sf-map-control .sf-map-control-icon {
|
||||
font-size: 18px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sf-map-control-container .sf-map-control:not(.disabled):hover {
|
@ -1,3 +1,3 @@
|
||||
.sf-map-control-container.sf-map-zoom-control-container .sf-map-control {
|
||||
width: 55px;
|
||||
width: 40px;
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
import classnames from 'classnames';
|
||||
import { Utils } from '../../../../../../utils/utils';
|
||||
import { Utils } from '../../../../../utils/utils';
|
||||
|
||||
import './index.css';
|
||||
|
||||
export function createBMapZoomControl(BMapGL, { maxZoom, minZoom }, callback) {
|
||||
function ZoomControl() {
|
||||
this.defaultAnchor = window.BMAP_ANCHOR_BOTTOM_RIGHT;
|
||||
this.defaultOffset = new BMapGL.Size(80, Utils.isDesktop() ? 30 : 90);
|
||||
this.defaultOffset = new BMapGL.Size(66, Utils.isDesktop() ? 30 : 90);
|
||||
}
|
||||
ZoomControl.prototype = new BMapGL.Control();
|
||||
ZoomControl.prototype.initialize = function (map) {
|
@ -4,3 +4,86 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sf-metadata-view-map #platform div:has(.custom-avatar-overlay) {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.sf-metadata-view-map #platform div:has(.custom-image-overlay) {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.sf-metadata-view-map .sf-metadata-map-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sf-metadata-view-map .custom-image-container {
|
||||
width: 86px;
|
||||
height: 86px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fff;
|
||||
padding: 3px;
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.sf-metadata-view-map .custom-image-container img {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.sf-metadata-view-map .custom-image-number {
|
||||
position: absolute;
|
||||
right: -16px;
|
||||
top: -16px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
background: #007bff;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.sf-metadata-view-map .custom-image-container:active::before,
|
||||
.sf-metadata-view-map .custom-image-container:active .custom-image-number::before,
|
||||
.sf-metadata-view-map .custom-image-number:active::before,
|
||||
.sf-metadata-view-map .custom-image-number:active .custom-image-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.sf-metadata-view-map .custom-image-container:active .custom-image-number::before,
|
||||
.sf-metadata-view-map .custom-image-number:active::before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.sf-metadata-view-map .custom-image-container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid #fff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.sf-metadata-view-map .custom-image-container:active::after,
|
||||
.sf-metadata-view-map .custom-image-number:active .custom-image-container::after {
|
||||
border-top: 10px solid rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
@ -1,17 +1,32 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { getFileNameFromRecord, getFileTypeFromRecord, getImageLocationFromRecord, getParentDirFromRecord, getRecordIdFromRecord } from '../../utils/cell';
|
||||
import MapView from './map-view';
|
||||
import { PREDEFINED_FILE_TYPE_OPTION_KEY } from '../../constants';
|
||||
import { useMetadataView } from '../../hooks/metadata-view';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import { fileServerRoot, siteRoot, thumbnailSizeForGrid, thumbnailSizeForOriginal } from '../../../utils/constants';
|
||||
import { isValidPosition } from '../../utils/validate';
|
||||
import { gcj02_to_bd09, wgs84_to_gcj02 } from '../../../utils/coord-transform';
|
||||
import { PRIVATE_FILE_TYPE } from '../../../constants';
|
||||
import loadBMap, { initMapInfo } from '../../../utils/map-utils';
|
||||
import { appAvatarURL, baiduMapKey, fileServerRoot, googleMapKey, mediaUrl, siteRoot, thumbnailSizeForGrid, thumbnailSizeForOriginal } from '../../../utils/constants';
|
||||
import { MAP_TYPE as MAP_PROVIDER, PRIVATE_FILE_TYPE } from '../../../constants';
|
||||
import { EVENT_BUS_TYPE, MAP_TYPE, PREDEFINED_FILE_TYPE_OPTION_KEY, STORAGE_MAP_CENTER_KEY, STORAGE_MAP_TYPE_KEY, STORAGE_MAP_ZOOM_KEY } from '../../constants';
|
||||
import { createBMapGeolocationControl, createBMapZoomControl } from './control';
|
||||
import { customAvatarOverlay, customImageOverlay } from './overlay';
|
||||
import ModalPortal from '../../../components/modal-portal';
|
||||
import ImageDialog from '../../../components/dialog/image-dialog';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const DEFAULT_POSITION = { lng: 104.195, lat: 35.861 };
|
||||
const DEFAULT_ZOOM = 4;
|
||||
const MAX_ZOOM = 21;
|
||||
const MIN_ZOOM = 3;
|
||||
|
||||
const Map = () => {
|
||||
const [imageIndex, setImageIndex] = useState(0);
|
||||
const [clusterLeaveIds, setClusterLeaveIds] = useState([]);
|
||||
|
||||
const mapRef = useRef(null);
|
||||
const clusterRef = useRef(null);
|
||||
const clickTimeoutRef = useRef(null);
|
||||
const { metadata, viewID, updateCurrentPath } = useMetadataView();
|
||||
|
||||
const repoID = window.sfMetadataContext.getSetting('repoID');
|
||||
@ -52,18 +67,219 @@ const Map = () => {
|
||||
downloadURL: `${fileServerRoot}repos/${repoID}/files${path}?op=download`,
|
||||
thumbnail,
|
||||
parentDir,
|
||||
location: { lng: bdPosition.lng, lat: bdPosition.lat }
|
||||
location: bdPosition
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}, [repoID, repoInfo.encrypted, metadata]);
|
||||
|
||||
const mapInfo = useMemo(() => initMapInfo({ baiduMapKey, googleMapKey }), []);
|
||||
const clusterLeaves = useMemo(() => images.filter(image => clusterLeaveIds.includes(image.id)), [images, clusterLeaveIds]);
|
||||
|
||||
const getPoints = useCallback(() => {
|
||||
if (!window.Cluster || !images) return [];
|
||||
return window.Cluster.pointTransformer(images, (data) => ({
|
||||
point: [data.location.lng, data.location.lat],
|
||||
properties: {
|
||||
id: data.id,
|
||||
src: data.src,
|
||||
}
|
||||
}));
|
||||
}, [images]);
|
||||
|
||||
const saveMapState = useCallback(() => {
|
||||
if (!mapRef.current) return;
|
||||
const point = mapRef.current.getCenter && mapRef.current.getCenter();
|
||||
const zoom = mapRef.current.getZoom && mapRef.current.getZoom();
|
||||
window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_CENTER_KEY, point);
|
||||
window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_ZOOM_KEY, zoom);
|
||||
}, []);
|
||||
|
||||
const addMapController = useCallback(() => {
|
||||
const ZoomControl = createBMapZoomControl(window.BMapGL, { maxZoom: MAX_ZOOM, minZoom: MIN_ZOOM }, saveMapState);
|
||||
const zoomControl = new ZoomControl();
|
||||
const GeolocationControl = createBMapGeolocationControl(window.BMapGL, (point) => {
|
||||
point && mapRef.current && mapRef.current.setCenter(point);
|
||||
});
|
||||
|
||||
const geolocationControl = new GeolocationControl();
|
||||
mapRef.current.addControl(zoomControl);
|
||||
mapRef.current.addControl(geolocationControl);
|
||||
}, [saveMapState]);
|
||||
|
||||
const initializeUserMarker = useCallback((centerPoint) => {
|
||||
if (!window.BMapGL || !mapRef.current) return;
|
||||
const imageUrl = `${mediaUrl}img/marker.png`;
|
||||
const avatarMarker = customAvatarOverlay(centerPoint, appAvatarURL, imageUrl);
|
||||
mapRef.current.addOverlay(avatarMarker);
|
||||
}, []);
|
||||
|
||||
const getBMapType = useCallback((type) => {
|
||||
switch (type) {
|
||||
case MAP_TYPE.SATELLITE: {
|
||||
return window.BMAP_EARTH_MAP;
|
||||
}
|
||||
default: {
|
||||
return window.BMAP_NORMAL_MAP;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadMapState = useCallback(() => {
|
||||
const savedCenter = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_CENTER_KEY) || DEFAULT_POSITION;
|
||||
const savedZoom = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_ZOOM_KEY) || DEFAULT_ZOOM;
|
||||
return { center: savedCenter, zoom: savedZoom };
|
||||
}, []);
|
||||
|
||||
const initializeCluster = useCallback(() => {
|
||||
clusterRef.current = new window.Cluster.View(mapRef.current, {
|
||||
clusterRadius: 60,
|
||||
updateRealTime: true,
|
||||
fitViewOnClick: false,
|
||||
isAnimation: true,
|
||||
clusterMap: (properties) => ({ src: properties.src, id: properties.id }),
|
||||
clusterReduce: (acc, properties) => {
|
||||
if (!acc.properties) {
|
||||
acc.properties = [];
|
||||
}
|
||||
acc.properties.push(properties);
|
||||
},
|
||||
renderClusterStyle: {
|
||||
type: window.Cluster.ClusterRender.DOM,
|
||||
style: { offsetX: -40, offsetY: -80 },
|
||||
inject: (props) => customImageOverlay(props),
|
||||
},
|
||||
});
|
||||
|
||||
clusterRef.current.setData(getPoints());
|
||||
|
||||
clusterRef.current.on(window.Cluster.ClusterEvent.CLICK, (element) => {
|
||||
if (clickTimeoutRef.current) {
|
||||
clearTimeout(clickTimeoutRef.current);
|
||||
clickTimeoutRef.current = null;
|
||||
return;
|
||||
} else {
|
||||
clickTimeoutRef.current = setTimeout(() => {
|
||||
let imageIds = [];
|
||||
if (element.isCluster) {
|
||||
imageIds = clusterRef.current.getLeaves(element.id).map(item => item.properties.id).filter(Boolean);
|
||||
} else {
|
||||
imageIds = [element.properties.id];
|
||||
}
|
||||
clickTimeoutRef.current = null;
|
||||
setClusterLeaveIds(imageIds);
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
window.BMapCluster = clusterRef.current;
|
||||
}, [getPoints]);
|
||||
|
||||
const renderBaiduMap = useCallback(() => {
|
||||
if (!window.BMapGL.Map) return;
|
||||
let { center, zoom } = loadMapState();
|
||||
let userPosition = { lng: 116.40396418840683, lat: 39.915106021711345 };
|
||||
// ask for user location
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition((userInfo) => {
|
||||
const gcPosition = wgs84_to_gcj02(userInfo.coords.longitude, userInfo.coords.latitude);
|
||||
const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat);
|
||||
const { lng, lat } = bdPosition;
|
||||
userPosition = new window.BMapGL.Point(lng, lat);
|
||||
center = userPosition;
|
||||
window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_CENTER_KEY, center);
|
||||
});
|
||||
}
|
||||
const mapTypeValue = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_TYPE_KEY);
|
||||
|
||||
if (!window.BMapInstance) {
|
||||
mapRef.current = new window.BMapGL.Map('sf-metadata-map-container', {
|
||||
enableMapClick: false,
|
||||
minZoom: MIN_ZOOM,
|
||||
maxZoom: MAX_ZOOM,
|
||||
mapType: getBMapType(mapTypeValue),
|
||||
});
|
||||
window.BMapInstance = mapRef.current;
|
||||
|
||||
if (isValidPosition(center?.lng, center?.lat)) {
|
||||
mapRef.current.centerAndZoom(center, zoom);
|
||||
}
|
||||
|
||||
mapRef.current.enableScrollWheelZoom(true);
|
||||
addMapController();
|
||||
|
||||
initializeUserMarker(userPosition);
|
||||
initializeCluster();
|
||||
} else {
|
||||
const viewDom = document.getElementById('sf-metadata-view-map');
|
||||
const container = window.BMapInstance.getContainer();
|
||||
viewDom.replaceChild(container, mapRef.current);
|
||||
|
||||
mapRef.current = window.BMapInstance;
|
||||
clusterRef.current = window.BMapCluster;
|
||||
clusterRef.current.setData(getPoints());
|
||||
}
|
||||
}, [addMapController, initializeCluster, initializeUserMarker, getBMapType, loadMapState, getPoints]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setImageIndex(0);
|
||||
setClusterLeaveIds([]);
|
||||
}, []);
|
||||
|
||||
const moveToPrevImage = useCallback(() => {
|
||||
setImageIndex((imageIndex + clusterLeaves.length - 1) % clusterLeaves.length);
|
||||
}, [imageIndex, clusterLeaves.length]);
|
||||
|
||||
const moveToNextImage = useCallback(() => {
|
||||
setImageIndex((imageIndex + 1) % clusterLeaves.length);
|
||||
}, [imageIndex, clusterLeaves.length]);
|
||||
|
||||
useEffect(() => {
|
||||
updateCurrentPath(`/${PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES}/${viewID}`);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (<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;
|
||||
|
@ -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);
|
||||
}
|
@ -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;
|
Loading…
Reference in New Issue
Block a user