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

Feature/add map view (#7034)

* add map

* use custom baidu js

* use custom baidu plugin

* optimize user location

* optimize code

* show tips when request user location failed

* optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

* feat: optimize code

---------

Co-authored-by: zhouwenxuan <aries@Mac.local>
Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
Aries
2024-11-18 17:36:49 +08:00
committed by GitHub
parent 29d2d18a0b
commit 3b384be610
23 changed files with 2331 additions and 81 deletions

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
<style type="text/css">
.st0{fill:#999999;}
</style>
<title>map</title>
<g id="map">
<g id="形状结合" transform="translate(1.000000, 1.000000)">
<path class="st0" d="M29.5,6.8C29.8,7,30,7.4,30,7.8v17.6c0,0.5-0.3,1-0.8,1.2l-8.8,3.3c-0.3,0.1-0.6,0.1-0.8,0L10,26.7l-8.3,3.2
c-0.4,0.1-0.8,0.1-1.2-0.1c-0.3-0.2-0.5-0.6-0.5-1V11.2c0-0.5,0.3-1,0.8-1.2l4-1.5c-0.2,1.1-0.2,2.3,0,3.4l-2,0.9v13.4l5.6-2.4
l0-5.4l2.8,3.5v2.7l7.5,2.5v-5.2l2.8-3.5l0,8l5.6-2.2V10.4l-1.9,0.8c0.1-1.1,0.1-2.2-0.2-3.3l3.2-1.2C28.7,6.5,29.1,6.6,29.5,6.8z
M8.3,2.7c3.7-3.6,9.6-3.6,13.3,0c3.3,3.1,3.7,8,1.1,11.7l-6.2,7.7c-0.1,0.1-0.2,0.2-0.3,0.3c-0.9,0.7-2.1,0.6-2.8-0.3l-6.1-7.6
C4.7,10.8,5.2,5.9,8.3,2.7z M19.4,4.8c-2.4-2.2-6.4-2.2-8.8,0c-2.2,2-2.4,5.1-0.7,7.4l4.1,5.5c0.5,0.7,1.5,0.7,2,0l4.1-5.5
C21.8,9.9,21.6,6.8,19.4,4.8z M15,5c2.2,0,4,1.8,4,4c0,1.4-0.8,2.7-2,3.5s-2.8,0.7-4,0s-2-2-2-3.5C11,6.8,12.8,5,15,5z M15,8
c-0.6,0-1,0.4-1,1s0.4,1,1,1s1-0.4,1-1S15.6,8,15,8z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -19,6 +19,10 @@ const VIEW_OPTIONS = [
{
key: 'kanban',
type: VIEW_TYPE.KANBAN,
},
{
key: 'map',
type: VIEW_TYPE.MAP,
}
];
@@ -49,6 +53,8 @@ const AddView = ({ target, toggle, onOptionClick }) => {
return gettext('Gallery');
case VIEW_TYPE.KANBAN:
return gettext('Kanban');
case VIEW_TYPE.MAP:
return gettext('Map');
default:
return type;
}

View File

@@ -5,6 +5,7 @@ import TableViewToolbar from './table-view-toolbar';
import GalleryViewToolbar from './gallery-view-toolbar';
import FaceRecognitionViewToolbar from './face-recognition';
import KanbanViewToolBar from './kanban-view-toolbar';
import MapViewToolBar from './map-view-toolbar';
import './index.css';
@@ -110,6 +111,14 @@ const ViewToolBar = ({ viewId, isCustomPermission, showDetail, closeDetail }) =>
closeDetail={closeDetail}
/>
)}
{viewType === VIEW_TYPE.MAP && (
<MapViewToolBar
readOnly={readOnly}
view={view}
collaborators={collaborators}
modifyFilters={modifyFilters}
/>
)}
</div>
);
};

View File

