1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-24 12:58:34 +00:00

Feature/google map view (#7828)

* custom overlay

* google map clusterer

* google controls

* update google map in details

* fix bug - image dialog does not work after add filters

---------

Co-authored-by: zhouwenxuan <aries@Mac.local>
This commit is contained in:
Aries
2025-05-23 10:01:33 +08:00
committed by GitHub
parent 1fd5df190d
commit 29c56a3daf
14 changed files with 580 additions and 215 deletions

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import Icon from '../../icon'; import Icon from '../../icon';
import TextTranslation from '../../../utils/text-translation'; import TextTranslation from '../../../utils/text-translation';
import { baiduMapKey, gettext } from '../../../utils/constants'; import { baiduMapKey, gettext, googleMapKey } from '../../../utils/constants';
import { VIEW_TYPE, VIEW_TYPE_ICON } from '../../../metadata/constants'; import { VIEW_TYPE, VIEW_TYPE_ICON } from '../../../metadata/constants';
export const KEY_ADD_VIEW_MAP = { export const KEY_ADD_VIEW_MAP = {
@@ -46,7 +46,7 @@ export const getNewViewSubMenu = () => {
const options = [...ADD_VIEW_OPTIONS]; const options = [...ADD_VIEW_OPTIONS];
const hasMapOption = options.some(opt => opt.type === VIEW_TYPE.MAP); const hasMapOption = options.some(opt => opt.type === VIEW_TYPE.MAP);
if (!hasMapOption && baiduMapKey) { if (!hasMapOption && (baiduMapKey || googleMapKey)) {
options.push({ options.push({
key: KEY_ADD_VIEW_MAP.ADD_MAP, key: KEY_ADD_VIEW_MAP.ADD_MAP,
type: VIEW_TYPE.MAP, type: VIEW_TYPE.MAP,

View File

@@ -1,12 +1,41 @@
import classnames from 'classnames'; import classnames from 'classnames';
import { Utils } from '../../../utils/utils'; import { Utils } from '../../../utils/utils';
import { wgs84_to_gcj02 } from '../../../utils/coord-transform';
export function createBMapGeolocationControl(BMapGL, callback) { export const createGeolocationControl = (map) => {
function GeolocationControl() { const container = document.createElement('div');
this.defaultAnchor = window.BMAP_ANCHOR_BOTTOM_RIGHT; container.className = classnames(
this.defaultOffset = new BMapGL.Size(30, Utils.isDesktop() ? 30 : 90); 'sf-map-control-container sf-map-geolocation-control-container d-flex align-items-center justify-content-center',
{ 'sf-map-geolocation-control-mobile': !Utils.isDesktop() }
);
const button = document.createElement('div');
button.className = 'sf-map-control sf-map-geolocation-control d-flex align-items-center justify-content-center';
button.innerHTML = '<i class="sf-map-control-icon sf3-font sf3-font-current-location"></i>';
container.appendChild(button);
container.addEventListener('click', async (e) => {
e.preventDefault();
const originalClass = container.className;
container.className = classnames(originalClass, 'sf-map-control-loading');
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition((userInfo) => {
const gcPosition = wgs84_to_gcj02(userInfo.coords.longitude, userInfo.coords.latitude);
map.setCenter(gcPosition);
});
} }
GeolocationControl.prototype = new BMapGL.Control(); });
return container;
};
export function createBMapGeolocationControl({ anchor, offset, callback }) {
function GeolocationControl() {
this.defaultAnchor = anchor || window.BMAP_ANCHOR_BOTTOM_RIGHT;
this.defaultOffset = new window.BMapGL.Size(offset?.x || 30, offset?.y || 90);
}
GeolocationControl.prototype = new window.BMapGL.Control();
GeolocationControl.prototype.initialize = function (map) { GeolocationControl.prototype.initialize = function (map) {
const div = document.createElement('div'); const div = document.createElement('div');
let className = classnames('sf-map-control-container sf-map-geolocation-control-container d-flex align-items-center justify-content-center', { let className = classnames('sf-map-control-container sf-map-geolocation-control-container d-flex align-items-center justify-content-center', {
@@ -21,7 +50,7 @@ export function createBMapGeolocationControl(BMapGL, callback) {
div.className = className; div.className = className;
div.onclick = (e) => { div.onclick = (e) => {
e.preventDefault(); e.preventDefault();
const geolocation = new BMapGL.Geolocation(); const geolocation = new window.BMapGL.Geolocation();
div.className = classnames(className, 'sf-map-control-loading'); div.className = classnames(className, 'sf-map-control-loading');
geolocation.getCurrentPosition((result) => { geolocation.getCurrentPosition((result) => {
div.className = className; div.className = className;

View File

@@ -37,11 +37,21 @@
background-color: #ccc; background-color: #ccc;
} }
.sf-map-control-container.sf-map-geolocation-control-container {
right: 30px !important;
bottom: 30px !important;
}
.sf-map-control-container .sf-map-geolocation-control { .sf-map-control-container .sf-map-geolocation-control {
width: 30px; width: 30px;
line-height: 30px; line-height: 30px;
} }
.sf-map-control-container.sf-map-zoom-control-container {
right: 66px !important;
bottom: 30px !important;
}
.sf-map-control-container .sf-map-zoom-control { .sf-map-control-container .sf-map-zoom-control {
width: 40px; width: 40px;
} }
@@ -64,3 +74,12 @@
.sf-map-control-container .sf-map-control.disabled { .sf-map-control-container .sf-map-control.disabled {
color: #ccc; color: #ccc;
} }
@media (max-width: 768px) {
.sf-map-control-container.sf-map-geolocation-control-container {
bottom: 90px !important;
}
.sf-map-control-container.sf-map-zoom-control-container {
bottom: 90px !important;
}
}

View File

@@ -1,56 +1,97 @@
import classnames from 'classnames'; import classnames from 'classnames';
import { Utils } from '../../../utils/utils'; import { Utils } from '../../../utils/utils';
import { MIN_ZOOM, MAX_ZOOM } from '../../constants/view/map';
export function createBMapZoomControl(BMapGL, { maxZoom, minZoom, offset }, callback) {
function ZoomControl() {
this.defaultAnchor = window.BMAP_ANCHOR_BOTTOM_RIGHT;
this.defaultOffset = new BMapGL.Size(offset.x, offset.y);
}
ZoomControl.prototype = new BMapGL.Control();
ZoomControl.prototype.initialize = function (map) {
const zoomLevel = map.getZoom();
const div = document.createElement('div');
div.className = classnames('sf-map-control-container sf-map-zoom-control-container d-flex align-items-center justify-content-center', {
'sf-map-control-container-mobile': !Utils.isDesktop()
});
const buttonClassName = 'sf-map-control sf-map-zoom-control d-flex align-items-center justify-content-center'; const buttonClassName = 'sf-map-control sf-map-zoom-control d-flex align-items-center justify-content-center';
const zoomInButton = document.createElement('div');
zoomInButton.className = classnames(buttonClassName, { 'disabled': zoomLevel >= maxZoom });
zoomInButton.innerHTML = '<i class="sf-map-control-icon sf3-font sf3-font-zoom-in"></i>';
div.appendChild(zoomInButton);
const createZoomContainer = () => {
const container = document.createElement('div');
container.className = classnames(
'sf-map-control-container sf-map-zoom-control-container d-flex align-items-center justify-content-center',
{ 'sf-map-control-container-mobile': !Utils.isDesktop() }
);
return container;
};
const createButton = (innerHTML) => {
const button = document.createElement('div');
button.className = 'sf-map-control sf-map-zoom-control d-flex align-items-center justify-content-center';
button.innerHTML = innerHTML;
return button;
};
const createDivider = () => {
const divider = document.createElement('div'); const divider = document.createElement('div');
divider.className = 'sf-map-control-divider'; divider.className = 'sf-map-control-divider';
div.appendChild(divider); return divider;
const zoomOutButton = document.createElement('div');
zoomOutButton.className = classnames(buttonClassName, { 'disabled': zoomLevel <= minZoom });
zoomOutButton.innerHTML = '<i class="sf-map-control-icon sf3-font sf3-font-zoom-out"></i>';
div.appendChild(zoomOutButton);
const updateButtonStates = () => {
const zoomLevel = map.getZoom();
zoomInButton.className = classnames(buttonClassName, { 'disabled': zoomLevel >= maxZoom });
zoomOutButton.className = classnames(buttonClassName, { 'disabled': zoomLevel <= minZoom });
callback && callback(zoomLevel);
}; };
const updateButtonStates = (map, zoomIn, zoomOut) => {
const zoomLevel = map.getZoom();
zoomIn.className = classnames(buttonClassName, { 'disabled': zoomLevel >= MAX_ZOOM });
zoomOut.className = classnames(buttonClassName, { 'disabled': zoomLevel <= MIN_ZOOM });
};
export const createZoomControl = (map) => {
const container = createZoomContainer();
const zoomInButton = createButton('<i class="sf-map-control-icon sf3-font sf3-font-zoom-in"></i>');
const divider = createDivider();
const zoomOutButton = createButton('<i class="sf-map-control-icon sf3-font sf3-font-zoom-out"></i>');
container.appendChild(zoomInButton);
container.appendChild(divider);
container.appendChild(zoomOutButton);
zoomInButton.addEventListener('click', (e) => {
e.preventDefault();
const nextZoom = Math.min(map.getZoom() + 1, MAX_ZOOM);
map.setZoom(nextZoom);
});
zoomOutButton.addEventListener('click', (e) => {
e.preventDefault();
const nextZoom = Math.max(map.getZoom() - 1, MIN_ZOOM);
map.setZoom(nextZoom);
});
map.addListener('zoom_changed', () => updateButtonStates(map, zoomInButton, zoomOutButton));
return container;
};
export function createBMapZoomControl(anchor, offset) {
function ZoomControl() {
this.defaultAnchor = anchor || window.BMAP_ANCHOR_BOTTOM_RIGHT;
this.defaultOffset = new window.BMapGL.Size(offset?.x || 66, offset?.y || 30);
}
ZoomControl.prototype = new window.BMapGL.Control();
ZoomControl.prototype.initialize = function (map) {
const container = createZoomContainer();
const zoomInButton = createButton('<i class="sf-map-control-icon sf3-font sf3-font-zoom-in"></i>');
const divider = createDivider();
const zoomOutButton = createButton('<i class="sf-map-control-icon sf3-font sf3-font-zoom-out"></i>');
container.appendChild(zoomInButton);
container.appendChild(divider);
container.appendChild(zoomOutButton);
zoomInButton.onclick = (e) => { zoomInButton.onclick = (e) => {
e.preventDefault(); e.preventDefault();
const nextZoom = map.getZoom() + 1; const nextZoom = map.getZoom() + 1;
map.zoomTo(Math.min(nextZoom, maxZoom)); map.zoomTo(Math.min(nextZoom, MAX_ZOOM));
}; };
zoomOutButton.onclick = (e) => { zoomOutButton.onclick = (e) => {
e.preventDefault(); e.preventDefault();
const nextZoom = map.getZoom() - 1; const nextZoom = map.getZoom() - 1;
map.zoomTo(Math.max(nextZoom, minZoom)); map.zoomTo(Math.max(nextZoom, MIN_ZOOM));
}; };
map.addEventListener('zoomend', updateButtonStates); map.addEventListener('zoomend', () => updateButtonStates(map, zoomInButton, zoomOutButton));
map.getContainer().appendChild(div); map.getContainer().appendChild(container);
return div; return container;
}; };
return ZoomControl; return ZoomControl;

View File

@@ -27,3 +27,8 @@
.dirent-detail-item-value-map .sf-map-control-container .sf-map-control-divider::before { .dirent-detail-item-value-map .sf-map-control-container .sf-map-control-divider::before {
height: 14px; height: 14px;
} }
.dirent-detail-item-value-map .sf-map-control-container.sf-map-zoom-control-container {
right: 10px !important;
bottom: 16px !important;
}

View File

@@ -14,6 +14,7 @@ import { getColumnDisplayName } from '../../../utils/column';
import { createBMapZoomControl } from '../../map-controller'; import { createBMapZoomControl } from '../../map-controller';
import { Utils } from '../../../../utils/utils'; import { Utils } from '../../../../utils/utils';
import { eventBus } from '../../../../components/common/event-bus'; import { eventBus } from '../../../../components/common/event-bus';
import { createZoomControl } from '../../map-controller/zoom';
import './index.css'; import './index.css';
@@ -30,7 +31,6 @@ class Location extends React.Component {
this.mapType = type; this.mapType = type;
this.mapKey = key; this.mapKey = key;
this.map = null; this.map = null;
this.currentPosition = {};
this.state = { this.state = {
address: '', address: '',
isLoading: false, isLoading: false,
@@ -50,12 +50,12 @@ class Location extends React.Component {
const { position, record } = this.props; const { position, record } = this.props;
if (!isValidPosition(position?.lng, position?.lat) || typeof record !== 'object') return; if (!isValidPosition(position?.lng, position?.lat) || typeof record !== 'object') return;
if (prevProps.position?.lng === position?.lng && prevProps.position?.lat === position?.lat) return; if (prevProps.position?.lng === position?.lng && prevProps.position?.lat === position?.lat) return;
this.currentPosition = position; let transformedPos = wgs84_to_gcj02(position.lng, position.lat);
let convertedPos = wgs84_to_gcj02(position.lng, position.lat);
if (this.mapType === MAP_TYPE.B_MAP) { if (this.mapType === MAP_TYPE.B_MAP) {
convertedPos = gcj02_to_bd09(convertedPos.lng, convertedPos.lat); transformedPos = gcj02_to_bd09(transformedPos.lng, transformedPos.lat);
} }
this.addMarkerByPosition(convertedPos.lng, convertedPos.lat); this.addMarkerByPosition(transformedPos.lng, transformedPos.lat);
this.setState({ address: record._location_translated?.address }); this.setState({ address: record._location_translated?.address });
} }
@@ -68,7 +68,6 @@ class Location extends React.Component {
const { position, record } = this.props; const { position, record } = this.props;
if (!isValidPosition(position?.lng, position?.lat) || typeof record !== 'object') return; if (!isValidPosition(position?.lng, position?.lat) || typeof record !== 'object') return;
this.currentPosition = position;
this.setState({ isLoading: true, address: record._location_translated?.address }); this.setState({ isLoading: true, address: record._location_translated?.address });
if (this.mapType === MAP_TYPE.B_MAP) { if (this.mapType === MAP_TYPE.B_MAP) {
@@ -156,11 +155,15 @@ class Location extends React.Component {
scaleControl: false, scaleControl: false,
streetViewControl: false, streetViewControl: false,
rotateControl: false, rotateControl: false,
fullscreenControl: false fullscreenControl: false,
disableDefaultUI: true,
gestureHandling: 'cooperative',
}); });
this.map = window.mapInstance; this.map = window.mapInstance;
this.addMarkerByPosition(lng, lat); this.addMarkerByPosition(lng, lat);
const zoomControl = createZoomControl(this.map);
this.map.controls[window.google.maps.ControlPosition.RIGHT_BOTTOM].push(zoomControl);
this.map.setCenter(gcPosition); this.map.setCenter(gcPosition);
}); });
}; };

View File

@@ -13,3 +13,11 @@ export const MAP_VIEW_TOOLBAR_MODE = {
MAP: 'map', MAP: 'map',
GALLERY: 'gallery', GALLERY: 'gallery',
}; };
export const DEFAULT_POSITION = { lng: 104.195, lat: 35.861 };
export const DEFAULT_ZOOM = 4;
export const MAX_ZOOM = 21;
export const MIN_ZOOM = 3;

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { baiduMapKey, gettext } from '../../utils/constants'; import { baiduMapKey, gettext, googleMapKey } from '../../utils/constants';
import Icon from '../../components/icon'; import Icon from '../../components/icon';
import ItemDropdownMenu from '../../components/dropdown-menu/metadata-item-dropdown-menu'; import ItemDropdownMenu from '../../components/dropdown-menu/metadata-item-dropdown-menu';
import toaster from '../../components/toast'; import toaster from '../../components/toast';
@@ -77,7 +77,7 @@ const ViewItem = ({
const convertableViews = Object.values(VIEW_TYPE).filter(type => const convertableViews = Object.values(VIEW_TYPE).filter(type =>
type !== viewType && type !== viewType &&
type !== VIEW_TYPE.FACE_RECOGNITION && type !== VIEW_TYPE.FACE_RECOGNITION &&
!(type === VIEW_TYPE.MAP && !baiduMapKey) !(type === VIEW_TYPE.MAP && !baiduMapKey && !googleMapKey)
); );
value.push({ value.push({
key: 'turn', key: 'turn',

View File

@@ -0,0 +1,113 @@
import { appAvatarURL, mediaUrl } from '../../../utils/constants';
import { gcj02_to_bd09, wgs84_to_gcj02 } from '../../../utils/coord-transform';
import { Utils } from '../../../utils/utils';
import { createBMapGeolocationControl, createBMapZoomControl } from '../../components/map-controller';
import { MIN_ZOOM, MAX_ZOOM } from '../../constants';
import { customAvatarOverlay, customImageOverlay } from './overlay';
const getPoints = (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,
}
}));
};
export const createBaiduMarkerClusterer = (map, images, onClusterLeaveIds) => {
let clickTimeout = null;
const cluster = new window.Cluster.View(map, {
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),
},
});
cluster.setData(getPoints(images));
cluster.on(window.Cluster.ClusterEvent.CLICK, (element) => {
if (clickTimeout) {
clearTimeout(clickTimeout);
clickTimeout = null;
return;
} else {
clickTimeout = setTimeout(() => {
let imageIds = [];
if (element.isCluster) {
imageIds = cluster.getLeaves(element.id).map(item => item.properties.id).filter(Boolean);
} else {
imageIds = [element.properties.id];
}
clickTimeout = null;
onClusterLeaveIds(imageIds);
}, 300);
}
});
return cluster;
};
export const createBaiduMap = ({ type, center, zoom, onMapState }) => {
if (!window.BMapGL) return;
const map = new window.BMapGL.Map('sf-metadata-map-container', {
enableMapClick: false,
minZoom: MIN_ZOOM,
maxZoom: MAX_ZOOM,
mapType: type,
});
map.centerAndZoom(center, zoom);
map.enableScrollWheelZoom(true);
// add controls
const ZoomControl = createBMapZoomControl({
anchor: window.BMAP_ANCHOR_BOTTOM_RIGHT,
offset: new window.BMapGL.Size(66, Utils.isDesktop() ? 30 : 90),
});
const zoomControl = new ZoomControl();
const GeolocationControl = createBMapGeolocationControl({
anchor: window.BMAP_ANCHOR_BOTTOM_RIGHT,
offset: new window.BMapGL.Size(30, Utils.isDesktop() ? 30 : 90),
callback: (point) => {
const gcPosition = wgs84_to_gcj02(point.lng, point.lat);
const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat);
map.centerAndZoom(new window.BMapGL.Point(bdPosition.lng, bdPosition.lat), map.getZoom());
}
});
const geolocationControl = new GeolocationControl();
map.addControl(zoomControl);
map.addControl(geolocationControl);
map.addEventListener('zoomend', () => onMapState());
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;
const userPosition = new window.BMapGL.Point(lng, lat);
const imageUrl = `${mediaUrl}img/marker.png`;
const avatarMarker = customAvatarOverlay(userPosition, appAvatarURL, imageUrl);
map.addOverlay(avatarMarker);
onMapState();
});
}
return map;
};

View File

@@ -0,0 +1,103 @@
import { appAvatarURL, googleMapId, mediaUrl } from '../../../utils/constants';
import { createGeolocationControl } from '../../components/map-controller/geolocation';
import { createZoomControl } from '../../components/map-controller/zoom';
import { MIN_ZOOM, MAX_ZOOM } from '../../constants';
import { customImageOverlay, googleCustomAvatarOverlay } from './overlay';
let clickTimeout = null;
export const createGoogleMarkerClusterer = (map, images, onClusterLeaveIds) => {
const markers = images.map(image => {
const overlay = customImageOverlay({ isCluster: false, src: image.src });
overlay.addEventListener('click', () => {
if (clickTimeout) {
clearTimeout(clickTimeout);
clickTimeout = null;
const zoom = map.getZoom();
map.setZoom(Math.min(zoom + 1, MAX_ZOOM));
map.setCenter(image.wgs_84);
return;
}
clickTimeout = setTimeout(() => {
onClusterLeaveIds([image.id]);
clickTimeout = null;
}, 300);
});
return new window.google.maps.marker.AdvancedMarkerElement({
position: image.wgs_84,
map,
content: overlay,
});
});
return new window.markerClusterer.MarkerClusterer({
map,
markers,
renderer: {
render: (cluster) => {
const imagesInBounds = images.filter(image => cluster.bounds.contains(image.wgs_84));
const overlay = customImageOverlay({ isCluster: true, reduces: { src: imagesInBounds[0].src }, pointCount: cluster.count });
overlay.addEventListener('click', () => {
if (clickTimeout) {
clearTimeout(clickTimeout);
clickTimeout = null;
const zoom = map.getZoom();
map.setZoom(Math.min(zoom + 1, MAX_ZOOM));
map.setCenter(cluster.position);
return;
}
clickTimeout = setTimeout(() => {
const imagesInBounds = images.filter(image => cluster.bounds.contains(image.wgs_84));
onClusterLeaveIds(imagesInBounds.map(image => image.id));
clickTimeout = null;
}, 300);
});
return new window.google.maps.marker.AdvancedMarkerElement({
position: cluster.position,
content: overlay,
});
}
},
onClusterClick: () => {},
});
};
export const createGoogleMap = ({ center, zoom, onMapState }) => {
if (!window.google?.maps?.Map) return;
const map = new window.google.maps.Map(document.getElementById('sf-metadata-map-container'), {
mapId: googleMapId,
center,
zoom,
mapTypeControl: false,
streetViewControl: false,
fullscreenControl: false,
cameraControl: false,
disableDefaultUI: true,
mapTypeId: window.google.maps.MapTypeId.ROADMAP,
minZoom: MIN_ZOOM,
maxZoom: MAX_ZOOM,
});
const zoomControl = createZoomControl(map);
const geolocationControl = createGeolocationControl(map);
map.controls[window.google.maps.ControlPosition.RIGHT_BOTTOM].push(zoomControl);
map.controls[window.google.maps.ControlPosition.RIGHT_BOTTOM].push(geolocationControl);
map.addListener('center_changed', () => onMapState());
map.addListener('zoom_changed', () => onMapState());
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition((userInfo) => {
const userPosition = { lat: userInfo.coords.latitude, lng: userInfo.coords.longitude };
const imageUrl = `${mediaUrl}img/marker.png`;
googleCustomAvatarOverlay(map, userPosition, appAvatarURL, imageUrl);
onMapState();
});
}
return map;
};

View File

@@ -5,29 +5,25 @@ import { useMetadataView } from '../../hooks/metadata-view';
import { Utils } from '../../../utils/utils'; import { Utils } from '../../../utils/utils';
import { isValidPosition } from '../../utils/validate'; import { isValidPosition } from '../../utils/validate';
import { gcj02_to_bd09, wgs84_to_gcj02 } from '../../../utils/coord-transform'; import { gcj02_to_bd09, wgs84_to_gcj02 } from '../../../utils/coord-transform';
import loadBMap, { initMapInfo } from '../../../utils/map-utils'; import { initMapInfo, loadBMap, loadGMap } from '../../../utils/map-utils';
import { appAvatarURL, baiduMapKey, fileServerRoot, googleMapKey, mediaUrl, siteRoot, thumbnailSizeForGrid, thumbnailSizeForOriginal } from '../../../utils/constants'; import { baiduMapKey, fileServerRoot, googleMapKey, siteRoot, thumbnailSizeForGrid, thumbnailSizeForOriginal } from '../../../utils/constants';
import { MAP_TYPE as MAP_PROVIDER, PRIVATE_FILE_TYPE } from '../../../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 { EVENT_BUS_TYPE, MAP_TYPE, PREDEFINED_FILE_TYPE_OPTION_KEY, STORAGE_MAP_CENTER_KEY, STORAGE_MAP_TYPE_KEY, STORAGE_MAP_ZOOM_KEY, DEFAULT_POSITION, DEFAULT_ZOOM } from '../../constants';
import { createBMapGeolocationControl, createBMapZoomControl } from '../../components/map-controller';
import { customAvatarOverlay, customImageOverlay } from './overlay';
import ModalPortal from '../../../components/modal-portal'; import ModalPortal from '../../../components/modal-portal';
import ImageDialog from '../../../components/dialog/image-dialog'; import ImageDialog from '../../../components/dialog/image-dialog';
import { createGoogleMap, createGoogleMarkerClusterer } from './google';
import { createBaiduMap, createBaiduMarkerClusterer } from './baidu';
import './index.css'; import './index.css';
const DEFAULT_POSITION = { lng: 104.195, lat: 35.861 };
const DEFAULT_ZOOM = 4;
const MAX_ZOOM = 21;
const MIN_ZOOM = 3;
const Map = () => { const Map = () => {
const [imageIndex, setImageIndex] = useState(0); const [imageIndex, setImageIndex] = useState(0);
const [clusterLeaveIds, setClusterLeaveIds] = useState([]); const [clusterLeaveIds, setClusterLeaveIds] = useState([]);
const [center, setCenter] = useState(DEFAULT_POSITION);
const [zoom, setZoom] = useState(DEFAULT_ZOOM);
const mapRef = useRef(null); const mapRef = useRef(null);
const clusterRef = useRef(null); const containerRef = useRef(null);
const clickTimeoutRef = useRef(null);
const { metadata, viewID, updateCurrentPath } = useMetadataView(); const { metadata, viewID, updateCurrentPath } = useMetadataView();
const repoID = window.sfMetadataContext.getSetting('repoID'); const repoID = window.sfMetadataContext.getSetting('repoID');
@@ -69,7 +65,8 @@ const Map = () => {
downloadURL: `${fileServerRoot}repos/${repoID}/files${path}?op=download`, downloadURL: `${fileServerRoot}repos/${repoID}/files${path}?op=download`,
thumbnail, thumbnail,
parentDir, parentDir,
location: bdPosition location: bdPosition,
wgs_84: { lng, lat },
}; };
}) })
.filter(Boolean); .filter(Boolean);
@@ -78,7 +75,19 @@ const Map = () => {
const mapInfo = useMemo(() => initMapInfo({ baiduMapKey, googleMapKey }), []); const mapInfo = useMemo(() => initMapInfo({ baiduMapKey, googleMapKey }), []);
const clusterLeaves = useMemo(() => images.filter(image => clusterLeaveIds.includes(image.id)), [images, clusterLeaveIds]); const clusterLeaves = useMemo(() => images.filter(image => clusterLeaveIds.includes(image.id)), [images, clusterLeaveIds]);
const getPoints = useCallback(() => { const onMapState = 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 onClusterLeaveIds = useCallback((ids) => {
setClusterLeaveIds(ids);
}, []);
const getPoints = useCallback((images) => {
if (!window.Cluster || !images) return []; if (!window.Cluster || !images) return [];
return window.Cluster.pointTransformer(images, (data) => ({ return window.Cluster.pointTransformer(images, (data) => ({
point: [data.location.lng, data.location.lat], point: [data.location.lng, data.location.lat],
@@ -87,141 +96,23 @@ const Map = () => {
src: data.src, 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 offset = { x: 66, y: Utils.isDesktop() ? 30 : 90 };
const ZoomControl = createBMapZoomControl(window.BMapGL, { maxZoom: MAX_ZOOM, minZoom: MIN_ZOOM, offset }, 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(() => { const renderBaiduMap = useCallback(() => {
if (!window.BMapGL.Map) return; if (!window.BMapGL.Map) return;
let { center, zoom } = loadMapState(); mapRef.current = createBaiduMap({ images, center, zoom, getPoints, onMapState, onClusterLeaveIds });
let userPosition = { lng: 116.40396418840683, lat: 39.915106021711345 }; createBaiduMarkerClusterer(mapRef.current, images, onClusterLeaveIds);
// 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) { window.mapViewInstance = mapRef.current;
mapRef.current = new window.BMapGL.Map('sf-metadata-map-container', { }, [images, center, zoom, getPoints, onMapState, onClusterLeaveIds]);
enableMapClick: false,
minZoom: MIN_ZOOM,
maxZoom: MAX_ZOOM,
mapType: getBMapType(mapTypeValue),
});
window.BMapInstance = mapRef.current;
if (isValidPosition(center?.lng, center?.lat)) { const renderGoogleMap = useCallback(() => {
mapRef.current.centerAndZoom(center, zoom); if (!window.google?.maps?.Map) return;
} mapRef.current = createGoogleMap({ center, zoom, onMapState });
createGoogleMarkerClusterer(mapRef.current, images, onClusterLeaveIds);
mapRef.current.enableScrollWheelZoom(true); window.mapViewInstance = mapRef.current;
addMapController(); }, [images, center, zoom, onMapState, onClusterLeaveIds]);
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(() => { const handleClose = useCallback(() => {
setImageIndex(0); setImageIndex(0);
@@ -236,21 +127,60 @@ const Map = () => {
setImageIndex((imageIndex + 1) % clusterLeaves.length); setImageIndex((imageIndex + 1) % clusterLeaves.length);
}, [imageIndex, clusterLeaves.length]); }, [imageIndex, clusterLeaves.length]);
const onMapTypeChange = useCallback((newType) => {
window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_TYPE_KEY, newType);
if (mapInfo.type === MAP_PROVIDER.B_MAP) {
const baiduMapType = {
[MAP_TYPE.MAP]: window.BMAP_NORMAL_MAP,
[MAP_TYPE.SATELLITE]: window.BMAP_SATELLITE_MAP
}[newType] || window.BMAP_NORMAL_MAP;
mapRef.current?.setMapType(baiduMapType);
} else if (mapInfo.type === MAP_PROVIDER.G_MAP) {
const googleMapType = {
[MAP_TYPE.MAP]: window.google.maps.MapTypeId.ROADMAP,
[MAP_TYPE.SATELLITE]: window.google.maps.MapTypeId.HYBRID
}[newType] || window.google.maps.MapTypeId.ROADMAP;
mapRef.current?.setMapTypeId(googleMapType);
}
mapRef.current?.setCenter(mapRef.current.getCenter());
}, [mapInfo.type]);
const onClearMapInstance = useCallback(() => {
if (window.mapViewInstance) {
if (mapInfo.type === MAP_PROVIDER.B_MAP) {
window.mapViewInstance.destroy();
} else if (mapInfo.type === MAP_PROVIDER.G_MAP) {
window.mapViewInstance.setMap(null);
}
delete window.mapViewInstance;
}
mapRef.current = null;
}, [mapInfo.type]);
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;
setCenter(savedCenter);
setZoom(savedZoom);
}, []);
useEffect(() => { useEffect(() => {
updateCurrentPath(`/${PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES}/${viewID}`); updateCurrentPath(`/${PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES}/${viewID}`);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
useEffect(() => { useEffect(() => {
const modifyMapTypeSubscribe = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.MODIFY_MAP_TYPE, (newType) => { loadMapState();
window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_TYPE_KEY, newType); const unsubscribeModifyMapType = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.MODIFY_MAP_TYPE, onMapTypeChange);
const mapType = getBMapType(newType); const unsubscribeClearMapInstance = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.CLEAR_MAP_INSTANCE, onClearMapInstance);
mapRef.current && mapRef.current.setMapType(mapType);
mapRef.current.setCenter(mapRef.current.getCenter());
});
return () => { return () => {
modifyMapTypeSubscribe(); unsubscribeModifyMapType();
unsubscribeClearMapInstance();
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -258,18 +188,34 @@ const Map = () => {
useEffect(() => { useEffect(() => {
if (mapInfo.type === MAP_PROVIDER.B_MAP) { if (mapInfo.type === MAP_PROVIDER.B_MAP) {
if (!window.mapViewInstance) {
loadBMap(mapInfo.key).then(() => renderBaiduMap()); loadBMap(mapInfo.key).then(() => renderBaiduMap());
return () => { } else {
window.renderMap = null; const viewDom = document.getElementById('sf-metadata-view-map');
}; const container = window.mapViewInstance.getContainer();
viewDom.replaceChild(container, containerRef.current);
mapRef.current = window.mapViewInstance;
createBaiduMarkerClusterer(mapRef.current, images, onClusterLeaveIds);
}
} else if (mapInfo.type === MAP_PROVIDER.G_MAP) {
if (!window.mapViewInstance) {
loadGMap(mapInfo.key).then(() => renderGoogleMap());
} else {
const viewDom = document.getElementById('sf-metadata-view-map');
const container = window.mapViewInstance.getDiv();
viewDom.replaceChild(container, containerRef.current);
mapRef.current = window.mapViewInstance;
createGoogleMarkerClusterer(mapRef.current, images, onClusterLeaveIds);
}
} }
return;
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
return ( return (
<div className="sf-metadata-view-map" id="sf-metadata-view-map"> <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> <div ref={containerRef} className="sf-metadata-map-container" id="sf-metadata-map-container"></div>
{clusterLeaveIds.length > 0 && ( {clusterLeaveIds.length > 0 && (
<ModalPortal> <ModalPortal>
<ImageDialog <ImageDialog

View File

@@ -1,4 +1,4 @@
const customAvatarOverlay = (point, avatarUrl, bgUrl, width = 20, height = 25) => { export const customAvatarOverlay = (point, avatarUrl, bgUrl, width = 20, height = 25) => {
class AvatarOverlay extends window.BMapGL.Overlay { class AvatarOverlay extends window.BMapGL.Overlay {
constructor(point, avatarUrl, bgUrl, width, height) { constructor(point, avatarUrl, bgUrl, width, height) {
super(); super();
@@ -44,4 +44,70 @@ const customAvatarOverlay = (point, avatarUrl, bgUrl, width = 20, height = 25) =
return new AvatarOverlay(point, avatarUrl, bgUrl, width, height); return new AvatarOverlay(point, avatarUrl, bgUrl, width, height);
}; };
export default customAvatarOverlay; export const googleCustomAvatarOverlay = (map, position, avatarUrl, bgUrl, width = 20, height = 25) => {
class AvatarOverlay extends window.google.maps.OverlayView {
constructor(map, position, avatarUrl, bgUrl, width, height) {
super();
this._position = position;
this._avatarUrl = avatarUrl;
this._bgUrl = bgUrl;
this._width = width;
this._height = height;
this._div = null;
this.setMap(map);
}
onAdd() {
this.createDomElements();
const panes = this.getPanes();
panes.overlayLayer.appendChild(this._div);
}
draw() {
const overlayProjection = this.getProjection();
const pos = overlayProjection.fromLatLngToDivPixel(this._position);
this._div.style.left = `${pos.x - this._width / 2}px`;
this._div.style.top = `${pos.y - (this._height * 7) / 10}px`;
}
onRemove() {
this._div.parentNode?.removeChild(this._div);
this._div = null;
}
createDomElements() {
this._div = document.createElement('div');
this._div.className = 'custom-avatar-overlay';
Object.assign(this._div.style, {
position: 'absolute',
width: `${this._width}px`,
height: `${this._height}px`,
backgroundImage: `url(${this._bgUrl})`,
backgroundPosition: '.5px 0px',
display: 'flex',
padding: '2px 2.5px 0 2px'
});
const img = document.createElement('img');
img.src = this._avatarUrl;
Object.assign(img.style, {
width: '16px',
height: '16px',
borderRadius: '50%',
display: 'block'
});
this._div.appendChild(img);
}
}
return new AvatarOverlay(
map,
position,
avatarUrl,
bgUrl,
width,
height
);
};

View File

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

View File

@@ -8,26 +8,27 @@ export const initMapInfo = ({ baiduMapKey, googleMapKey, mineMapKey }) => {
}; };
export const loadMapSource = (type, key, callback) => { export const loadMapSource = (type, key, callback) => {
if (!type || !key) return;
let scriptUrl = ''; let scriptUrl = '';
const sourceId = 'map-source-script'; const sourceId = 'map-source-script';
if (document.getElementById(sourceId)) return; if (document.getElementById(sourceId)) return;
let script = document.createElement('script');
script.type = 'text/javascript';
script.id = sourceId;
if (type === MAP_TYPE.B_MAP) { if (type === MAP_TYPE.B_MAP) {
scriptUrl = `https://api.map.baidu.com/api?type=webgl&v=3.0&ak=${key}&callback=renderBaiduMap`; scriptUrl = `https://api.map.baidu.com/api?type=webgl&v=3.0&ak=${key}&callback=renderBaiduMap`;
} else if (type === MAP_TYPE.G_MAP) { } else if (type === MAP_TYPE.G_MAP) {
scriptUrl = `https://maps.googleapis.com/maps/api/js?key=${key}&callback=renderGoogleMap&libraries=marker&v=weekly`; scriptUrl = `https://maps.googleapis.com/maps/api/js?key=${key}&libraries=marker,geometry&v=weekly&callback=renderGoogleMap`;
} }
if (scriptUrl) { if (scriptUrl) {
const script = document.createElement('script');
script.id = sourceId;
script.src = scriptUrl; script.src = scriptUrl;
script.onload = callback;
document.body.appendChild(script); document.body.appendChild(script);
} }
callback && callback();
}; };
export default function loadBMap(ak) { export function loadBMap(ak) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (typeof window.BMapGL !== 'undefined' && document.querySelector(`script[src*="${mediaUrl}js/map/cluster.js"]`)) { if (typeof window.BMapGL !== 'undefined' && document.querySelector(`script[src*="${mediaUrl}js/map/cluster.js"]`)) {
resolve(true); resolve(true);
@@ -57,6 +58,36 @@ export function asyncLoadBaiduJs(ak) {
}); });
} }
export function loadGMap(ak) {
return new Promise((resolve, reject) => {
if (typeof window.google !== 'undefined' && document.querySelector(`script[src*="${mediaUrl}js/map/cluster.js"]`)) {
resolve(true);
return;
}
asyncLoadGMapJs(ak)
.then(() => asyncLoadJs('https://unpkg.com/@googlemaps/markerclusterer/dist/index.min.js'))
.then(() => resolve(true))
.catch((err) => reject(err));
});
}
export function asyncLoadGMapJs(key) {
return new Promise((resolve, reject) => {
if (typeof window.google !== 'undefined') {
resolve(window.google);
return;
}
window.renderGoogleMap = function () {
resolve(window.google);
};
let script = document.createElement('script');
script.type = 'text/javascript';
script.src = `https://maps.googleapis.com/maps/api/js?key=${key}&libraries=marker,geometry&v=weekly&callback=renderGoogleMap&loading=async`;
script.onerror = reject;
document.body.appendChild(script);
});
}
export function asyncLoadJs(url) { export function asyncLoadJs(url) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let script = document.createElement('script'); let script = document.createElement('script');