1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-15 14:49:09 +00:00

show cluster photos

This commit is contained in:
zhouwenxuan
2024-12-28 11:47:46 +08:00
committed by 杨国璇
parent 4d703eb910
commit 86d5d564da
21 changed files with 636 additions and 262 deletions

View File

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

View File

@@ -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,10 +21,57 @@ 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 ? (
<div className="sf-metadata-tool-left-operations">
<>
<GalleryGroupBySetter view={{ _id: view._id }} />
<GallerySliderSetter view={{ _id: view._id }} />
<SortSetter
isNeedSubmit={true}
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-left-operations">
<MapTypeSetter view={view} />
@@ -39,15 +91,17 @@ const MapViewToolBar = ({
/>
</div>
<div className="sf-metadata-tool-right-operations"></div>
</>}
</>
);
};
MapViewToolBar.propTypes = {
isCustomPermission: PropTypes.bool,
readOnly: PropTypes.bool,
view: PropTypes.object,
collaborators: PropTypes.array,
modifyFilters: PropTypes.func,
onToggleDetail: PropTypes.func,
};
export default MapViewToolBar;

View File

@@ -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',
};

View File

@@ -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 = {

View File

@@ -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 = {

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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 = [

View File

@@ -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);
}

View File

@@ -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 ? (
<CenteredLoading />
) : (
<div className="sf-metadata-view-map sf-metadata-map-photos-container">
<div className="sf-metadata-map-photos-header" onClick={onClose}>
<div className="sf-metadata-map-photos-header-back">
<i className="sf3-font sf3-font-arrow rotate-180"></i>
</div>
<div className="sf-metadata-map-location">{gettext('Location')}</div>
</div>
<Gallery metadata={metadata} onDelete={handelDelete} />
</div>
)
);
};
ClusterPhotos.propTypes = {
view: PropTypes.object,
markerIds: PropTypes.array,
onClose: PropTypes.func,
onDelete: PropTypes.func,
};
export default ClusterPhotos;

View File