@@ -0,0 +1,52 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { PRIVATE_COLUMN_KEY } from '../../../constants';
import { FilterSetter } from '../../data-process-setter';
const MapViewToolBar = ({
readOnly,
view,
collaborators,
modifyFilters,
}) => {
const viewType = useMemo(() => view.type, [view]);
const viewColumns = useMemo(() => {
if (!view) return [];
return view.columns;
}, [view]);
const filterColumns = useMemo(() => {
return viewColumns.filter(c => c.key !== PRIVATE_COLUMN_KEY.FILE_TYPE);
}, [viewColumns]);
return (
<>
<div className="sf-metadata-tool-left-operations">
<FilterSetter
isNeedSubmit={true}
wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-filter"
filtersClassName="sf-metadata-filters"
target="sf-metadata-filter-popover"
readOnly={readOnly}
filterConjunction={view.filter_conjunction}
basicFilters={view.basic_filters}
filters={view.filters}
columns={filterColumns}
modifyFilters={modifyFilters}
collaborators={collaborators}
viewType={viewType}
/>
</div>
<div className="sf-metadata-tool-right-operations"></div>
</>
);
};
MapViewToolBar.propTypes = {
readOnly: PropTypes.bool,
view: PropTypes.object,
collaborators: PropTypes.array,
modifyFilters: PropTypes.func,
};
export default MapViewToolBar;

View File

@@ -9,6 +9,7 @@ export const VIEW_TYPE = {
GALLERY: 'gallery',
FACE_RECOGNITION: 'face_recognition',
KANBAN: 'kanban',
MAP: 'map',
};
export const FACE_RECOGNITION_VIEW_ID = '_face_recognition';
@@ -18,6 +19,7 @@ export const VIEW_TYPE_ICON = {
[VIEW_TYPE.GALLERY]: 'image',
[VIEW_TYPE.FACE_RECOGNITION]: 'face-recognition-view',
[VIEW_TYPE.KANBAN]: 'kanban',
[VIEW_TYPE.MAP]: 'map',
'image': 'image'
};
@@ -52,24 +54,38 @@ export const VIEW_TYPE_DEFAULT_BASIC_FILTER = {
filter_term: []
},
],
[VIEW_TYPE.MAP]: [
{
column_key: PRIVATE_COLUMN_KEY.IS_DIR,
filter_predicate: FILTER_PREDICATE_TYPE.IS,
filter_term: 'file'
}, {
column_key: PRIVATE_COLUMN_KEY.FILE_TYPE,
filter_predicate: FILTER_PREDICATE_TYPE.IS_ANY_OF,
filter_term: ['_picture']
},
],
};
export const VIEW_TYPE_DEFAULT_SORTS = {
[VIEW_TYPE.TABLE]: [],
[VIEW_TYPE.GALLERY]: [{ column_key: PRIVATE_COLUMN_KEY.FILE_CTIME, sort_type: SORT_TYPE.DOWN }],
[VIEW_TYPE.KANBAN]: [],
[VIEW_TYPE.MAP]: [],
};
export const VIEW_SORT_COLUMN_RULES = {
[VIEW_TYPE.TABLE]: (column) => SORT_COLUMN_OPTIONS.includes(column.type),
[VIEW_TYPE.GALLERY]: (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]: () => {},
};
export const VIEW_FIRST_SORT_COLUMN_RULES = {
[VIEW_TYPE.TABLE]: (column) => SORT_COLUMN_OPTIONS.includes(column.type),
[VIEW_TYPE.GALLERY]: (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]: () => {},
};
export const KANBAN_SETTINGS_KEYS = {

View File

@@ -32,6 +32,9 @@ const updateFavicon = (type) => {
case VIEW_TYPE.KANBAN:
favicon.href = `${mediaUrl}favicons/kanban.png`;
break;
case VIEW_TYPE.MAP:
favicon.href = `${mediaUrl}favicons/map.png`;
break;
default:
favicon.href = `${mediaUrl}favicons/favicon.png`;
}

View File

@@ -4,6 +4,7 @@ import Table from '../views/table';
import Gallery from '../views/gallery';
import FaceRecognition from '../views/face-recognition';
import Kanban from '../views/kanban';
import Map from '../views/map';
import { useMetadataView } from '../hooks/metadata-view';
import { gettext } from '../../utils/constants';
import { VIEW_TYPE } from '../constants';
@@ -27,6 +28,9 @@ const View = () => {
case VIEW_TYPE.KANBAN: {
return <Kanban />;
}
case VIEW_TYPE.MAP: {
return <Map />;
}
default:
return null;
}

View File

@@ -7,9 +7,9 @@ import { getRowsByIds } from '../utils/table';
import { isGroupView } from '../utils/view';
import { username } from '../../utils/constants';
import { COLUMN_DATA_OPERATION_TYPE, OPERATION_TYPE } from './operations';
import { CellType, PRIVATE_COLUMN_KEY } from '../constants';
import { CellType } from '../constants';
import { getCellValueByColumn, getOption, isValidCellValue, checkIsPredefinedOption, getColumnOptionIdsByNames,
getColumnOptionNamesByIds
getColumnOptionNamesByIds, geRecordIdFromRecord,
} from '../utils/cell';
// const DEFAULT_COMPUTER_PROPERTIES_CONTROLLER = {
@@ -181,7 +181,8 @@ class DataProcessor {
const newOptionNames = getColumnOptionNamesByIds(newColumn, oldOptionIds);
row[columnOriginalName] = newOptionNames ? newOptionNames : null;
}
table.id_row_map[row[PRIVATE_COLUMN_KEY.ID]] = row;
const id = geRecordIdFromRecord(row);
table.id_row_map[id] = row;
}
}
}

