diff --git a/frontend/src/assets/icons/full-screen.svg b/frontend/src/assets/icons/full-screen.svg new file mode 100644 index 0000000000..79f63df87e --- /dev/null +++ b/frontend/src/assets/icons/full-screen.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/frontend/src/components/dirent-detail/detail-item/index.js b/frontend/src/components/dirent-detail/detail-item/index.js index e9f86633a9..decdb17209 100644 --- a/frontend/src/components/dirent-detail/detail-item/index.js +++ b/frontend/src/components/dirent-detail/detail-item/index.js @@ -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 ( -
+
{field.name} diff --git a/frontend/src/metadata/components/cell-editors/geolocation-editor/custom-label.js b/frontend/src/metadata/components/cell-editors/geolocation-editor/custom-label.js new file mode 100644 index 0000000000..b6560b035d --- /dev/null +++ b/frontend/src/metadata/components/cell-editors/geolocation-editor/custom-label.js @@ -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 ` +
+ ${` +
+ ${title ? ` + ${title} + + + + ` : ` + ${address} + + + + `} +
+ ${title ? ` + ${tagContent ? `${tagContent}` : ''} + ${gettext('Address')} + ${address} + ` : ''} +
${gettext('Fill in')}
+ `} +
+ `; + } else { + const container = document.createElement('div'); + container.className = title ? 'selection-label-content' : 'selection-label-content simple'; + container.id = 'selection-label-content'; + + const content = ` +
+ ${title ? ` + ${title} + + + + ` : ` + ${address} + + + + `} +
+ ${title ? ` + ${tagContent ? `${tagContent}` : ''} + ${gettext('Address')} + ${address} + ` : ''} +
${gettext('Fill in')}
+ `; + + 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); +}; diff --git a/frontend/src/metadata/components/cell-editors/geolocation-editor/index.css b/frontend/src/metadata/components/cell-editors/geolocation-editor/index.css new file mode 100644 index 0000000000..1db986c618 --- /dev/null +++ b/frontend/src/metadata/components/cell-editors/geolocation-editor/index.css @@ -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; +} diff --git a/frontend/src/metadata/components/cell-editors/geolocation-editor/index.js b/frontend/src/metadata/components/cell-editors/geolocation-editor/index.js new file mode 100644 index 0000000000..efb5441be5 --- /dev/null +++ b/frontend/src/metadata/components/cell-editors/geolocation-editor/index.js @@ -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 ( +
+
+
+ + {gettext('Address')} +
+ +
+
+
+
+ + {inputValue && } +
+ + + +
+
+ {searchResults.length > 0 && ( +
+ {searchResults.map((result, index) => ( +
onSelect(result)}> + {result.title || ''} + {result.address || ''} +
+ ))} +
+ )} +
+
+ ); +}; + +export default GeolocationEditor; diff --git a/frontend/src/metadata/components/map-controller/geolocation.js b/frontend/src/metadata/components/map-controller/geolocation.js index a049cf94de..328dbee745 100644 --- a/frontend/src/metadata/components/map-controller/geolocation.js +++ b/frontend/src/metadata/components/map-controller/geolocation.js @@ -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'); diff --git a/frontend/src/metadata/components/map-controller/index.css b/frontend/src/metadata/components/map-controller/index.css index 80ef7fd67d..3d8e9ed2f0 100644 --- a/frontend/src/metadata/components/map-controller/index.css +++ b/frontend/src/metadata/components/map-controller/index.css @@ -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; } diff --git a/frontend/src/metadata/components/map-controller/zoom.js b/frontend/src/metadata/components/map-controller/zoom.js index 9f2ec7822f..41ca41858f 100644 --- a/frontend/src/metadata/components/map-controller/zoom.js +++ b/frontend/src/metadata/components/map-controller/zoom.js @@ -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(''); @@ -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); diff --git a/frontend/src/metadata/components/metadata-details/index.js b/frontend/src/metadata/components/metadata-details/index.js index 6d637d0ac6..66c455bb33 100644 --- a/frontend/src/metadata/components/metadata-details/index.js +++ b/frontend/src/metadata/components/metadata-details/index.js @@ -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 ; + if (field.key === PRIVATE_COLUMN_KEY.LOCATION && Utils.imageCheck(fileName)) { + return ; } let canEdit = canModifyRecord && field.editable && !readOnly; diff --git a/frontend/src/metadata/components/metadata-details/location/index.css b/frontend/src/metadata/components/metadata-details/location/index.css index 1ac3651832..6889710512 100644 --- a/frontend/src/metadata/components/metadata-details/location/index.css +++ b/frontend/src/metadata/components/metadata-details/location/index.css @@ -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); +} diff --git a/frontend/src/metadata/components/metadata-details/location/index.js b/frontend/src/metadata/components/metadata-details/location/index.js index 1f553c5440..153a7b5c37 100644 --- a/frontend/src/metadata/components/metadata-details/location/index.js +++ b/frontend/src/metadata/components/metadata-details/location/index.js @@ -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 ( <> {isValid ? ( -
+
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 ? ( {address} ) : ( - {getGeolocationDisplayString(position, { geo_format: GEOLOCATION_FORMAT.LNG_LAT })} + {getGeolocationDisplayString(latLng, { geo_format: GEOLOCATION_FORMAT.LNG_LAT })} )}
) : ( -
+
this.editorRef = ref} className="sf-metadata-property-detail-editor sf-metadata-record-cell-empty cursor-pointer" placeholder={gettext('Empty')} onClick={this.openEditor}>
)} {isLoading ? () : this.mapType && ( @@ -201,6 +234,37 @@ class Location extends React.Component {
this.ref = ref} id="sf-geolocation-map-container">
)} + {isEditorShown && ( + !isFullScreen ? ( + + + + + + ) : ( + + + + ) + )} ); } diff --git a/frontend/src/metadata/constants/column/private.js b/frontend/src/metadata/constants/column/private.js index ed357757a0..0130b38bca 100644 --- a/frontend/src/metadata/constants/column/private.js +++ b/frontend/src/metadata/constants/column/private.js @@ -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 = [ diff --git a/frontend/src/metadata/views/map/baidu.js b/frontend/src/metadata/views/map/baidu.js index a30071f8fc..0dd09db89c 100644 --- a/frontend/src/metadata/views/map/baidu.js +++ b/frontend/src/metadata/views/map/baidu.js @@ -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); diff --git a/frontend/src/metadata/views/map/google.js b/frontend/src/metadata/views/map/google.js index eaa20b0580..cd3c37a702 100644 --- a/frontend/src/metadata/views/map/google.js +++ b/frontend/src/metadata/views/map/google.js @@ -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); diff --git a/frontend/src/utils/map-utils.js b/frontend/src/utils/map-utils.js index 6603378646..61947e38aa 100644 --- a/frontend/src/utils/map-utils.js +++ b/frontend/src/utils/map-utils.js @@ -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) { diff --git a/seahub/repo_metadata/utils.py b/seahub/repo_metadata/utils.py index 1a1f258b50..12aeda69c5 100644 --- a/seahub/repo_metadata/utils.py +++ b/seahub/repo_metadata/utils.py @@ -101,7 +101,6 @@ def get_unmodifiable_columns(): METADATA_TABLE.columns.file_name.to_dict(), METADATA_TABLE.columns.is_dir.to_dict(), METADATA_TABLE.columns.file_type.to_dict(), - METADATA_TABLE.columns.location.to_dict(), METADATA_TABLE.columns.obj_id.to_dict(), METADATA_TABLE.columns.size.to_dict(), METADATA_TABLE.columns.suffix.to_dict(),