@@ -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() {

View File

@@ -0,0 +1,7 @@
import { createBMapGeolocationControl } from './geolocation-control';
import { createBMapZoomControl } from './zoom-control';
export {
createBMapGeolocationControl,
createBMapZoomControl
};

View File

@@ -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 = '<svg class="zoom-in-icon"><use xlink:href="#plus_sign" /></svg>';
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 = '<svg class="zoom-out-icon"><use xlink:href="#minus_sign" /></svg>';
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;

View File

@@ -1,30 +1,38 @@
.sf-metadata-view-map {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.sf-metadata-view-map .sf-metadata-map-container {
width: 100%;;
width: 100%;
height: 100%;
min-height: 0;
}
.sf-metadata-view-map .custom-image-container {
padding: 4px;
background: #fff;
width: 80px;
height: 80px;
cursor: default;
border-radius: 4px;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 10%);
width: 86px;
height: 86px;
background-color: #fff;
padding: 3px;
border-radius: 6px;
position: relative;
cursor: default;
}
.sf-metadata-view-map .custom-image-container img {
width: 100%;
height: 100%;
border-radius: 6px;
}
.sf-metadata-view-map .custom-image-number {
position: absolute;
right: -15px;
top: -8px;
padding: 0 12px;
top: -16px;
width: 32px;
height: 32px;
padding: 6px;
background: #007bff;
color: #fff;
border-radius: 50%;
@@ -33,26 +41,23 @@
line-height: 20px;
}
.sf-metadata-view-map .custom-image-container .plugin-label-arrow {
.sf-metadata-view-map .custom-image-container:active::before,
.sf-metadata-view-map .custom-image-container:active .custom-image-number::before,
.sf-metadata-view-map .custom-image-number:active::before,
.sf-metadata-view-map .custom-image-number:active .custom-image-container::before {
content: '';
position: absolute;
bottom: 5px;
transform: translate( -50%, 100%);
left: 50%;
color: #fff;
display: inline-block;
line-height: 16px;
height: 16px;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
border-radius: 6px;
}
.sf-metadata-view-map .custom-image-container .image-overlay-arrow {
bottom: 5px;
color: #fff;
display: inline-block;
height: 16px;
left: 50%;
line-height: 16px;
position: absolute;
transform: translate(-50%, 100%);
.sf-metadata-view-map .custom-image-container:active .custom-image-number::before,
.sf-metadata-view-map .custom-image-number:active::before {
border-radius: 50%;
}
.sf-metadata-view-map .custom-image-container::after {
@@ -66,14 +71,21 @@
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid #fff;
border-radius: 2px;
}
.sf-metadata-view-map .sf-BMap-geolocation-control {
background-color: #ffffff;
box-shadow: 0 0 4px rgb(0 0 0 / 12%);
.sf-metadata-view-map .custom-image-container:active::after,
.sf-metadata-view-map .custom-image-number:active .custom-image-container::after {
border-top: 10px solid rgba(0, 0, 0, 0.4);
}
.sf-metadata-view-map .sf-BMap-geolocation-control,
.sf-metadata-view-map .sf-BMap-zoom-control {
background-color: #fff;
opacity: 1;
overflow: hidden;
border-radius: 6px;
text-align: center;
color: #212529;
box-shadow: -2px -2px 4px 2px rgba(0, 0, 0, 0.1);
}
.sf-metadata-view-map .sf-BMap-geolocation-control-loading {
@@ -81,24 +93,17 @@
}
.sf-metadata-view-map .sf-BMap-geolocation-control:hover,
.sf-metadata-view-map .sf-BMap-zoom-button:hover {
.sf-metadata-view-map .sf-BMap-zoom-button:not(.disabled):hover {
background-color: #f5f5f5;
cursor: pointer;
}
.sf-metadata-view-map .sf-BMap-zoom-control {
background-color: #fff;
opacity: 1;
overflow: hidden;
border-radius: 6px;
box-shadow: 2px 2px 4px 2px rgba(0, 0, 0, 0.1);
}
.sf-metadata-view-map .sf-BMap-zoom-button {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
color: #666;
background-color: #fff;
border: none;
overflow: hidden;
@@ -109,15 +114,17 @@
.sf-metadata-view-map .sf-BMap-zoom-button .zoom-out-icon {
width: 18px;
height: 18px;
fill: #666;
fill: currentColor;
}
.sf-metadata-view-map .sf-BMap-zoom-button:hover .zoom-in-icon,
.sf-metadata-view-map .sf-BMap-zoom-button:hover .zoom-out-icon {
fill: #212529;
.sf-metadata-view-map .sf-BMap-zoom-button:not(:disabled):active:focus {
box-shadow: none;
}
.sf-metadata-view-map .sf-BMap-zoom-button:disabled .zoom-in-icon,
.sf-metadata-view-map .sf-BMap-zoom-button:disabled .zoom-out-icon {
fill: #ccc;
.sf-metadata-view-map .sf-BMap-zoom-button:hover {
color: #212529;
}
.sf-metadata-view-map .sf-BMap-zoom-button:disabled {
color: #ccc !important;
}

View File

@@ -1,38 +1,21 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { CenteredLoading } from '@seafile/sf-metadata-ui-component';
import loadBMap, { initMapInfo } from '../../../utils/map-utils';
import { wgs84_to_gcj02, gcj02_to_bd09 } from '../../../utils/coord-transform';
import { isValidPosition } from '../../utils/validate';
import { MAP_TYPE as MAP_PROVIDER } from '../../../constants';
import { appAvatarURL, baiduMapKey, gettext, googleMapKey, mediaUrl, siteRoot, thumbnailSizeForGrid } from '../../../utils/constants';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { getFileNameFromRecord, getFileTypeFromRecord, getImageLocationFromRecord, getParentDirFromRecord, getRecordIdFromRecord } from '../../utils/cell';
import ClusterPhotos from './cluster-photos';
import Main from './main';
import { EVENT_BUS_TYPE, PREDEFINED_FILE_TYPE_OPTION_KEY } from '../../constants';
import { useMetadataView } from '../../hooks/metadata-view';
import { EVENT_BUS_TYPE, PREDEFINED_FILE_TYPE_OPTION_KEY, MAP_TYPE } from '../../constants';
import { getRecordIdFromRecord, getFileNameFromRecord, getImageLocationFromRecord, getParentDirFromRecord,
getFileTypeFromRecord
} from '../../utils/cell';
import { Utils } from '../../../utils/utils';
import customImageOverlay from './custom-image-overlay';
import customAvatarOverlay from './custom-avatar-overlay';
import { createBMapGeolocationControl } from './geolocation-control';
import toaster from '../../../components/toast';
import { siteRoot, thumbnailSizeForGrid } from '../../../utils/constants';
import { isValidPosition } from '../../utils/validate';
import { gcj02_to_bd09, wgs84_to_gcj02 } from '../../../utils/coord-transform';
import './index.css';
import { createBMapZoomControl } from './zoom-control';
const DEFAULT_POSITION = { lng: 104.195, lat: 35.861 };
const DEFAULT_ZOOM = 4;
const BATCH_SIZE = 500;
const Map = () => {
const [isLoading, setIsLoading] = useState(true);
const [showGallery, setShowGallery] = useState(false);
const [markerIds, setMarkerIds] = useState([]);
const { metadata, store } = useMetadataView();
const mapRef = useRef(null);
const clusterRef = useRef(null);
const batchIndexRef = useRef(0);
const { metadata } = useMetadataView();
const mapInfo = useMemo(() => initMapInfo({ baiduMapKey, googleMapKey }), []);
const repoID = window.sfMetadataContext.getSetting('repoID');
const validImages = useMemo(() => {
@@ -54,150 +37,36 @@ const Map = () => {
return { id, src, lng: bdPosition.lng, lat: bdPosition.lat };
})
.filter(Boolean);
}, [repoID, metadata]);
}, [repoID, metadata.rows]);
const addMapController = useCallback(() => {
const ZoomControl = createBMapZoomControl(window.BMap);
const zoomControl = new ZoomControl();
const GeolocationControl = createBMapGeolocationControl(window.BMap, (err, point) => {
if (!err && point) {
mapRef.current.setCenter({ lng: point.lng, lat: point.lat });
}
});
const openGallery = useCallback((cluster_marker_ids) => {
setMarkerIds(cluster_marker_ids);
setShowGallery(true);
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_MAP_VIEW_TOOLBAR, true);
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MAP_VIEW, metadata.view);
}, [metadata.view]);
const geolocationControl = new GeolocationControl();
mapRef.current.addControl(zoomControl);
mapRef.current.addControl(geolocationControl);
const closeGallery = useCallback(() => {
setShowGallery(false);
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_MAP_VIEW_TOOLBAR, false);
}, []);
const renderMarkersBatch = useCallback(() => {
if (!validImages.length || !clusterRef.current) return;
const startIndex = batchIndexRef.current * BATCH_SIZE;
const endIndex = Math.min(startIndex + BATCH_SIZE, validImages.length);
const batchMarkers = [];
for (let i = startIndex; i < endIndex; i++) {
const image = validImages[i];
const { lng, lat } = image;
const point = new window.BMap.Point(lng, lat);
const marker = customImageOverlay(point, image.src);
batchMarkers.push(marker);
}
clusterRef.current.addMarkers(batchMarkers);
if (endIndex < validImages.length) {
batchIndexRef.current += 1;
setTimeout(renderMarkersBatch, 20); // Schedule the next batch
}
}, [validImages]);
const initializeClusterer = useCallback(() => {
if (mapRef.current && !clusterRef.current) {
clusterRef.current = new window.BMapLib.MarkerClusterer(mapRef.current);
}
}, []);
const initializeUserMarker = useCallback(() => {
if (!window.BMap) return;
const imageUrl = `${mediaUrl}/img/marker.png`;
const addMarker = (lng, lat) => {
const gcPosition = wgs84_to_gcj02(lng, lat);
const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat);
const point = new window.BMap.Point(bdPosition.lng, bdPosition.lat);
const avatarMarker = customAvatarOverlay(point, appAvatarURL, imageUrl);
mapRef.current.addOverlay(avatarMarker);
};
if (!navigator.geolocation) {
addMarker(DEFAULT_POSITION.lng, DEFAULT_POSITION.lat);
return;
}
navigator.geolocation.getCurrentPosition(
position => addMarker(position.coords.longitude, position.coords.latitude),
() => {
addMarker(DEFAULT_POSITION.lng, DEFAULT_POSITION.lat);
toaster.danger(gettext('Failed to get user location'));
}
);
}, []);
const getMapType = useCallback((type) => {
if (!mapRef.current) return;
switch (type) {
case MAP_TYPE.SATELLITE:
return window.BMAP_SATELLITE_MAP;
default:
return window.BMAP_NORMAL_MAP;
}
}, []);
const renderBaiduMap = useCallback(() => {
setIsLoading(false);
if (!window.BMap.Map) return;
let mapCenter = window.sfMetadataContext.localStorage.getItem('map-center') || DEFAULT_POSITION;
// ask for user location
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition((userInfo) => {
mapCenter = { lng: userInfo.coords.longitude, lat: userInfo.coords.latitude };
window.sfMetadataContext.localStorage.setItem('map-center', mapCenter);
});
}
if (!isValidPosition(mapCenter?.lng, mapCenter?.lat)) return;
const gcPosition = wgs84_to_gcj02(mapCenter.lng, mapCenter.lat);
const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat);
const { lng, lat } = bdPosition;
mapRef.current = new window.BMap.Map('sf-metadata-map-container', { enableMapClick: false });
const point = new window.BMap.Point(lng, lat);
mapRef.current.centerAndZoom(point, DEFAULT_ZOOM);
mapRef.current.enableScrollWheelZoom(true);
// const type = window.sfMetadataContext.localStorage.getItem('map-type');
// mapRef.current.setMapType(getMapType(type));
addMapController();
initializeUserMarker();
initializeClusterer();
batchIndexRef.current = 0; // Reset batch index
renderMarkersBatch();
}, [addMapController, initializeClusterer, initializeUserMarker, renderMarkersBatch]);
const onDeleteLocationPhotos = useCallback((ids) => {
store.deleteLocationPhotos(ids);
}, [store]);
useEffect(() => {
const switchMapTypeSubscribe = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.SWITCH_MAP_TYPE, (newType) => {
window.sfMetadataContext.localStorage.setItem('map-type', newType);
mapRef.current && mapRef.current.setMapType(getMapType(newType));
});
return () => {
switchMapTypeSubscribe();
};
}, [getMapType]);
useEffect(() => {
if (mapInfo.type === MAP_PROVIDER.B_MAP) {
window.renderMap = renderBaiduMap;
loadBMap(mapInfo.key).then(() => renderBaiduMap());
return () => {
window.renderMap = null;
};
}
return;
}, [mapInfo, renderBaiduMap]);
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MAP_VIEW, metadata.view);
}, [metadata.view]);
return (
<div className="w-100 h-100 sf-metadata-view-map">
{isLoading ? (
<CenteredLoading />
<>
{showGallery ? (
<ClusterPhotos view={metadata.view} markerIds={markerIds} onClose={closeGallery} onDelete={onDeleteLocationPhotos} />
) : (
<div className="sf-metadata-map-container" ref={mapRef} id="sf-metadata-map-container"></div>
<Main validImages={validImages} onOpen={openGallery} />
)}
</div>
</>
);
};

View File

@@ -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 (
<div className="sf-metadata-view-map">
<div className="sf-metadata-map-container" ref={mapRef} id="sf-metadata-map-container"></div>
</div>
);
};
Main.propTypes = {
validImages: PropTypes.array,
onOpen: PropTypes.func,
};
export default Main;

View File

@@ -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 = `<img src=${this._imageUrl} width="72" height="72" />`;
const imageElement = `<img src=${this._imageUrl} />`;
const htmlString =
`
<div class="custom-image-container">
${this._imageUrl ? imageElement : '<div class="empty-custom-image-wrapper"></div>'}
<i class='sf3-font image-overlay-arrow'></i>
</div>
`;
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;

View File

@@ -0,0 +1,7 @@
import customAvatarOverlay from './custom-avatar-overlay';
import customImageOverlay from './custom-image-overlay';
export {
customAvatarOverlay,
customImageOverlay
};

View File

@@ -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);
}
});
};

View File

@@ -775,6 +775,7 @@ var BMapLib = window.BMapLib = BMapLib || {};
this._map = map;
this._domElement = document.createElement('div');
this._domElement.className = 'custom-image-overlay';
// this._updateCss();
// this._updateText();
this._updatePosition();