View File

@@ -1,10 +1,11 @@
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import { PRIVATE_COLUMN_KEY, UTC_FORMAT_DEFAULT } from '../../constants';
import { UTC_FORMAT_DEFAULT } from '../../constants';
import { OPERATION_TYPE } from './constants';
import Column from '../../model/metadata/column';
import View from '../../model/metadata/view';
import { getColumnOriginName } from '../../utils/column';
import { geRecordIdFromRecord } from '../../utils/cell';
dayjs.extend(utc);
@@ -141,8 +142,9 @@ export default function apply(data, operation) {
let id_row_map = {};
data.rows.forEach(row => {
delete row[columnOriginName];
const id = geRecordIdFromRecord(row);
rows.push(row);
id_row_map[row[PRIVATE_COLUMN_KEY.ID]] = row;
id_row_map[id] = row;
});
data.id_row_map = id_row_map;
delete data.key_column_map[column_key];

View File

@@ -13,40 +13,6 @@ const getAutoTimeDisplayString = (autoTime) => {
return date.format('YYYY-MM-DD HH:mm:ss');
};
export const getClientCellValueDisplayString = (record, column, { collaborators = [] } = {}) => {
const cellValue = getCellValueByColumn(record, column);
const { type } = column;
if (type === CellType.CTIME || type === CellType.MTIME) {
return getAutoTimeDisplayString(cellValue);
}
return getCellValueDisplayString(record, column, { collaborators });
};
export const getFormatRecordData = (columns, recordData) => {
let keyColumnMap = {};
columns.forEach(column => {
keyColumnMap[column.key] = column;
});
return convertedToRecordData(recordData, keyColumnMap);
};
export const getFormattedRecordsData = (recordsData, columns, excludesColumnTypes) => {
let keyColumnMap = {};
columns.forEach(column => {
keyColumnMap[column.key] = column;
});
return recordsData.map(recordData => {
let formattedRecordsData = convertedToRecordData(recordData, keyColumnMap, excludesColumnTypes);
if (recordData._id) {
formattedRecordsData._id = recordData._id;
}
if (Object.prototype.hasOwnProperty.call(recordData, '_archived')) {
formattedRecordsData._archived = recordData._archived ? 'true' : 'false';
}
return formattedRecordsData;
});
};
// { [column.key]: cellValue } -> { [column.name]: cellValue }
// { [option-column.key]: option.id } -> { [option-column.name]: option.name }
function convertedToRecordData(originRecordData, keyColumnMap, excludesColumnTypes = []) {
@@ -80,3 +46,37 @@ function convertedToRecordData(originRecordData, keyColumnMap, excludesColumnTyp
});
return recordData;
}
export const getClientCellValueDisplayString = (record, column, { collaborators = [] } = {}) => {
const cellValue = getCellValueByColumn(record, column);
const { type } = column;
if (type === CellType.CTIME || type === CellType.MTIME) {
return getAutoTimeDisplayString(cellValue);
}
return getCellValueDisplayString(record, column, { collaborators });
};
export const getFormatRecordData = (columns, recordData) => {
let keyColumnMap = {};
columns.forEach(column => {
keyColumnMap[column.key] = column;
});
return convertedToRecordData(recordData, keyColumnMap);
};
export const getFormattedRecordsData = (recordsData, columns, excludesColumnTypes) => {
let keyColumnMap = {};
columns.forEach(column => {
keyColumnMap[column.key] = column;
});
return recordsData.map(recordData => {
let formattedRecordsData = convertedToRecordData(recordData, keyColumnMap, excludesColumnTypes);
if (recordData._id) {
formattedRecordsData._id = recordData._id;
}
if (Object.prototype.hasOwnProperty.call(recordData, '_archived')) {
formattedRecordsData._archived = recordData._archived ? 'true' : 'false';
}
return formattedRecordsData;
});
};

