1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-23 20:37:42 +00:00

Feature/location editor (#8084)

* add geolocation editor

* update location with selected position

* optimize

* google map

* optimize

* optimize ui & clean up code

* fix position validation

* update geolocation modal zIndex

---------

Co-authored-by: zhouwenxuan <aries@Mac.local>
This commit is contained in:
Aries
2025-08-02 14:50:00 +08:00
committed by GitHub
parent 1faf296760
commit 06f7cae9c7
16 changed files with 1153 additions and 110 deletions

View File

@@ -0,0 +1,16 @@
<?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>
<path class="st0" d="M4.6,12.7c0.9,0,1.7-0.7,1.7-1.6V7.8c0-0.4,0.2-0.8,0.5-1.1s0.7-0.5,1.1-0.5h3.3c0.9,0,1.6-0.7,1.6-1.6
C12.7,3.7,12,3,11.2,3H4.6C3.7,3,3,3.7,3,4.6v6.6C3,12,3.7,12.7,4.6,12.7z M7.9,25.8c-0.4,0-0.8-0.2-1.1-0.5s-0.5-0.7-0.5-1.1v-3.3
c0-0.4-0.2-0.8-0.5-1.1s-0.7-0.5-1.1-0.5c-1,0-1.7,0.7-1.7,1.6v6.5C3,28.3,3.7,29,4.6,29h6.5c0.9,0,1.6-0.7,1.6-1.6
s-0.7-1.6-1.6-1.6C11.1,25.8,7.9,25.8,7.9,25.8z M27.3,19.2c-0.9,0-1.6,0.7-1.6,1.6v3.3c0,0.4-0.2,0.8-0.5,1.1
c-0.3,0.3-0.7,0.5-1.1,0.5h-3.3c-0.9,0-1.6,0.7-1.6,1.6v0.1c0,0.4,0.2,0.8,0.5,1.1c0.3,0.3,0.7,0.5,1.1,0.5h6.6
c0.4,0,0.8-0.2,1.1-0.5c0.3-0.3,0.5-0.7,0.5-1.1v-6.6C29,20,28.3,19.2,27.3,19.2z M20.8,3c-0.9,0-1.6,0.7-1.6,1.6v0.1
c0,0.9,0.7,1.6,1.6,1.6h3.3c0.4,0,0.8,0.2,1.1,0.5c0.3,0.3,0.5,0.7,0.5,1.1v3.3c0,0.9,0.7,1.6,1.6,1.6h0.1c0.9-0.1,1.6-0.8,1.6-1.6
V4.6C29,3.7,28.3,3,27.4,3H20.8z M21,13v6c0,1.1-0.9,2-2,2h-6c-1.1,0-2-0.9-2-2v-6c0-1.1,0.9-2,2-2h6C20.1,11,21,11.9,21,13z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -6,7 +6,7 @@ import { CellType, COLUMNS_ICON_CONFIG } from '../../../metadata/constants';
import './index.css';
const DetailItem = ({ readonly = true, field, className, children }) => {
const DetailItem = ({ id, readonly = true, field, className, children }) => {
const icon = useMemo(() => {
if (field.type === 'size') {
return COLUMNS_ICON_CONFIG[CellType.NUMBER];
@@ -15,7 +15,7 @@ const DetailItem = ({ readonly = true, field, className, children }) => {
}, [field]);
return (
<div className={classnames('dirent-detail-item', className)}>
<div id={id} className={classnames('dirent-detail-item', className)}>
<div className="dirent-detail-item-name d-flex">
<div><Icon className="sf-metadata-icon" symbol={icon} /></div>
<span className="dirent-detail-item-name-value">{field.name}</span>

View File

@@ -0,0 +1,162 @@
import { gettext } from '../../../../utils/constants';
const generateLabelContent = (info, isBMap = false) => {
const { location_translated, title, tag } = info;
const { address } = location_translated;
const tagContent = Array.isArray(tag) && tag.length > 0 ? tag[0] : '';
if (isBMap) {
return `
<div
class='${title ? 'selection-label-content' : 'selection-label-content simple'}'
id='selection-label-content'
>
${`
<div class="w-100 d-flex align-items-center">
${title ? `
<span class="label-title text-truncate" title="${title}">${title}</span>
<span class="close-btn" id="selection-label-close">
<i class="sf3-font sf3-font-x-01"></i>
</span>
` : `
<span class="label-address text-truncate simple" title="${address}">${address}</span>
<span class="close-btn" id="selection-label-close">
<i class="sf3-font sf3-font-x-01"></i>
</span>
`}
</div>
${title ? `
${tagContent ? `<span class="label-tag">${tagContent}</span>` : ''}
<span class="label-address-tip">${gettext('Address')}</span>
<span class="label-address text-truncate" title="${address}">${address}</span>
` : ''}
<div class="label-submit btn btn-primary" id="selection-label-submit">${gettext('Fill in')}</div>
`}
</div>
`;
} else {
const container = document.createElement('div');
container.className = title ? 'selection-label-content' : 'selection-label-content simple';
container.id = 'selection-label-content';
const content = `
<div class="w-100 d-flex align-items-center">
${title ? `
<span class="label-title text-truncate" title="${title}">${title}</span>
<span class="close-btn" id="selection-label-close">
<i class="sf3-font sf3-font-x-01"></i>
</span>
` : `
<span class="label-address text-truncate simple" title="${address}">${address}</span>
<span class="close-btn" id="selection-label-close">
<i class="sf3-font sf3-font-x-01"></i>
</span>
`}
</div>
${title ? `
${tagContent ? `<span class="label-tag">${tagContent}</span>` : ''}
<span class="label-address-tip">${gettext('Address')}</span>
<span class="label-address text-truncate" title="${address}">${address}</span>
` : ''}
<div class="label-submit btn btn-primary" id="selection-label-submit">${gettext('Fill in')}</div>
`;
container.innerHTML = content;
return container;
}
};
export const customBMapLabel = (info) => {
const content = generateLabelContent(info, true);
const label = new window.BMapGL.Label(content, { offset: new window.BMapGL.Size(9, -5) });
const style = info.title
? { transform: 'translateY(-50%, 10%)' }
: { transform: 'translateY(-50%, 15%)' };
label.setStyle(style);
return label;
};
export const customGMapLabel = (info, submit) => {
class Popup extends window.google.maps.OverlayView {
constructor(position, content) {
super();
this.position = position;
this.info = info;
this.containerDiv = document.createElement('div');
this.containerDiv.classList.add('popup-label-container');
this.containerDiv.appendChild(content);
const closeBtn = this.containerDiv.querySelector('#selection-label-close');
if (closeBtn) {
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.onRemove();
});
}
const submitBtn = this.containerDiv.querySelector('#selection-label-submit');
if (submitBtn) {
submitBtn.addEventListener('click', (e) => {
e.stopPropagation();
submit(info);
});
}
Popup.preventMapHitsAndGesturesFrom(this.containerDiv);
}
onAdd() {
this.getPanes().floatPane.appendChild(this.containerDiv);
}
onRemove() {
if (this.containerDiv.parentElement) {
this.containerDiv.parentElement.removeChild(this.containerDiv);
}
}
setPosition(position) {
this.position = position;
this.draw();
}
setInfo(info) {
this.info = info;
this.containerDiv.innerHTML = generateLabelContent(info, true);
const closeBtn = this.containerDiv.querySelector('#selection-label-close');
if (closeBtn) {
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.onRemove();
});
}
const submitBtn = this.containerDiv.querySelector('#selection-label-submit');
if (submitBtn) {
submitBtn.addEventListener('click', (e) => {
e.stopPropagation();
submit(info);
});
}
this.draw();
}
draw() {
const divPosition = this.getProjection().fromLatLngToDivPixel(
this.position,
);
// Hide the popup when it is far out of view.
const display =
Math.abs(divPosition.x) < 4000 && Math.abs(divPosition.y) < 4000
? 'block'
: 'none';
if (display === 'block') {
this.containerDiv.style.left = divPosition.x + 'px';
this.containerDiv.style.top = divPosition.y + 'px';
}
if (this.containerDiv.style.display !== display) {
this.containerDiv.style.display = display;
}
}
}
const content = generateLabelContent(info);
return new Popup(info.position, content);
};

View File

@@ -0,0 +1,284 @@
.sf-geolocation-editor-container {
width: 100%;
height: 434px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 1px solid var(--bs-border-color);
border-radius: 4px;
background-color: #fff;
}
.sf-geolocation-editor-container.full-screen {
width: 100%;
height: 600px;
border-radius: 4px;
box-shadow: 0 0 10px #0000004d;
transform: none;
}
.sf-geolocation-editor-container .editor-header {
width: 100%;
height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
background-color: #fff;
color: var(--bs-body-color);
}
.sf-geolocation-editor-container .editor-header .title {
display: flex;
align-items: center;
font-size: 14px;
font-weight: 500;
}
.sf-geolocation-editor-container .editor-header .title .location-icon {
color: #999;
}
.sf-geolocation-editor-container .editor-header .full-screen {
width: 24px;
height: 24px;
color: #666;
}
.sf-geolocation-editor-container .editor-header .full-screen:hover {
background-color: var(--bs-hover-bg);
}
.sf-geolocation-editor-container .search-container {
width: 90%;
max-width: 450px;
height: 38px;
display: flex;
justify-content: center;
position: absolute;
top: 16px;
left: 24px;
z-index: 10;
}
.sf-geolocation-editor-container .search-container .search-input {
flex: 1;
height: 100%;
padding-right: 38px;
border-radius: 3px 0 0 3px;
border-right: none;
box-shadow: 0 0 2px #0000004d;
cursor: text;
}
.sf-geolocation-editor-container .search-container .clean-btn {
width: 24px;
height: 24px;
position: absolute;
font-size: 16px;
color: #666;
top: 7px;
right: 8px;
cursor: pointer;
}
.sf-geolocation-editor-container .search-container .clean-btn:hover {
background-color: var(--bs-hover-bg);
}
.sf-geolocation-editor-container .search-container .search-btn {
width: 12%;
height: 38px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bs-body-tertiary-bg);
color: #666;
border: 1px solid var(--bs-border-color);
border-radius: 0 3px 3px 0;
box-shadow: 0 0 2px #0000004d;
cursor: pointer;
}
.sf-geolocation-editor-container .selection-label-content {
width: 225px;
height: 130px;
position: absolute;
background-color: #fff;
border: none;
border-radius: 3px;
box-shadow: 1px 2px 1px rgba(0, 0, 0, .15);
padding: 10px;
font-weight: 500;
cursor: default;
left: -115px;
top: 16px;
}
.sf-geolocation-editor-container .selection-label-content .close-btn {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 4px;
top: 4px;
color: #666;
cursor: pointer;
text-align: center;
}
.sf-geolocation-editor-container .selection-label-content .close-btn:hover {
background-color: var(--bs-hover-bg);
}
.sf-geolocation-editor-container .selection-label-content .label-title {
width: 90%;
color: #212529;
font-size: 18px;
text-overflow: ellipsis;
}
.sf-geolocation-editor-container .selection-label-content .label-tag {
font-size: 12px;
font-weight: 400;
}
.sf-geolocation-editor-container .selection-label-content .label-address-tip {
position: absolute;
top: 58px;
left: 10px;
font-size: 12px;
font-weight: 400;
color: #666;
}
.sf-geolocation-editor-container .selection-label-content .label-address {
width: 90%;
position: absolute;
top: 74px;
left: 10px;
font-size: 12px;
font-weight: 400;
cursor: text;
}
.sf-geolocation-editor-container .selection-label-content .label-submit {
width: fit-content;
min-width: 50px;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
bottom: 4px;
right: 4px;
font-size: 12px;
}
.sf-geolocation-editor-container .selection-label-content seafile-multicolor-icon-drop-down {
position: absolute;
transform: rotate(180deg);
top: -16px;
left: 47%;
color: #fff;
}
.sf-geolocation-editor-container .selection-label-content.simple {
height: 90px;
}
.sf-geolocation-editor-container .selection-label-content.simple .label-address {
width: 205px;
position: absolute;
top: 32px;
font-size: 14px;
cursor: text;
}
.sf-geolocation-editor-container .sf-map-control-container {
width: 30px;
height: fit-content;
flex-direction: column;
right: 30px;
}
.sf-geolocation-editor-container .sf-map-control-container.sf-map-geolocation-control-container {
right: 16px !important;
bottom: 96px !important;
}
.sf-geolocation-editor-container .sf-map-control-container.sf-map-zoom-control-container {
right: 16px !important;
bottom: 30px !important;
}
.sf-geolocation-editor-container .sf-map-control-container .sf-map-control-divider {
width: 100%;
height: 1px;
}
.sf-geolocation-editor-container .sf-map-control-container .sf-map-control-divider::before {
width: 22px;
height: 1px;
left: 4px;
top: 0;
}
.sf-geolocation-editor-container .search-results-container {
width: 404px;
height: fit-content;
max-height: 300px;
overflow-y: scroll;
position: absolute;
top: 62px;
left: 20px;
background-color: #fff;
border: 1px solid var(--bs-border-color);
border-radius: 3px;
padding: 0 10px;
box-shadow: 0 -0 3px rgb(0 0 0 / 30%);
z-index: 10;
}
.sf-geolocation-editor-container .search-result-item {
height: 56px;
position: relative;
display: flex;
flex-direction: column;
justify-content: space-around;
padding: 8px;
border-bottom: 1px solid var(--bs-border-color);
cursor: pointer;
}
.sf-geolocation-editor-container .search-result-item:hover {
background-color: var(--bs-th-bg);
}
.sf-geolocation-editor-container .search-result-item .search-result-item-title {
position: relative;
height: 16px;
color: #212529;
font-size: 14px;
line-height: 14px;
}
.sf-geolocation-editor-container .search-result-item .search-result-item-address {
position: relative;
color: #666;
font-size: 12px;
line-height: 12px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.popup-label-container {
cursor: auto;
height: 0;
position: absolute;
width: 200px;
}

View File

@@ -0,0 +1,501 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import classNames from 'classnames';
import { createBMapGeolocationControl, createBMapZoomControl } from '../../map-controller';
import { initMapInfo, loadMapSource } from '../../../../utils/map-utils';
import { baiduMapKey, gettext, googleMapId, googleMapKey, lang } from '../../../../utils/constants';
import { KeyCodes, MAP_TYPE } from '../../../../constants';
import Icon from '../../../../components/icon';
import IconBtn from '../../../../components/icon-btn';
import toaster from '../../../../components/toast';
import { createZoomControl } from '../../map-controller/zoom';
import { createGeolocationControl } from '../../map-controller/geolocation';
import { customBMapLabel, customGMapLabel } from './custom-label';
import { isValidPosition } from '../../../utils/validate';
import { DEFAULT_POSITION } from '../../../constants';
import './index.css';
const GeolocationEditor = ({ position, isFullScreen, onSubmit, onFullScreen, onReadyToEraseLocation }) => {
const [inputValue, setInputValue] = useState('');
const [searchResults, setSearchResults] = useState([]);
const type = useMemo(() => {
const { type } = initMapInfo({ baiduMapKey, googleMapKey });
return type;
}, []);
const ref = useRef(null);
const mapRef = useRef(null);
const geocRef = useRef(null);
const markerRef = useRef(null);
const labelRef = useRef(null);
const googlePlacesRef = useRef(null);
const onChange = useCallback((e) => {
setInputValue(e.target.value);
}, []);
const search = useCallback(() => {
if (type === MAP_TYPE.B_MAP) {
const options = {
onSearchComplete: (results) => {
const status = local.getStatus();
if (status !== window.BMAP_STATUS_SUCCESS) {
toaster.danger(gettext('Search failed, please enter detailed address.'));
return;
}
let searchResults = [];
for (let i = 0; i < results.getCurrentNumPois(); i++) {
const value = results.getPoi(i);
const position = {
address: value.address || '',
title: value.title || '',
tag: value.tags || [],
lngLat: {
lng: value.point.lng,
lat: value.point.lat,
}
};
searchResults.push(position);
}
setSearchResults(searchResults);
}
};
const local = new window.BMapGL.LocalSearch(mapRef.current, options);
local.search(inputValue);
} else if (type === MAP_TYPE.G_MAP) {
const request = {
query: inputValue,
language: lang,
};
googlePlacesRef.current.textSearch(request, (results, status) => {
if (status === 'OK' && results[0]) {
let searchResults = [];
for (let i = 0; i < results.length; i++) {
const value = {
address: results[i].formatted_address || '',
title: results[i].name || '',
tag: results[i].types || [],
lngLat: {
lng: results[i].geometry.location.lng(),
lat: results[i].geometry.location.lat(),
}
};
searchResults.push(value);
}
setSearchResults(searchResults);
}
});
}
}, [type, inputValue]);
const onKeyDown = useCallback((e) => {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
if (e.keyCode === KeyCodes.Enter) {
search();
} else if (e.keyCode === KeyCodes.Backspace) {
setSearchResults([]);
}
}, [search]);
const clear = useCallback(() => {
setInputValue('');
setSearchResults([]);
if (type === MAP_TYPE.B_MAP) {
mapRef.current.clearOverlays();
} else {
labelRef.current.setMap(null);
labelRef.current = null;
}
onReadyToEraseLocation();
}, [type, onReadyToEraseLocation]);
const close = useCallback((e) => {
e.stopPropagation();
if (type === MAP_TYPE.B_MAP) {
markerRef.current.getLabel()?.remove();
} else {
labelRef.current.setMap(null);
labelRef.current = null;
}
}, [type]);
const submit = useCallback((value) => {
const { position, location_translated } = value;
const location = {
position,
location_translated,
};
onSubmit(location);
}, [onSubmit]);
const parseBMapAddress = useCallback((result) => {
let value = {};
const { surroundingPois, address, addressComponents, point } = result;
if (surroundingPois.length === 0) {
const { province, city, district, street } = addressComponents;
value.position = { lng: point.lng, lat: point.lat };
value.location_translated = {
address,
country: '',
province,
city,
district,
street,
};
value.title = '';
value.tags = [];
} else {
const position = surroundingPois[0];
const { address, title, tags, point, city, province } = position;
value.position = { lng: point.lng, lat: point.lat };
value.location_translated = {
address,
country: '',
province,
city,
district: '',
street: '',
};
value.title = title || '';
value.tags = tags || [];
}
return value;
}, []);
const parseGMapAddress = useCallback((result) => {
const location_translated = {
country: '',
province: '',
city: '',
district: '',
street: ''
};
result.address_components.forEach(component => {
if (component.types.includes('country')) {
location_translated.country = component.long_name;
} else if (component.types.includes('administrative_area_level_1')) {
location_translated.province = component.long_name;
} else if (component.types.includes('locality')) {
location_translated.city = component.long_name;
} else if (component.types.includes('sublocality')) {
location_translated.district = component.long_name;
} else if (component.types.includes('route')) {
location_translated.street = component.long_name;
}
});
location_translated.address = result.formatted_address;
const position = {
lng: result.geometry.location.lng(),
lat: result.geometry.location.lat()
};
return { position, location_translated };
}, []);
const addLabel = useCallback((point) => {
if (type === MAP_TYPE.B_MAP) {
markerRef.current.getLabel()?.remove();
geocRef.current.getLocation(point, (result) => {
const info = parseBMapAddress(result);
const { title, location_translated } = info;
setInputValue(title || location_translated.address);
const label = customBMapLabel(info);
markerRef.current.setLabel(label);
setTimeout(() => {
const label = document.getElementById('selection-label-content');
if (label) {
label.addEventListener('click', (e) => e.stopPropagation());
}
const closeBtn = document.getElementById('selection-label-close');
if (closeBtn) {
closeBtn.addEventListener('click', close);
}
const submitBtn = document.getElementById('selection-label-submit');
if (submitBtn) {
submitBtn.addEventListener('click', () => submit(info));
}
}, 100);
});
} else {
geocRef.current.geocode({ location: point, language: lang }, (results, status) => {
if (status === 'OK' && results[0]) {
const info = parseGMapAddress(results[0]);
labelRef.current = customGMapLabel(info, submit);
labelRef.current.setMap(mapRef.current);
setInputValue(info.location_translated.address);
}
});
}
}, [type, close, submit, parseBMapAddress, parseGMapAddress]);
const renderBaiduMap = useCallback(() => {
if (!window.BMapGL.Map) return;
mapRef.current = new window.BMapGL.Map(ref.current);
const initPos = isValidPosition(position?.lng, position?.lat) ? position : DEFAULT_POSITION;
const point = new window.BMapGL.Point(initPos.lng, initPos.lat);
mapRef.current.centerAndZoom(point, 16);
mapRef.current.enableScrollWheelZoom();
mapRef.current.clearOverlays();
const ZoomControl = createBMapZoomControl({
anchor: window.BMAP_ANCHOR_BOTTOM_RIGHT,
offset: { x: 16, y: 30 }
});
const zoomControl = new ZoomControl();
mapRef.current.addControl(zoomControl);
const GeolocationControl = createBMapGeolocationControl({
anchor: window.BMAP_ANCHOR_BOTTOM_RIGHT,
offset: { x: 16, y: 96 },
callback: (point) => {
if (mapRef.current.getOverlays().length === 0) {
mapRef.current.addOverlay(markerRef.current);
}
mapRef.current.centerAndZoom(point, 16);
markerRef.current.setPosition(point);
addLabel(point);
}
});
const geolocationControl = new GeolocationControl();
mapRef.current.addControl(geolocationControl);
markerRef.current = new window.BMapGL.Marker(point, { offset: new window.BMapGL.Size(-2, -5) });
geocRef.current = new window.BMapGL.Geocoder();
if (isValidPosition(position?.lng, position?.lat)) {
mapRef.current.addOverlay(markerRef.current);
addLabel(point);
}
mapRef.current.addEventListener('click', (e) => {
if (searchResults.length > 0) {
setSearchResults([]);
return;
}
const { lng, lat } = e.latlng;
const point = new window.BMapGL.Point(lng, lat);
if (mapRef.current.getOverlays().length === 0) {
mapRef.current.addOverlay(markerRef.current);
}
markerRef.current.setPosition(point);
mapRef.current.setCenter(point);
addLabel(point);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [position?.lng, position?.lat]);
const renderGoogleMap = useCallback(() => {
const isValid = isValidPosition(position?.lng, position?.lat);
const initPos = isValid ? position : DEFAULT_POSITION;
mapRef.current = new window.google.maps.Map(ref.current, {
center: initPos,
zoom: 16,
mapId: googleMapId,
zoomControl: false,
mapTypeControl: false,
scaleControl: false,
streetViewControl: false,
rotateControl: false,
fullscreenControl: false,
disableDefaultUI: true,
gestrueHandling: 'cooperative',
clickableIcons: false,
});
// control
const zoomControl = createZoomControl({ map: mapRef.current });
const geolocationControl = createGeolocationControl({
map: mapRef.current,
callback: (lngLat) => {
geocRef.current.geocode({ location: lngLat, language: lang }, (results, status) => {
if (status === 'OK' && results[0]) {
const info = parseGMapAddress(results[0]);
setInputValue(info.location_translated.address);
if (!markerRef.current) {
markerRef.current = new window.google.maps.marker.AdvancedMarkerElement({
position: lngLat,
map: mapRef.current,
});
} else {
markerRef.current.position = lngLat;
}
if (!labelRef.current) {
addLabel(lngLat);
} else {
labelRef.current.setPosition(lngLat);
labelRef.current.setInfo(info);
}
}
});
}
});
mapRef.current.controls[window.google.maps.ControlPosition.RIGHT_BOTTOM].push(zoomControl);
mapRef.current.controls[window.google.maps.ControlPosition.RIGHT_BOTTOM].push(geolocationControl);
// marker
if (isValid) {
markerRef.current = new window.google.maps.marker.AdvancedMarkerElement({
position,
map: mapRef.current,
});
}
// geocoder
geocRef.current = new window.google.maps.Geocoder();
isValid && addLabel(position);
googlePlacesRef.current = new window.google.maps.places.PlacesService(mapRef.current);
// map click event
window.google.maps.event.addListener(mapRef.current, 'click', (e) => {
if (searchResults.length > 0) {
setSearchResults([]);
return;
}
const latLng = e.latLng;
const point = { lat: latLng.lat(), lng: latLng.lng() };
if (!markerRef.current) {
markerRef.current = new window.google.maps.marker.AdvancedMarkerElement({
position: point,
map: mapRef.current,
});
} else {
markerRef.current.position = latLng;
}
mapRef.current.panTo(latLng);
geocRef.current.geocode({ location: point, language: lang }, (results, status) => {
if (status === 'OK' && results[0]) {
const info = parseGMapAddress(results[0]);
if (!labelRef.current) {
addLabel(point);
} else {
labelRef.current.setPosition(latLng);
labelRef.current.setInfo(info);
}
setInputValue(info.location_translated.address);
}
});
});
}, [searchResults, position, addLabel, parseGMapAddress]);
const toggleFullScreen = useCallback((e) => {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
onFullScreen();
}, [onFullScreen]);
const onSelect = useCallback((result) => {
const { lngLat, title, address } = result;
let point = lngLat;
if (type === MAP_TYPE.B_MAP) {
const { lng, lat } = lngLat;
point = new window.BMapGL.Point(lng, lat);
if (mapRef.current.getOverlays().length === 0) {
mapRef.current.addOverlay(markerRef.current);
}
markerRef.current.setPosition(point);
mapRef.current.setCenter(point);
addLabel(point);
} else {
const point = { lat: lngLat.lat, lng: lngLat.lng };
markerRef.current.position = point;
if (!labelRef.current) {
addLabel(point);
} else {
labelRef.current.setPosition(point);
labelRef.current.setInfo({
title,
tag: [],
position: point,
location_translated: {
address,
country: '',
province: '',
city: '',
district: '',
street: '',
}
});
}
mapRef.current.panTo(point);
}
setSearchResults([]);
setInputValue(title || address);
}, [type, addLabel]);
useEffect(() => {
if (mapRef.current) return;
const { type, key } = initMapInfo({ baiduMapKey, googleMapKey });
if (type === MAP_TYPE.B_MAP) {
if (!window.BMapGL) {
window.renderBaiduMap = () => renderBaiduMap();
loadMapSource(type, key);
} else {
renderBaiduMap();
}
} else if (type === MAP_TYPE.G_MAP) {
if (!window.google?.maps.Map) {
window.renderGoogleMap = () => renderGoogleMap();
loadMapSource(type, key);
} else {
renderGoogleMap();
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className={classNames('sf-geolocation-editor-container', { 'full-screen': isFullScreen })}>
<div className="editor-header">
<div className="title">
<Icon symbol="location" size={24} className="location-icon" />
<span className="ml-2">{gettext('Address')}</span>
</div>
<IconBtn className="full-screen" symbol="full-screen" size={24} onClick={toggleFullScreen} />
</div>
<div className="w-100 h-100 position-relative">
<div className="search-container">
<div className="flex-1 d-flex position-relative">
<input
type="text"
value={inputValue}
className="form-control search-input"
placeholder={gettext('Please enter the address')}
onChange={onChange}
onKeyDown={onKeyDown}
autoFocus
/>
{inputValue && <IconBtn symbol="close" className="clean-btn" size={24} onClick={clear} />}
</div>
<span className="search-btn" onClick={search}>
<i className="sf3-font sf3-font-search"></i>
</span>
</div>
<div ref={ref} className="w-100 h-100 sf-metadata-geolocation-editor-container"></div>
{searchResults.length > 0 && (
<div className="search-results-container">
{searchResults.map((result, index) => (
<div key={index} className="search-result-item" onClick={() => onSelect(result)}>
<span className="search-result-item-title">{result.title || ''}</span>
<span className="search-result-item-address">{result.address || ''}</span>
</div>
))}
</div>
)}
</div>
</div>
);
};
export default GeolocationEditor;

View File

@@ -2,7 +2,7 @@ import classnames from 'classnames';
import { Utils } from '../../../utils/utils';
import { wgs84_to_gcj02 } from '../../../utils/coord-transform';
export const createGeolocationControl = (map) => {
export const createGeolocationControl = ({ map, callback }) => {
const container = document.createElement('div');
container.className = classnames(
'sf-map-control-container sf-map-geolocation-control-container d-flex align-items-center justify-content-center',
@@ -23,6 +23,7 @@ export const createGeolocationControl = (map) => {
navigator.geolocation.getCurrentPosition((userInfo) => {
const gcPosition = wgs84_to_gcj02(userInfo.coords.longitude, userInfo.coords.latitude);
map.setCenter(gcPosition);
callback(gcPosition);
});
}
});
@@ -39,7 +40,7 @@ export function createBMapGeolocationControl({ anchor, offset, callback }) {
GeolocationControl.prototype.initialize = function (map) {
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', {
'sf-map-geolocation-control-mobile': !Utils.isDesktop()
'sf-map-control-container-mobile': !Utils.isDesktop()
});
const locationButton = document.createElement('div');

View File

@@ -37,21 +37,11 @@
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 {
width: 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 {
width: 40px;
}

View File

@@ -32,7 +32,7 @@ const updateButtonStates = (map, zoomIn, zoomOut) => {
zoomOut.className = classnames(buttonClassName, { 'disabled': zoomLevel <= MIN_ZOOM });
};
export const createZoomControl = (map) => {
export const createZoomControl = ({ map }) => {
const container = createZoomContainer();
const zoomInButton = createButton('<i class="sf-map-control-icon sf3-font sf3-font-zoom-in"></i>');
@@ -60,7 +60,7 @@ export const createZoomControl = (map) => {
return container;
};
export function createBMapZoomControl(anchor, offset) {
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);

View File

@@ -33,8 +33,8 @@ const MetadataDetails = ({ readOnly, tagsData }) => {
if (isDir && FOLDER_NOT_DISPLAY_COLUMN_KEYS.includes(field.key)) return null;
const value = getCellValueByColumn(record, field);
if (field.key === PRIVATE_COLUMN_KEY.LOCATION && Utils.imageCheck(fileName) && value) {
return <Location key={field.key} position={value} record={record} />;
if (field.key === PRIVATE_COLUMN_KEY.LOCATION && Utils.imageCheck(fileName)) {
return <Location key={field.key} position={value} record={record} onChange={onChange} />;
}
let canEdit = canModifyRecord && field.editable && !readOnly;

View File

@@ -32,3 +32,27 @@
right: 10px !important;
bottom: 16px !important;
}
.dirent-detail-item-value-map .sf-metadata-ui.sf-metadata-geolocation-formatter {
width: 100%;
cursor: pointer;
}
.sf-metadata-record-cell-empty {
position: relative;
width: 100%;
padding: 7px 6px 0 6px;
}
.sf-metadata-record-cell-empty:empty::before {
content: attr(placeholder);
color: rgba(255, 255, 255, .7);
}
.sf-metadata-geolocation-property-detail-editor-popover .popover.show {
width: 500px;
max-width: 500px;
border: none;
background-color: transparent;
transform: translateX(-140px);
}

View File

@@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Modal, Popover } from 'reactstrap';
import { initMapInfo, loadMapSource } from '../../../../utils/map-utils';
import { wgs84_to_gcj02, gcj02_to_bd09 } from '../../../../utils/coord-transform';
import { MAP_TYPE } from '../../../../constants';
import Loading from '../../../../components/loading';
import { gettext, baiduMapKey, googleMapKey, googleMapId } from '../../../../utils/constants';
@@ -15,6 +15,8 @@ import { createBMapZoomControl } from '../../map-controller';
import { Utils } from '../../../../utils/utils';
import { eventBus } from '../../../../components/common/event-bus';
import { createZoomControl } from '../../map-controller/zoom';
import ClickOutside from '../../../../components/click-outside';
import GeolocationEditor from '../../cell-editors/geolocation-editor';
import './index.css';
@@ -23,6 +25,7 @@ class Location extends React.Component {
static propTypes = {
position: PropTypes.object,
record: PropTypes.object,
onChange: PropTypes.func,
};
constructor(props) {
@@ -31,9 +34,14 @@ class Location extends React.Component {
this.mapType = type;
this.mapKey = key;
this.map = null;
this.marker = null;
this.state = {
address: '',
latLng: this.props.position,
address: this.props.record?._location_translated?.address || '',
isLoading: false,
isEditorShown: false,
isFullScreen: false,
isReadyToEraseLocation: false,
};
}
@@ -48,109 +56,96 @@ class Location extends React.Component {
});
}
componentDidUpdate(prevProps) {
const { position, record } = this.props;
if (!isValidPosition(position?.lng, position?.lat) || typeof record !== 'object') return;
if (prevProps.position?.lng === position?.lng && prevProps.position?.lat === position?.lat) return;
let transformedPos = wgs84_to_gcj02(position.lng, position.lat);
if (this.mapType === MAP_TYPE.B_MAP) {
transformedPos = gcj02_to_bd09(transformedPos.lng, transformedPos.lat);
componentDidUpdate(prevProps, prevState) {
const { latLng } = this.state;
if (prevProps.record._id !== this.props.record._id) {
this.setState({
latLng: this.props.position,
address: this.props.record?._location_translated?.address || '',
isReadyToEraseLocation: false,
});
}
this.addMarkerByPosition(transformedPos.lng, transformedPos.lat);
if (!this.map) return;
if (!isValidPosition(latLng?.lng, latLng?.lat)) return;
this.setState({ address: record._location_translated?.address });
if (prevState.latLng?.lat !== latLng.lat || prevState.latLng?.lng !== latLng.lng) {
if (this.mapType === MAP_TYPE.B_MAP) {
this.marker.setPosition(latLng);
this.map.panTo(latLng);
} else if (this.mapType === MAP_TYPE.G_MAP) {
this.marker.position = latLng;
this.map.panTo(latLng);
}
}
}
componentWillUnmount() {
this.unsubscribeClearMapInstance();
this.map = null;
}
initMap = () => {
if (this.map) return;
const { record } = this.props;
const { latLng } = this.state;
if (!isValidPosition(latLng?.lng, latLng?.lat) || typeof record !== 'object') return;
const { position, record } = this.props;
if (!isValidPosition(position?.lng, position?.lat) || typeof record !== 'object') return;
this.setState({ isLoading: true, address: record._location_translated?.address });
if (this.mapType === MAP_TYPE.B_MAP) {
if (!window.BMapGL) {
window.renderBaiduMap = () => this.renderBaiduMap(position);
loadMapSource(this.mapType, this.mapKey);
} else {
this.renderBaiduMap(position);
this.setState({ isLoading: true }, () => {
if (this.mapType === MAP_TYPE.B_MAP) {
if (!window.BMapGL) {
window.renderBaiduMap = () => this.renderBaiduMap();
loadMapSource(this.mapType, this.mapKey);
} else {
this.renderBaiduMap();
}
} else if (this.mapType === MAP_TYPE.G_MAP) {
if (!window.google?.maps.Map) {
window.renderGoogleMap = () => this.renderGoogleMap();
loadMapSource(this.mapType, this.mapKey);
} else {
this.renderGoogleMap();
}
}
});
};
addMarker = () => {
const { latLng } = this.state;
if (this.mapType === MAP_TYPE.B_MAP) {
this.marker = new window.BMapGL.Marker(latLng);
this.map.addOverlay(this.marker);
} else if (this.mapType === MAP_TYPE.G_MAP) {
if (!window.google?.maps.Map) {
window.renderGoogleMap = () => this.renderGoogleMap(position);
loadMapSource(this.mapType, this.mapKey);
} else {
this.renderGoogleMap(position);
}
this.marker = new window.google.maps.marker.AdvancedMarkerElement({
position: latLng,
map: this.map,
});
}
};
addMarkerByPosition = (lng, lat) => {
if (!this.map) {
this.initMap(this.props.position);
return;
}
if (this.mapType === MAP_TYPE.B_MAP) {
if (this.lastLng === lng && this.lastLat === lat) return;
this.lastLng = lng;
this.lastLat = lat;
const point = new window.BMapGL.Point(lng, lat);
const marker = new window.BMapGL.Marker(point, { offset: new window.BMapGL.Size(-2, -5) });
this.map.clearOverlays();
this.map.addOverlay(marker);
this.map.setCenter(point);
}
if (this.mapType === MAP_TYPE.G_MAP) {
if (!this.googleMarker) {
this.googleMarker = new window.google.maps.marker.AdvancedMarkerElement({
position: { lat, lng },
map: this.map,
});
return;
}
this.googleMarker.position = { lat, lng };
this.map.setCenter({ lat, lng });
return;
}
};
renderBaiduMap = (position = {}) => {
renderBaiduMap = () => {
this.setState({ isLoading: false }, () => {
if (!window.BMapGL.Map) return;
window.mapInstance = new window.BMapGL.Map('sf-geolocation-map-container', { enableMapClick: false });
this.map = window.mapInstance;
const gcPosition = wgs84_to_gcj02(position.lng, position.lat);
const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat);
const { lng, lat } = bdPosition;
const point = new window.BMapGL.Point(lng, lat);
this.map.centerAndZoom(point, 16);
const { latLng } = this.state;
this.map = new window.BMapGL.Map('sf-geolocation-map-container');
this.map.disableScrollWheelZoom(true);
this.map.centerAndZoom(latLng, 16);
const offset = { x: 10, y: Utils.isDesktop() ? 16 : 40 };
const ZoomControl = createBMapZoomControl(window.BMapGL, { maxZoom: 21, minZoom: 3, offset });
const zoomControl = new ZoomControl();
this.map.addControl(zoomControl);
this.addMarkerByPosition(lng, lat);
this.addMarker();
});
};
renderGoogleMap = (position) => {
renderGoogleMap = () => {
const { latLng } = this.state;
this.setState({ isLoading: false }, () => {
if (!window.google.maps.Map) return;
const gcPosition = wgs84_to_gcj02(position.lng, position.lat);
const { lng, lat } = gcPosition || {};
window.mapInstance = new window.google.maps.Map(this.ref, {
this.map = new window.google.maps.Map(this.ref, {
zoom: 16,
center: gcPosition,
center: latLng,
mapId: googleMapId,
zoomControl: false,
mapTypeControl: false,
@@ -161,39 +156,77 @@ class Location extends React.Component {
disableDefaultUI: true,
gestureHandling: 'cooperative',
});
this.map = window.mapInstance;
this.addMarkerByPosition(lng, lat);
const zoomControl = createZoomControl(this.map);
this.addMarker();
const zoomControl = createZoomControl({ map: this.map });
this.map.controls[window.google.maps.ControlPosition.RIGHT_BOTTOM].push(zoomControl);
this.map.setCenter(gcPosition);
this.map.panTo(latLng);
});
};
openEditor = () => {
this.setState({ isEditorShown: true });
};
onFullScreen = () => {
this.setState({ isFullScreen: !this.state.isFullScreen });
};
closeEditor = () => {
this.setState({ isEditorShown: false });
if (this.state.isReadyToEraseLocation) {
this.props.onChange(PRIVATE_COLUMN_KEY.LOCATION_TRANSLATED, null);
this.props.onChange(PRIVATE_COLUMN_KEY.LOCATION, null);
this.setState({ latLng: null, address: '', isReadyToEraseLocation: false });
this.mapType === MAP_TYPE.B_MAP && this.map.destroy();
this.map = null;
}
};
onSubmit = (value) => {
const { position, location_translated } = value;
this.props.onChange(PRIVATE_COLUMN_KEY.LOCATION_TRANSLATED, location_translated);
this.props.onChange(PRIVATE_COLUMN_KEY.LOCATION, position);
this.setState({
latLng: position,
address: location_translated?.address,
isEditorShown: false,
isFullScreen: false,
}, () => {
if (!this.map) {
this.initMap();
}
});
};
onReadyToEraseLocation = () => {
this.setState({ isReadyToEraseLocation: true });
};
render() {
const { isLoading, address } = this.state;
const { position } = this.props;
const isValid = isValidPosition(position?.lng, position?.lat);
const { isLoading, latLng, address, isEditorShown, isFullScreen } = this.state;
const isValid = isValidPosition(latLng?.lng, latLng?.lat);
return (
<>
<DetailItem
id="location-item"
field={{
key: PRIVATE_COLUMN_KEY.LOCATION,
type: CellType.GEOLOCATION,
name: getColumnDisplayName(PRIVATE_COLUMN_KEY.LOCATION)
}}
readonly={true}
readonly={false}
>
{isValid ? (
<div className="sf-metadata-ui cell-formatter-container geolocation-formatter sf-metadata-geolocation-formatter">
<div ref={ref => this.editorRef = ref} className="sf-metadata-ui cell-formatter-container geolocation-formatter sf-metadata-geolocation-formatter w-100 cursor-pointer" onClick={this.openEditor}>
{!isLoading && this.mapType && address ? (
<span>{address}</span>
) : (
<span>{getGeolocationDisplayString(position, { geo_format: GEOLOCATION_FORMAT.LNG_LAT })}</span>
<span>{getGeolocationDisplayString(latLng, { geo_format: GEOLOCATION_FORMAT.LNG_LAT })}</span>
)}
</div>
) : (
<div className="sf-metadata-record-cell-empty" placeholder={gettext('Empty')}></div>
<div ref={ref => this.editorRef = ref} className="sf-metadata-property-detail-editor sf-metadata-record-cell-empty cursor-pointer" placeholder={gettext('Empty')} onClick={this.openEditor}></div>
)}
</DetailItem>
{isLoading ? (<Loading />) : this.mapType && (
@@ -201,6 +234,37 @@ class Location extends React.Component {
<div className="w-100 h-100" ref={ref => this.ref = ref} id="sf-geolocation-map-container"></div>
</div>
)}
{isEditorShown && (
!isFullScreen ? (
<ClickOutside onClickOutside={this.closeEditor}>
<Popover
target="location-item"
isOpen={true}
hideArrow={true}
fade={false}
placement="left"
className="sf-metadata-property-detail-editor-popover sf-metadata-geolocation-property-detail-editor-popover"
boundariesElement="viewport"
style={{
width: '100%',
border: 'none',
background: 'transparent',
}}
>
<GeolocationEditor position={latLng} onSubmit={this.onSubmit} onFullScreen={this.onFullScreen} onReadyToEraseLocation={this.onReadyToEraseLocation} />
</Popover>
</ClickOutside>
) : (
<Modal
size='lg'
isOpen={true}
toggle={this.onFullScreen}
zIndex={1052}
>
<GeolocationEditor position={latLng} isFullScreen={isFullScreen} onSubmit={this.onSubmit} onFullScreen={this.onFullScreen} onReadyToEraseLocation={this.onReadyToEraseLocation} />
</Modal>
)
)}
</>
);
}

View File

@@ -93,6 +93,8 @@ export const EDITABLE_PRIVATE_COLUMN_KEYS = [
PRIVATE_COLUMN_KEY.OWNER,
PRIVATE_COLUMN_KEY.FILE_RATE,
PRIVATE_COLUMN_KEY.TAGS,
PRIVATE_COLUMN_KEY.LOCATION,
PRIVATE_COLUMN_KEY.LOCATION_TRANSLATED,
];
export const EDITABLE_DATA_PRIVATE_COLUMN_KEYS = [

View File

@@ -76,13 +76,13 @@ export const createBaiduMap = ({ type, center, zoom, onMapState }) => {
// add controls
const ZoomControl = createBMapZoomControl({
anchor: window.BMAP_ANCHOR_BOTTOM_RIGHT,
offset: new window.BMapGL.Size(66, Utils.isDesktop() ? 30 : 90),
offset: { x: 66, y: 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),
offset: { x: 30, y: Utils.isDesktop() ? 30 : 90 },
callback: (point) => {
const gcPosition = wgs84_to_gcj02(point.lng, point.lat);
const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat);

View File

@@ -82,8 +82,8 @@ export const createGoogleMap = ({ center, zoom, onMapState }) => {
maxZoom: MAX_ZOOM,
});
const zoomControl = createZoomControl(map);
const geolocationControl = createGeolocationControl(map);
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);

View File

@@ -1,5 +1,5 @@
import { MAP_TYPE } from '../constants';
import { mediaUrl } from './constants';
import { lang, mediaUrl } from './constants';
export const initMapInfo = ({ baiduMapKey, googleMapKey, mineMapKey }) => {
if (baiduMapKey) return { type: MAP_TYPE.B_MAP, key: baiduMapKey };
@@ -16,7 +16,7 @@ export const loadMapSource = (type, key, callback) => {
if (type === MAP_TYPE.B_MAP) {
scriptUrl = `https://api.map.baidu.com/api?type=webgl&v=3.0&ak=${key}&callback=renderBaiduMap`;
} else if (type === MAP_TYPE.G_MAP) {
scriptUrl = `https://maps.googleapis.com/maps/api/js?key=${key}&libraries=marker,geometry&v=weekly&callback=renderGoogleMap`;
scriptUrl = `https://maps.googleapis.com/maps/api/js?key=${key}&language=${lang}&libraries=marker,geometry,core,places&v=weekly&callback=renderGoogleMap`;
}
if (scriptUrl) {