View File

@@ -39,3 +39,11 @@ export const geRecordIdFromRecord = record => {
export const getFileObjIdFromRecord = record => {
return record ? record[PRIVATE_COLUMN_KEY.OBJ_ID] : '';
};
export const getImageLocationFromRecord = (record) => {
return record ? record[PRIVATE_COLUMN_KEY.LOCATION] : null;
};
export const getFileTypeFromRecord = (record) => {
return record ? record[PRIVATE_COLUMN_KEY.FILE_TYPE] : null;
};

View File

@@ -1,42 +1,5 @@
export {
isValidCellValue,
getCellValueByColumn,
getParentDirFromRecord,
getFileNameFromRecord,
geRecordIdFromRecord,
getFileObjIdFromRecord,
} from './core';
export {
getCellValueDisplayString,
getCellValueStringResult,
} from './common';
export {
getDateDisplayString,
getPrecisionNumber,
getNumberDisplayString,
replaceNumberNotAllowInput,
formatStringToNumber,
formatTextToNumber,
checkIsPredefinedOption,
getOption,
getColumnOptionNameById,
getOptionName,
getMultipleOptionName,
getCollaborator,
getCollaboratorsNames,
getCollaboratorsName,
getCollaboratorEmailsByNames,
getLongtextDisplayString,
getGeolocationDisplayString,
getGeolocationByGranularity,
getFloatNumber,
getColumnOptionNamesByIds,
getColumnOptionIdsByNames,
decimalToExposureTime,
} from './column';
export { isCellValueChanged } from './cell-comparer';
export { getClientCellValueDisplayString } from './cell-format-utils';
export * from './core';
export * from './common';
export * from './column';
export * from './cell-comparer';
export * from './cell-format-utils';

View File

@@ -0,0 +1,46 @@
const customAvatarOverlay = (point, avatarUrl, bgUrl, width = 20, height = 25) => {
class AvatarOverlay extends window.BMap.Overlay {
constructor(point, avatarUrl, bgUrl, width, height) {
super();
this._point = point;
this._headerImg = avatarUrl;
this._bgUrl = bgUrl;
this._width = width;
this._height = height;
}
initialize(map) {
this._map = map;
const divBox = document.createElement('div');
const divImg = new Image();
divBox.style.position = 'absolute';
divBox.style.width = `${this._width}px`;
divBox.style.height = `${this._height}px`;
divBox.style.backgroundImage = `url(${this._bgUrl})`;
divBox.style.backgroundPosition = '.5px 0px';
divBox.style.display = 'flex';
divBox.style.padding = '2px 2.5px 0 2px';
divImg.src = this._headerImg;
divImg.style.width = '16px';
divImg.style.height = '16px';
divImg.style.borderRadius = '50%';
divImg.style.display = 'block';
divBox.appendChild(divImg);
map.getPanes().markerPane.appendChild(divBox);
this._div = divBox;
return divBox;
}
draw() {
const position = this._map.pointToOverlayPixel(this._point);
this._div.style.left = `${position.x - this._width / 2}px`;
this._div.style.top = `${position.y - (this._height * 7) / 10}px`;
}
}
return new AvatarOverlay(point, avatarUrl, bgUrl, width, height);
};
export default customAvatarOverlay;

View File

@@ -0,0 +1,69 @@
import { Utils } from '../../../utils/utils';
const customImageOverlay = (center, imageUrl) => {
class ImageOverlay extends window.BMap.Overlay {
constructor(center, imageUrl) {
super();
this._center = center;
this._imageUrl = imageUrl;
}
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 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');
const label = labelDocument.body.firstElementChild;
this._div.append(label);
const eventHandler = (event) => {
event.stopPropagation();
event.preventDefault();
};
if (Utils.isDesktop()) {
this._div.addEventListener('click', eventHandler);
} else {
this._div.addEventListener('touchend', eventHandler);
}
return div;
}
draw() {
const position = this._map.pointToOverlayPixel(this._center);
this._div.style.left = position.x - 40 + 'px'; // 40 is 1/2 container height
this._div.style.top = position.y - 88 + 'px'; // 80 is container height and 8 is icon height
}
getImageUrl() {
return imageUrl || '';
}
getPosition() {
return center;
}
getMap() {
return this._map || null;
}
}
return new ImageOverlay(center, imageUrl);
};
export default customImageOverlay;

View File

@@ -0,0 +1,50 @@
import { mediaUrl } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
export function createBMapGeolocationControl(BMap, callback) {
function GeolocationControl() {
this.defaultAnchor = window.BMAP_ANCHOR_BOTTOM_RIGHT;
this.defaultOffset = new BMap.Size(10, Utils.isDesktop() ? 20 : 90);
}
GeolocationControl.prototype = new window.BMap.Control();
GeolocationControl.prototype.initialize = function (map) {
const div = document.createElement('div');
div.className = 'sf-BMap-geolocation-control';
div.style = 'display: flex; justify-content: center; align-items: center;';
const icon = document.createElement('img');
icon.className = 'sf-BMap-icon-current-location';
icon.src = `${mediaUrl}/img/current-location.svg`;
icon.style = 'width: 16px; height: 16px; display: block;';
div.appendChild(icon);
if (Utils.isDesktop()) {
setNodeStyle(div, 'height: 30px; width: 30px; line-height: 30px');
} else {
setNodeStyle(div, 'height: 35px; width: 35px; line-height: 35px; opacity: 0.75');
}
div.onclick = (e) => {
e.preventDefault();
const geolocation = new BMap.Geolocation();
div.className = 'sf-BMap-geolocation-control sf-BMap-geolocation-control-loading';
geolocation.getCurrentPosition((result) => {
div.className = 'sf-BMap-geolocation-control';
if (result) {
const point = result.point;
map.setCenter(point);
callback(null, point);
} else {
// Positioning failed
callback(true);
}
});
};
map.getContainer().appendChild(div);
return div;
};
return GeolocationControl;
}
function setNodeStyle(dom, styleText) {
dom.style.cssText += styleText;
}

View File

@@ -0,0 +1,85 @@
.sf-metadata-view-map {
display: flex;
flex-direction: column;
}
.sf-metadata-view-map .sf-metadata-map-container {
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%);
position: relative;
}
.sf-metadata-view-map .custom-image-number {
position: absolute;
right: -15px;
top: -8px;
padding: 0 12px;
background: #20AD7E;
color: #fff;
border-radius: 10px;
text-align: center;
font-size: 14px;
line-height: 20px;
}
.sf-metadata-view-map .custom-image-container .plugin-label-arrow {
position: absolute;
bottom: 5px;
transform: translate( -50%, 100%);
left: 50%;
color: #fff;
display: inline-block;
line-height: 16px;
height: 16px;
}
.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::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;
}
.sf-metadata-view-map .sf-BMap-geolocation-control {
background-color: #ffffff;
box-shadow: 0 0 4px rgb(0 0 0 / 12%);
border-radius: 4px;
text-align: center;
color: #212529;
}
.sf-metadata-view-map .sf-BMap-geolocation-control-loading {
opacity: 0.7;
}
.sf-metadata-view-map .sf-BMap-geolocation-control:hover {
background-color: #f5f5f5;
}

View File

@@ -0,0 +1,171 @@
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 { MAP_TYPE } from '../../../constants';
import { isValidPosition } from '../../utils/validate';
import { appAvatarURL, baiduMapKey, gettext, googleMapKey, mediaUrl, siteRoot, thumbnailSizeForGrid } from '../../../utils/constants';
import { useMetadataView } from '../../hooks/metadata-view';
import { PREDEFINED_FILE_TYPE_OPTION_KEY } from '../../constants';
import { geRecordIdFromRecord, 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 './index.css';
const DEFAULT_POSITION = { lng: 104.195, lat: 35.861 };
const DEFAULT_ZOOM = 4;
const BATCH_SIZE = 500;
const Map = () => {
const [isLoading, setIsLoading] = useState(true);
const mapRef = useRef(null);
const clusterRef = useRef(null);
const batchIndexRef = useRef(0);
const { metadata } = useMetadataView();
const mapInfo = useMemo(() => initMapInfo({ baiduMapKey, googleMapKey }), []);
const repoID = window.sfMetadataContext.getSetting('repoID');
const validImages = useMemo(() => {
return metadata.rows
.map(record => {
const recordType = getFileTypeFromRecord(record);
if (recordType !== PREDEFINED_FILE_TYPE_OPTION_KEY.PICTURE) return null;
const id = geRecordIdFromRecord(record);
const fileName = getFileNameFromRecord(record);
const parentDir = getParentDirFromRecord(record);
const path = Utils.encodePath(Utils.joinPath(parentDir, fileName));
const src = `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForGrid}${path}`;
const location = getImageLocationFromRecord(record);
if (!location) return null;
const { lng, lat } = location;
return isValidPosition(lng, lat) ? { id, src, lng, lat } : null;
})
.filter(Boolean);
}, [repoID, metadata]);
const addMapController = useCallback(() => {
var navigation = new window.BMap.NavigationControl();
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(geolocationControl);
mapRef.current.addControl(navigation);
}, []);
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 point = new window.BMap.Point(lng, 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 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);
addMapController();
initializeUserMarker();
initializeClusterer();
batchIndexRef.current = 0; // Reset batch index
renderMarkersBatch();
}, [addMapController, initializeClusterer, initializeUserMarker, renderMarkersBatch]);
useEffect(() => {
if (mapInfo.type === MAP_TYPE.B_MAP) {
window.renderMap = renderBaiduMap;
loadBMap(mapInfo.key).then(() => renderBaiduMap());
return () => {
window.renderMap = null;
};
}
return;
}, [mapInfo, renderBaiduMap]);
return (
<div className="w-100 h-100 sf-metadata-view-map">
{isLoading ? (
<CenteredLoading />
) : (
<div className="sf-metadata-map-container" ref={mapRef} id="sf-metadata-map-container"></div>
)}
</div>
);
};
export default Map;

View File

@@ -1,4 +1,5 @@
import { MAP_TYPE } from '../constants';
import { mediaUrl } from './constants';
export const initMapInfo = ({ baiduMapKey, googleMapKey, mineMapKey }) => {
if (baiduMapKey) return { type: MAP_TYPE.B_MAP, key: baiduMapKey };
@@ -25,3 +26,41 @@ export const loadMapSource = (type, key, callback) => {
}
callback && callback();
};
export default function loadBMap(ak) {
return new Promise((resolve, reject) => {
asyncLoadBaiduJs(ak)
.then(() => asyncLoadJs(`${mediaUrl}/js/map/text-icon-overlay.js`))
.then(() => asyncLoadJs(`${mediaUrl}/js/map/marker-clusterer.js`))
.then(() => resolve(true))
.catch((err) => reject(err));
});
}
export function asyncLoadBaiduJs(ak) {
return new Promise((resolve, reject) => {
if (typeof window.BMap !== 'undefined') {
resolve(window.BMap);
return;
}
window.renderMap = function () {
resolve(window.BMap);
};
let script = document.createElement('script');
script.type = 'text/javascript';
script.src = `https://api.map.baidu.com/api?v=3.0&ak=${ak}&callback=renderMap`;
script.onerror = reject;
document.body.appendChild(script);
});
}
export function asyncLoadJs(url) {
return new Promise((resolve, reject) => {
let script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
document.body.appendChild(script);
script.onload = resolve;
script.onerror = reject;
});
}