diff --git a/frontend/src/components/cur-dir-path/dir-path.js b/frontend/src/components/cur-dir-path/dir-path.js index c2cee204af4..4f2c89f1ea5 100644 --- a/frontend/src/components/cur-dir-path/dir-path.js +++ b/frontend/src/components/cur-dir-path/dir-path.js @@ -9,7 +9,7 @@ import { siteRoot, gettext } from '../../utils/constants'; import { Utils } from '../../utils/utils'; import { PRIVATE_FILE_TYPE } from '../../constants'; import { debounce } from '../../metadata/utils/common'; -import { EVENT_BUS_TYPE, FACE_RECOGNITION_VIEW_ID } from '../../metadata/constants'; +import { EVENT_BUS_TYPE } from '../../metadata/constants'; import { ALL_TAGS_ID } from '../../tag/constants'; const propTypes = { @@ -126,13 +126,12 @@ class DirPath extends React.Component { turnViewPathToLink = (pathList) => { if (!Array.isArray(pathList) || pathList.length === 0) return null; const [, , viewId, children] = pathList; - const isViewSupportClick = viewId === FACE_RECOGNITION_VIEW_ID && children; return ( <> / {gettext('Views')} / - {}}> + {}}> {children && ( diff --git a/frontend/src/metadata/components/data-process-setter/gallery-group-by-setter.js b/frontend/src/metadata/components/data-process-setter/gallery-group-by-setter.js new file mode 100644 index 00000000000..2952839c39e --- /dev/null +++ b/frontend/src/metadata/components/data-process-setter/gallery-group-by-setter.js @@ -0,0 +1,37 @@ +import React, { useState, useCallback, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { EVENT_BUS_TYPE, GALLERY_DATE_MODE, STORAGE_GALLERY_DATE_MODE_KEY } from '../../constants'; +import { gettext } from '../../../utils/constants'; +import RadioGroup from '../radio-group'; + +const DATE_MODES = [ + { value: GALLERY_DATE_MODE.YEAR, label: gettext('Year') }, + { value: GALLERY_DATE_MODE.MONTH, label: gettext('Month') }, + { value: GALLERY_DATE_MODE.DAY, label: gettext('Day') }, + { value: GALLERY_DATE_MODE.ALL, label: gettext('All') }, +]; + +const GalleryGroupBySetter = ({ view }) => { + const [currentMode, setCurrentMode] = useState(GALLERY_DATE_MODE.DAY); + + useEffect(() => { + const savedValue = window.sfMetadataContext.localStorage.getItem(STORAGE_GALLERY_DATE_MODE_KEY) || GALLERY_DATE_MODE.DAY; + setCurrentMode(savedValue); + }, [view?._id]); + + const handleGroupByChange = useCallback((newMode) => { + if (currentMode === newMode) return; + setCurrentMode(newMode); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SWITCH_GALLERY_GROUP_BY, newMode); + }, [currentMode]); + + return (); +}; + +GalleryGroupBySetter.propTypes = { + view: PropTypes.shape({ + _id: PropTypes.string + }) +}; + +export default GalleryGroupBySetter; diff --git a/frontend/src/metadata/components/data-process-setter/gallery-group-by-setter/index.css b/frontend/src/metadata/components/data-process-setter/gallery-group-by-setter/index.css deleted file mode 100644 index 69560fc4b1b..00000000000 --- a/frontend/src/metadata/components/data-process-setter/gallery-group-by-setter/index.css +++ /dev/null @@ -1,57 +0,0 @@ -.metadata-gallery-group-by-setter { - width: 272px; - height: 36px; - display: flex; - justify-content: center; - align-items: center; - border: 1px solid #e2e2e2; - border-radius: 3px; -} - -.metadata-gallery-group-by-setter .metadata-gallery-group-by-button { - width: 66px; - height: 28px; - color: #212529; - background-color: #fff; - position: relative; - display: flex; - align-items: center; - justify-content: center; - font-size: 0.875rem; - border: 0; - border-radius: 2px; -} - -.metadata-gallery-group-by-setter .metadata-gallery-group-by-button:hover { - background-color: #f0f0f0; - cursor: pointer; -} - -.metadata-gallery-group-by-setter .metadata-gallery-group-by-button.active { - background-color: #f5f5f5; -} - -.metadata-gallery-group-by-setter .metadata-gallery-group-by-button span { - display: block; - text-align: center; - width: 100%; -} - -.metadata-gallery-group-by-button:not(:first-child)::before { - content: ''; - width: 1px; - height: 22px; - background-color: #e2e2e2; - position: absolute; - top: 50%; - transform: translateY(-50%); - transition: opacity 0.3s; - left: -1px; -} - -.metadata-gallery-group-by-button:hover::before, -.metadata-gallery-group-by-button.active::before, -.metadata-gallery-group-by-button:hover + .metadata-gallery-group-by-button::before, -.metadata-gallery-group-by-button.active + .metadata-gallery-group-by-button::before { - opacity: 0; -} diff --git a/frontend/src/metadata/components/data-process-setter/gallery-group-by-setter/index.js b/frontend/src/metadata/components/data-process-setter/gallery-group-by-setter/index.js deleted file mode 100644 index a79364d7f42..00000000000 --- a/frontend/src/metadata/components/data-process-setter/gallery-group-by-setter/index.js +++ /dev/null @@ -1,51 +0,0 @@ -import React, { useState, useCallback, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import { EVENT_BUS_TYPE, GALLERY_DATE_MODE } from '../../../constants'; -import { gettext } from '../../../../utils/constants'; - -import './index.css'; - -const DATE_MODE_MAP = { - [GALLERY_DATE_MODE.YEAR]: gettext('Year'), - [GALLERY_DATE_MODE.MONTH]: gettext('Month'), - [GALLERY_DATE_MODE.DAY]: gettext('Day'), - [GALLERY_DATE_MODE.ALL]: gettext('All') -}; - -const GalleryGroupBySetter = ({ view }) => { - const [currentMode, setCurrentMode] = useState(GALLERY_DATE_MODE.DAY); - - useEffect(() => { - const savedValue = window.sfMetadataContext.localStorage.getItem('gallery-group-by', GALLERY_DATE_MODE.DAY); - setCurrentMode(savedValue || GALLERY_DATE_MODE.DAY); - }, [view?._id]); - - const handleGroupByChange = useCallback((newMode) => { - setCurrentMode(newMode); - window.sfMetadataContext.localStorage.setItem('gallery-group-by', newMode); - window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SWITCH_GALLERY_GROUP_BY, newMode); - }, []); - - return ( -
- {Object.entries(DATE_MODE_MAP).map(([dateMode, label]) => ( - - ))} -
- ); -}; - -GalleryGroupBySetter.propTypes = { - view: PropTypes.shape({ - _id: PropTypes.string - }) -}; - -export default GalleryGroupBySetter; diff --git a/frontend/src/metadata/components/data-process-setter/index.js b/frontend/src/metadata/components/data-process-setter/index.js index 92add309ebf..fb3425f8abe 100644 --- a/frontend/src/metadata/components/data-process-setter/index.js +++ b/frontend/src/metadata/components/data-process-setter/index.js @@ -1,10 +1,11 @@ -import GalleryGroupBySetter from './gallery-group-by-setter/index'; +import GalleryGroupBySetter from './gallery-group-by-setter'; import GallerySliderSetter from './gallery-slider-setter/index'; import FilterSetter from './filter-setter'; import SortSetter from './sort-setter'; import GroupbySetter from './groupby-setter'; import PreHideColumnSetter from './pre-hide-column-setter'; import HideColumnSetter from './hide-column-setter'; +import MapTypeSetter from './map-type-setter'; export { GalleryGroupBySetter, @@ -14,4 +15,5 @@ export { GroupbySetter, PreHideColumnSetter, HideColumnSetter, + MapTypeSetter, }; diff --git a/frontend/src/metadata/components/data-process-setter/map-type-setter.js b/frontend/src/metadata/components/data-process-setter/map-type-setter.js new file mode 100644 index 00000000000..643d64c1805 --- /dev/null +++ b/frontend/src/metadata/components/data-process-setter/map-type-setter.js @@ -0,0 +1,35 @@ +import React, { useState, useCallback, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { EVENT_BUS_TYPE, MAP_TYPE, STORAGE_MAP_TYPE_KEY } from '../../constants'; +import { gettext } from '../../../utils/constants'; +import RadioGroup from '../radio-group'; + +const MAP_TYPES = [ + { value: MAP_TYPE.MAP, label: gettext('Map') }, + { value: MAP_TYPE.SATELLITE, label: gettext('Satellite') }, +]; + +const MapTypeSetter = ({ view }) => { + const [currentType, setCurrentType] = useState(MAP_TYPE.MAP); + + useEffect(() => { + const type = window.sfMetadataContext.localStorage.getItem(STORAGE_MAP_TYPE_KEY) || MAP_TYPE.MAP; + setCurrentType(type); + }, [view?._id]); + + const onChange = useCallback((type) => { + if (currentType === type) return; + setCurrentType(type); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MODIFY_MAP_TYPE, type); + }, [currentType]); + + return (); +}; + +MapTypeSetter.propTypes = { + view: PropTypes.shape({ + _id: PropTypes.string + }) +}; + +export default MapTypeSetter; diff --git a/frontend/src/metadata/components/popover/filter-popover/basic-filters/file-folder-filter.js b/frontend/src/metadata/components/popover/filter-popover/basic-filters/file-folder-filter.js index 1bf69397271..7a7652b7117 100644 --- a/frontend/src/metadata/components/popover/filter-popover/basic-filters/file-folder-filter.js +++ b/frontend/src/metadata/components/popover/filter-popover/basic-filters/file-folder-filter.js @@ -47,7 +47,7 @@ const FileOrFolderFilter = ({ readOnly, value = 'all', onChange: onChangeAPI }) return ( { return ( { readOnly={readOnly} searchable={true} supportMultipleSelect={true} - className="sf-metadata-basic-filters-select sf-metadata-table-view-basic-filter-file-type-select ml-4" + className="sf-metadata-basic-filters-select sf-metadata-table-view-basic-filter-file-type-select mr-4" value={displayValue} options={options} onSelectOption={onChange} diff --git a/frontend/src/metadata/components/radio-group/index.css b/frontend/src/metadata/components/radio-group/index.css new file mode 100644 index 00000000000..75c6e8705f1 --- /dev/null +++ b/frontend/src/metadata/components/radio-group/index.css @@ -0,0 +1,53 @@ +.sf-metadata-radio-group { + width: fit-content; + height: 36px; + display: flex; + justify-content: center; + align-items: center; + border: 1px solid #e2e2e2; + border-radius: 3px; + padding: 0 3px; +} + +.sf-metadata-radio-group .sf-metadata-radio-group-option { + min-width: 66px; + width: fit-content; + height: 28px; + color: #212529; + background-color: #fff; + position: relative; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.875rem; + border: 0; + border-radius: 2px; +} + +.sf-metadata-radio-group .sf-metadata-radio-group-option:hover { + background-color: #f0f0f0; + cursor: pointer; +} + +.sf-metadata-radio-group .sf-metadata-radio-group-option.active { + background-color: #f5f5f5; +} + +.sf-metadata-radio-group .sf-metadata-radio-group-option:not(:first-child)::before { + content: ''; + width: 1px; + height: 22px; + background-color: #e2e2e2; + position: absolute; + top: 50%; + transform: translateY(-50%); + transition: opacity 0.3s; + left: -1px; +} + +.sf-metadata-radio-group .sf-metadata-radio-group-option:hover::before, +.sf-metadata-radio-group .sf-metadata-radio-group-option.active::before, +.sf-metadata-radio-group .sf-metadata-radio-group-option:hover + .sf-metadata-radio-group-option::before, +.sf-metadata-radio-group .sf-metadata-radio-group-option.active + .sf-metadata-radio-group-option::before { + opacity: 0; +} diff --git a/frontend/src/metadata/components/radio-group/index.js b/frontend/src/metadata/components/radio-group/index.js new file mode 100644 index 00000000000..910f0cd136f --- /dev/null +++ b/frontend/src/metadata/components/radio-group/index.js @@ -0,0 +1,45 @@ +import React, { useCallback, useMemo } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +import './index.css'; + +const RadioGroup = ({ value, options, className, onChange: onChangeAPI }) => { + const selected = useMemo(() => { + const selectedOption = options.find(o => value === o.value) || options[0]; + return selectedOption.value; + }, [value, options]); + + const onChange = useCallback((event) => { + const newValue = event.target.dataset.option; + if (selected === newValue) return; + onChangeAPI(newValue); + }, [selected, onChangeAPI]); + + return ( +
+ {options.map(option => { + const { value, label } = option; + return ( +
+ {label} +
+ ); + })} +
+ ); +}; + +RadioGroup.propTypes = { + value: PropTypes.string, + options: PropTypes.array, + className: PropTypes.string, + onChange: PropTypes.func, +}; + +export default RadioGroup; diff --git a/frontend/src/metadata/components/view-toolbar/face-recognition/index.js b/frontend/src/metadata/components/view-toolbar/face-recognition/index.js index 601aab0a59e..8023d77f5fe 100644 --- a/frontend/src/metadata/components/view-toolbar/face-recognition/index.js +++ b/frontend/src/metadata/components/view-toolbar/face-recognition/index.js @@ -17,17 +17,17 @@ const FaceRecognitionViewToolbar = ({ readOnly, isCustomPermission, onToggleDeta setShow(isShow); }, []); - const setRecognitionView = useCallback(view => { + const resetView = useCallback(view => { setView(view); }, []); const modifySorts = useCallback((sorts) => { - window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.FACE_RECOGNITION_VIEW_CHANGE, { sorts }); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.UPDATE_SERVER_VIEW, { sorts }); }, []); useEffect(() => { const unsubscribeToggle = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, onToggle); - const unsubscribeView = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.FACE_RECOGNITION_VIEW, setRecognitionView); + const unsubscribeView = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.RESET_VIEW, resetView); return () => { unsubscribeToggle && unsubscribeToggle(); unsubscribeView && unsubscribeView(); diff --git a/frontend/src/metadata/components/view-toolbar/index.js b/frontend/src/metadata/components/view-toolbar/index.js index 7bfb186b5c3..8ced5ef287a 100644 --- a/frontend/src/metadata/components/view-toolbar/index.js +++ b/frontend/src/metadata/components/view-toolbar/index.js @@ -94,8 +94,8 @@ const ViewToolBar = ({ viewId, isCustomPermission, onToggleDetail, onCloseDetail )} {viewType === VIEW_TYPE.FACE_RECOGNITION && ( )} @@ -114,9 +114,11 @@ const ViewToolBar = ({ viewId, isCustomPermission, onToggleDetail, onCloseDetail {viewType === VIEW_TYPE.MAP && ( )} diff --git a/frontend/src/metadata/components/view-toolbar/map-view-toolbar/index.js b/frontend/src/metadata/components/view-toolbar/map-view-toolbar/index.js index 4be657a74f8..58eca8719e6 100644 --- a/frontend/src/metadata/components/view-toolbar/map-view-toolbar/index.js +++ b/frontend/src/metadata/components/view-toolbar/map-view-toolbar/index.js @@ -1,27 +1,83 @@ -import React, { useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; -import { PRIVATE_COLUMN_KEY } from '../../../constants'; -import { FilterSetter } from '../../data-process-setter'; +import { EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY, VIEW_TYPE } from '../../../constants'; +import { FilterSetter, GalleryGroupBySetter, GallerySliderSetter, MapTypeSetter, SortSetter } from '../../data-process-setter'; +import { gettext } from '../../../../utils/constants'; const MapViewToolBar = ({ + isCustomPermission, readOnly, - view, + viewID, collaborators, modifyFilters, + onToggleDetail, }) => { - const viewType = useMemo(() => view.type, [view]); + const [showGalleryToolbar, setShowGalleryToolbar] = useState(false); + const [view, setView] = useState({}); + + const viewType = useMemo(() => VIEW_TYPE.MAP, []); const viewColumns = useMemo(() => { if (!view) return []; return view.columns; }, [view]); const filterColumns = useMemo(() => { - return viewColumns.filter(c => c.key !== PRIVATE_COLUMN_KEY.FILE_TYPE); + return viewColumns && viewColumns.filter(c => c.key !== PRIVATE_COLUMN_KEY.FILE_TYPE); }, [viewColumns]); + const onToggle = useCallback((value) => { + setShowGalleryToolbar(value); + }, []); + + const modifySorts = useCallback((sorts) => { + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.UPDATE_SERVER_VIEW, { sorts }); + }, []); + + const resetView = useCallback(view => { + setView(view); + }, []); + + useEffect(() => { + setShowGalleryToolbar(false); + const unsubscribeToggle = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, onToggle); + const unsubscribeView = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.RESET_VIEW, resetView); + return () => { + unsubscribeToggle && unsubscribeToggle(); + unsubscribeView && unsubscribeView(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [viewID]); + + if (showGalleryToolbar) { + return ( + <> +
+ + + + {!isCustomPermission && ( +
+ +
+ )} +
+
+ + ); + } + return ( <>
+ GALLERY_SORT_COLUMN_OPTIONS.includes(column.type) || GALLERY_SORT_PRIVATE_COLUMN_KEYS.includes(column.key), [VIEW_TYPE.FACE_RECOGNITION]: (column) => GALLERY_SORT_COLUMN_OPTIONS.includes(column.type) || GALLERY_SORT_PRIVATE_COLUMN_KEYS.includes(column.key), [VIEW_TYPE.KANBAN]: (column) => SORT_COLUMN_OPTIONS.includes(column.type), - [VIEW_TYPE.MAP]: () => {}, + [VIEW_TYPE.MAP]: (column) => GALLERY_SORT_COLUMN_OPTIONS.includes(column.type) || GALLERY_SORT_PRIVATE_COLUMN_KEYS.includes(column.key), }; export const VIEW_FIRST_SORT_COLUMN_RULES = { @@ -114,7 +119,7 @@ export const VIEW_FIRST_SORT_COLUMN_RULES = { [VIEW_TYPE.GALLERY]: (column) => GALLERY_FIRST_SORT_COLUMN_OPTIONS.includes(column.type) || GALLERY_FIRST_SORT_PRIVATE_COLUMN_KEYS.includes(column.key), [VIEW_TYPE.FACE_RECOGNITION]: (column) => GALLERY_FIRST_SORT_COLUMN_OPTIONS.includes(column.type) || GALLERY_FIRST_SORT_PRIVATE_COLUMN_KEYS.includes(column.key), [VIEW_TYPE.KANBAN]: (column) => SORT_COLUMN_OPTIONS.includes(column.type), - [VIEW_TYPE.MAP]: () => {}, + [VIEW_TYPE.MAP]: (column) => GALLERY_FIRST_SORT_COLUMN_OPTIONS.includes(column.type) || GALLERY_FIRST_SORT_PRIVATE_COLUMN_KEYS.includes(column.key), }; export const KANBAN_SETTINGS_KEYS = { diff --git a/frontend/src/metadata/constants/view/kanban.js b/frontend/src/metadata/constants/view/kanban.js new file mode 100644 index 00000000000..d9d7085b5e0 --- /dev/null +++ b/frontend/src/metadata/constants/view/kanban.js @@ -0,0 +1 @@ +export const UNCATEGORIZED = '_uncategorized'; diff --git a/frontend/src/metadata/constants/view/map.js b/frontend/src/metadata/constants/view/map.js new file mode 100644 index 00000000000..d9511628ffa --- /dev/null +++ b/frontend/src/metadata/constants/view/map.js @@ -0,0 +1,15 @@ +export const MAP_TYPE = { + MAP: 'map', + SATELLITE: 'satellite', +}; + +export const STORAGE_MAP_TYPE_KEY = 'map_type'; + +export const STORAGE_MAP_CENTER_KEY = 'map_center'; + +export const STORAGE_MAP_ZOOM_KEY = 'map_zoom'; + +export const MAP_VIEW_TOOLBAR_MODE = { + MAP: 'map', + GALLERY: 'gallery', +}; diff --git a/frontend/src/metadata/constants/view/table.js b/frontend/src/metadata/constants/view/table.js new file mode 100644 index 00000000000..aa056116516 --- /dev/null +++ b/frontend/src/metadata/constants/view/table.js @@ -0,0 +1,100 @@ +import { CellType } from '../column'; + +export const CELL_NAVIGATION_MODE = { + NONE: 'none', + CHANGE_ROW: 'changeRow', + LOOP_OVER_ROW: 'loopOverRow', +}; + +export const SEQUENCE_COLUMN_WIDTH = 80; + +export const ROW_HEIGHT = 32; + +export const GRID_HEADER_DEFAULT_HEIGHT = 32; + +export const GRID_HEADER_DOUBLE_HEIGHT = 56; + +export const GROUP_VIEW_OFFSET = 16; + +export const GROUP_HEADER_HEIGHT = 48; + +export const TABLE_LEFT_MARGIN = 10; + +export const TABLE_BORDER_WIDTH = 1; + +export const UNABLE_TO_CALCULATE = '--'; + +export const FROZEN_COLUMN_SHADOW = '2px 0 5px -2px hsla(0,0%,53.3%,.3)'; + +export const TABLE_NOT_SUPPORT_EDIT_TYPE_MAP = { + [CellType.CREATOR]: true, + [CellType.LAST_MODIFIER]: true, + [CellType.CTIME]: true, + [CellType.MTIME]: true, + [CellType.FILE_NAME]: true, +}; + +export const TABLE_SUPPORT_EDIT_TYPE_MAP = { + [CellType.TEXT]: true, + [CellType.DATE]: true, + [CellType.NUMBER]: true, + [CellType.SINGLE_SELECT]: true, + [CellType.MULTIPLE_SELECT]: true, + [CellType.COLLABORATOR]: true, + [CellType.CHECKBOX]: true, + [CellType.LONG_TEXT]: true, + [CellType.LINK]: true, + [CellType.TAGS]: true, +}; + +export const TABLE_MOBILE_SUPPORT_EDIT_CELL_TYPE_MAP = { + [CellType.TEXT]: true, +}; + +export const CANVAS_RIGHT_INTERVAL = 44; + +export const LEFT_NAV = 280; +export const ROW_DETAIL_PADDING = 40 * 2; +export const ROW_DETAIL_MARGIN = 20 * 2; +export const EDITOR_PADDING = 1.5 * 16; // 1.5: 0.75 * 2 + +export const GROUP_ROW_TYPE = { + GROUP_CONTAINER: 'group_container', + ROW: 'row', + BTN_INSERT_ROW: 'btn_insert_row', +}; + +export const INSERT_ROW_HEIGHT = 32; + +export const CHANGE_HEADER_WIDTH = 'CHANGE_HEADER_WIDTH'; + +export const NOT_SUPPORT_DRAG_COPY_COLUMN_TYPES = [ +]; + +export const SUPPORT_PREVIEW_COLUMN_TYPES = []; + +export const OVER_SCAN_COLUMNS = 10; + +export const DELETED_OPTION_BACKGROUND_COLOR = '#eaeaea'; + +export const DELETED_OPTION_TIPS = 'deleted_option'; + +export const SUPPORT_BATCH_DOWNLOAD_TYPES = []; + +export const PER_LOAD_NUMBER = 1000; + +export const DEFAULT_RETRY_TIMES = 4; + +export const DEFAULT_RETRY_INTERVAL = 1000; + +export const MAX_LOAD_NUMBER = 10000; + +export const EDITOR_TYPE = { + PREVIEWER: 'previewer', + ADDITION: 'addition', +}; + +export const PASTE_SOURCE = { + COPY: 'copy', + CUT: 'cut', +}; diff --git a/frontend/src/metadata/hooks/metadata-view.js b/frontend/src/metadata/hooks/metadata-view.js index f7397091ba8..1b0ee43808e 100644 --- a/frontend/src/metadata/hooks/metadata-view.js +++ b/frontend/src/metadata/hooks/metadata-view.js @@ -340,6 +340,8 @@ export const MetadataViewProvider = ({ { * @param {object} value e.g. { collaborators, ... } * @returns sorted rows ids, array */ -const sortTableRows = (table, rows, sorts, { collaborators }) => { +const sortTableRows = (table, rows, sorts, { collaborators, isReturnID = true } = {}) => { const { columns } = table; if (!Array.isArray(rows) || rows.length === 0) return []; const sortRows = rows.slice(0); const validSorts = deleteInvalidSort(sorts, columns); sortRowsWithMultiSorts(sortRows, validSorts, { collaborators }); - return sortRows.map((row) => row._id); + return isReturnID ? sortRows.map((row) => row._id) : sortRows; }; export { diff --git a/frontend/src/metadata/views/face-recognition/person-photos/index.js b/frontend/src/metadata/views/face-recognition/person-photos/index.js index 6c24967be97..f8692af4258 100644 --- a/frontend/src/metadata/views/face-recognition/person-photos/index.js +++ b/frontend/src/metadata/views/face-recognition/person-photos/index.js @@ -1,6 +1,8 @@ import React, { useCallback, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import deepCopy from 'deep-copy'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; import { CenteredLoading } from '@seafile/sf-metadata-ui-component'; import metadataAPI from '../../../api'; import Metadata from '../../../model/metadata'; @@ -10,11 +12,16 @@ import { Utils } from '../../../../utils/utils'; import toaster from '../../../../components/toast'; import Gallery from '../../gallery/main'; import { useMetadataView } from '../../../hooks/metadata-view'; -import { PER_LOAD_NUMBER, EVENT_BUS_TYPE, FACE_RECOGNITION_VIEW_ID } from '../../../constants'; +import { PER_LOAD_NUMBER, EVENT_BUS_TYPE, FACE_RECOGNITION_VIEW_ID, UTC_FORMAT_DEFAULT } from '../../../constants'; +import { getRecordIdFromRecord, getParentDirFromRecord, getFileNameFromRecord } from '../../../utils/cell'; +import { sortTableRows } from '../../../utils/sort'; +import { useCollaborators } from '../../../hooks/collaborators'; import './index.css'; import '../../gallery/index.css'; +dayjs.extend(utc); + const PeoplePhotos = ({ view, people, onClose, onDeletePeoplePhotos, onRemovePeoplePhotos }) => { const [isLoading, setLoading] = useState(true); const [isLoadingMore, setLoadingMore] = useState(false); @@ -22,6 +29,7 @@ const PeoplePhotos = ({ view, people, onClose, onDeletePeoplePhotos, onRemovePeo const repoID = window.sfMetadataContext.getSetting('repoID'); const { deleteFilesCallback, store } = useMetadataView(); + const { collaborators } = useCollaborators(); const onLoadMore = useCallback(async () => { if (isLoadingMore) return; @@ -130,7 +138,7 @@ const PeoplePhotos = ({ view, people, onClose, onDeletePeoplePhotos, onRemovePeo metadata.hasMore = false; } setMetadata(metadata); - window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.FACE_RECOGNITION_VIEW, metadata.view); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.RESET_VIEW, metadata.view); setLoading(false); }).catch(error => { const errorMessage = Utils.getErrorMsg(error); @@ -150,6 +158,38 @@ const PeoplePhotos = ({ view, people, onClose, onDeletePeoplePhotos, onRemovePeo }); }, [repoID, metadata, store, loadData]); + const onRecordChange = useCallback(({ recordId, parentDir, fileName }, update) => { + const modifyTime = dayjs().utc().format(UTC_FORMAT_DEFAULT); + const modifier = window.sfMetadataContext.getUsername(); + const { rows, columns, view } = metadata; + let newRows = [...rows]; + newRows.forEach((row, index) => { + const _rowId = getRecordIdFromRecord(row); + const _parentDir = getParentDirFromRecord(row); + const _fileName = getFileNameFromRecord(row); + if ((_rowId === recordId || (_parentDir === parentDir && _fileName === fileName)) && update) { + const updatedRow = Object.assign({}, row, update, { + '_mtime': modifyTime, + '_last_modifier': modifier, + }); + newRows[index] = updatedRow; + } + }); + let updatedColumnKeyMap = { + '_mtime': true, + '_last_modifier': true + }; + Object.keys(update).forEach(key => { + updatedColumnKeyMap[key] = true; + }); + if (view.sorts.some(sort => updatedColumnKeyMap[sort.column_key])) { + newRows = sortTableRows({ columns }, newRows, view?.sorts || [], { collaborators, isReturnID: false }); + } + let newMetadata = new Metadata({ rows: newRows, columns, view }); + newMetadata.hasMore = false; + setMetadata(newMetadata); + }, [metadata, collaborators]); + useEffect(() => { loadData({ sorts: view.sorts }); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -164,11 +204,15 @@ const PeoplePhotos = ({ view, people, onClose, onDeletePeoplePhotos, onRemovePeo }, []); useEffect(() => { - const unsubscribeViewChange = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.FACE_RECOGNITION_VIEW_CHANGE, onViewChange); + const eventBus = window?.sfMetadataContext?.eventBus; + if (!eventBus) return; + const unsubscribeViewChange = eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_SERVER_VIEW, onViewChange); + const localRecordChangedSubscribe = eventBus.subscribe(EVENT_BUS_TYPE.LOCAL_RECORD_CHANGED, onRecordChange); return () => { unsubscribeViewChange && unsubscribeViewChange(); + localRecordChangedSubscribe && localRecordChangedSubscribe(); }; - }, [onViewChange]); + }, [onViewChange, onRecordChange]); if (isLoading) return (); diff --git a/frontend/src/metadata/views/gallery/context-menu/index.js b/frontend/src/metadata/views/gallery/context-menu/index.js index 307d39d8c7c..756989506b6 100644 --- a/frontend/src/metadata/views/gallery/context-menu/index.js +++ b/frontend/src/metadata/views/gallery/context-menu/index.js @@ -30,17 +30,17 @@ const GalleryContextMenu = ({ metadata, selectedImages, onDelete, onDuplicate, a const options = useMemo(() => { let validOptions = [{ value: CONTEXT_MENU_KEY.DOWNLOAD, label: gettext('Download') }]; - if (checkCanDeleteRow) { + if (onDelete && checkCanDeleteRow) { validOptions.push({ value: CONTEXT_MENU_KEY.DELETE, label: selectedImages.length > 1 ? gettext('Delete') : gettext('Delete file') }); } - if (canDuplicateRow && selectedImages.length === 1) { + if (onDuplicate && canDuplicateRow && selectedImages.length === 1) { validOptions.push({ value: CONTEXT_MENU_KEY.DUPLICATE, label: gettext('Duplicate') }); } - if (canRemovePhotoFromPeople) { + if (onRemoveImage && canRemovePhotoFromPeople) { validOptions.push({ value: CONTEXT_MENU_KEY.REMOVE, label: gettext('Remove from this group') }); } return validOptions; - }, [checkCanDeleteRow, canDuplicateRow, canRemovePhotoFromPeople, selectedImages]); + }, [checkCanDeleteRow, canDuplicateRow, canRemovePhotoFromPeople, selectedImages, onDuplicate, onDelete, onRemoveImage]); const closeZipDialog = () => { setIsZipDialogOpen(false); diff --git a/frontend/src/metadata/views/gallery/main.js b/frontend/src/metadata/views/gallery/main.js index 1b82a10dc78..76ba2b1f59b 100644 --- a/frontend/src/metadata/views/gallery/main.js +++ b/frontend/src/metadata/views/gallery/main.js @@ -8,7 +8,7 @@ import { useMetadataView } from '../../hooks/metadata-view'; import { Utils } from '../../../utils/utils'; import { getDateDisplayString, getFileNameFromRecord, getParentDirFromRecord, getRecordIdFromRecord } from '../../utils/cell'; import { siteRoot, fileServerRoot, thumbnailSizeForGrid, thumbnailSizeForOriginal } from '../../../utils/constants'; -import { EVENT_BUS_TYPE, GALLERY_DATE_MODE, DATE_TAG_HEIGHT, GALLERY_IMAGE_GAP } from '../../constants'; +import { EVENT_BUS_TYPE, GALLERY_DATE_MODE, DATE_TAG_HEIGHT, GALLERY_IMAGE_GAP, STORAGE_GALLERY_DATE_MODE_KEY } from '../../constants'; import { getRowById } from '../../utils/table'; import { getEventClassName } from '../../utils/common'; import GalleryContextmenu from './context-menu'; @@ -129,14 +129,14 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, const gear = window.sfMetadataContext.localStorage.getItem('zoom-gear', 0) || 0; setZoomGear(gear); - const mode = window.sfMetadataContext.localStorage.getItem('gallery-group-by', GALLERY_DATE_MODE.DAY) || GALLERY_DATE_MODE.DAY; + const mode = window.sfMetadataContext.localStorage.getItem(STORAGE_GALLERY_DATE_MODE_KEY, GALLERY_DATE_MODE.DAY) || GALLERY_DATE_MODE.DAY; setMode(mode); const switchGalleryModeSubscribe = window.sfMetadataContext.eventBus.subscribe( EVENT_BUS_TYPE.SWITCH_GALLERY_GROUP_BY, (mode) => { setMode(mode); - window.sfMetadataContext.localStorage.setItem('gallery-group-by', mode); + window.sfMetadataContext.localStorage.setItem(STORAGE_GALLERY_DATE_MODE_KEY, mode); } ); @@ -193,7 +193,7 @@ const Main = ({ isLoadingMore, metadata, onDelete, onLoadMore, duplicateRecord, if (!containerRef.current) return; const { scrollTop, scrollHeight, clientHeight } = containerRef.current; if (scrollTop + clientHeight >= scrollHeight - 10) { - onLoadMore(); + onLoadMore && onLoadMore(); } else { const { scrollTop, clientHeight } = containerRef.current; const overScanTop = Math.max(0, scrollTop - (imageSize + GALLERY_IMAGE_GAP) * OVER_SCAN_ROWS); diff --git a/frontend/src/metadata/views/map/cluster-photos/index.css b/frontend/src/metadata/views/map/cluster-photos/index.css new file mode 100644 index 00000000000..e190c9727a2 --- /dev/null +++ b/frontend/src/metadata/views/map/cluster-photos/index.css @@ -0,0 +1,4 @@ +.sf-metadata-map-photos-container { + padding: 0 !important; + overflow: hidden !important; +} diff --git a/frontend/src/metadata/views/map/cluster-photos/index.js b/frontend/src/metadata/views/map/cluster-photos/index.js new file mode 100644 index 00000000000..4678e616fc7 --- /dev/null +++ b/frontend/src/metadata/views/map/cluster-photos/index.js @@ -0,0 +1,159 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import deepCopy from 'deep-copy'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import { CenteredLoading } from '@seafile/sf-metadata-ui-component'; +import Gallery from '../../gallery/main'; +import { EVENT_BUS_TYPE, UTC_FORMAT_DEFAULT } from '../../../constants'; +import metadataAPI from '../../../api'; +import { Utils } from '../../../../utils/utils'; +import toaster from '../../../../components/toast'; +import { useMetadataView } from '../../../hooks/metadata-view'; +import { getRowsByIds } from '../../../utils/table'; +import Metadata from '../../../model/metadata'; +import { sortTableRows } from '../../../utils/sort'; +import { useCollaborators } from '../../../hooks/collaborators'; +import { getRecordIdFromRecord, getParentDirFromRecord, getFileNameFromRecord } from '../../../utils/cell'; + +import './index.css'; + +dayjs.extend(utc); + +const ClusterPhotos = ({ photoIds, onClose }) => { + const { repoID, viewID, metadata: allMetadata, store, addFolder, deleteRecords } = useMetadataView(); + const { collaborators } = useCollaborators(); + + const [isLoading, setLoading] = useState(true); + const [metadata, setMetadata] = useState({ rows: getRowsByIds(allMetadata, photoIds), columns: allMetadata?.columns || [] }); + + const loadData = useCallback((view) => { + setLoading(true); + const columns = metadata.columns; + const orderRows = sortTableRows({ columns }, metadata.rows, view?.sorts || [], { collaborators, isReturnID: false }); + let newMetadata = new Metadata({ rows: orderRows, columns, view }); + newMetadata.hasMore = false; + setMetadata(newMetadata); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.RESET_VIEW, newMetadata.view); + setLoading(false); + }, [metadata, collaborators]); + + const deletedByIds = useCallback((ids) => { + if (!Array.isArray(ids) || ids.length === 0) return; + const newMetadata = deepCopy(metadata); + const idNeedDeletedMap = ids.reduce((currIdNeedDeletedMap, rowId) => ({ ...currIdNeedDeletedMap, [rowId]: true }), {}); + newMetadata.rows = newMetadata.rows.filter((row) => !idNeedDeletedMap[row._id]); + newMetadata.row_ids = newMetadata.row_ids.filter((id) => !idNeedDeletedMap[id]); + + // delete rows in id_row_map + ids.forEach(rowId => { + delete newMetadata.id_row_map[rowId]; + }); + newMetadata.recordsCount = newMetadata.row_ids.length; + setMetadata(newMetadata); + + if (newMetadata.rows.length === 0) { + onClose && onClose(); + } + }, [metadata, onClose]); + + const handelDelete = useCallback((deletedImages, { success_callback } = {}) => { + if (!deletedImages.length) return; + let recordIds = []; + deletedImages.forEach((record) => { + const { id, parentDir, name } = record || {}; + if (parentDir && name) { + recordIds.push(id); + } + }); + deleteRecords(recordIds, { + success_callback: () => { + success_callback(); + deletedByIds(recordIds); + } + }); + }, [deleteRecords, deletedByIds]); + + const onViewChange = useCallback((update) => { + metadataAPI.modifyView(repoID, viewID, update).then(res => { + store.modifyLocalView(update); + const newView = { ...metadata.view, ...update }; + loadData(newView); + }).catch(error => { + const errorMessage = Utils.getErrorMsg(error); + toaster.danger(errorMessage); + }); + }, [metadata, repoID, viewID, store, loadData]); + + const onRecordChange = useCallback(({ recordId, parentDir, fileName }, update) => { + const modifyTime = dayjs().utc().format(UTC_FORMAT_DEFAULT); + const modifier = window.sfMetadataContext.getUsername(); + const { rows, columns, view } = metadata; + let newRows = [...rows]; + newRows.forEach((row, index) => { + const _rowId = getRecordIdFromRecord(row); + const _parentDir = getParentDirFromRecord(row); + const _fileName = getFileNameFromRecord(row); + if ((_rowId === recordId || (_parentDir === parentDir && _fileName === fileName)) && update) { + const updatedRow = Object.assign({}, row, update, { + '_mtime': modifyTime, + '_last_modifier': modifier, + }); + newRows[index] = updatedRow; + } + }); + let updatedColumnKeyMap = { + '_mtime': true, + '_last_modifier': true + }; + Object.keys(update).forEach(key => { + updatedColumnKeyMap[key] = true; + }); + if (view.sorts.some(sort => updatedColumnKeyMap[sort.column_key])) { + newRows = sortTableRows({ columns }, newRows, view?.sorts || [], { collaborators, isReturnID: false }); + } + let newMetadata = new Metadata({ rows: newRows, columns, view }); + newMetadata.hasMore = false; + setMetadata(newMetadata); + }, [metadata, collaborators]); + + useEffect(() => { + const eventBus = window?.sfMetadataContext?.eventBus; + if (!eventBus) return; + eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, true); + return () => { + eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_VIEW_TOOLBAR, false); + }; + }, []); + + useEffect(() => { + const eventBus = window?.sfMetadataContext?.eventBus; + if (!eventBus) return; + const unsubscribeViewChange = eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_SERVER_VIEW, onViewChange); + const localRecordChangedSubscribe = eventBus.subscribe(EVENT_BUS_TYPE.LOCAL_RECORD_CHANGED, onRecordChange); + return () => { + unsubscribeViewChange && unsubscribeViewChange(); + localRecordChangedSubscribe && localRecordChangedSubscribe(); + }; + }, [onViewChange, onRecordChange]); + + useEffect(() => { + loadData({ sorts: allMetadata.view.sorts }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (isLoading) return (); + + return ( +
+ +
+ ); +}; + +ClusterPhotos.propTypes = { + photoIds: PropTypes.array, + onClose: PropTypes.func, +}; + +export default ClusterPhotos; diff --git a/frontend/src/metadata/views/map/geolocation-control.js b/frontend/src/metadata/views/map/geolocation-control.js deleted file mode 100644 index 3a576253a4f..00000000000 --- a/frontend/src/metadata/views/map/geolocation-control.js +++ /dev/null @@ -1,50 +0,0 @@ -import { mediaUrl } from '../../../utils/constants'; -import { Utils } from '../../../utils/utils'; - -export function createBMapGeolocationControl(BMap, callback) { - function GeolocationControl() { - this.defaultAnchor = window.BMAP_ANCHOR_BOTTOM_RIGHT; - this.defaultOffset = new BMap.Size(10, Utils.isDesktop() ? 20 : 90); - } - GeolocationControl.prototype = new window.BMap.Control(); - GeolocationControl.prototype.initialize = function (map) { - const div = document.createElement('div'); - div.className = 'sf-BMap-geolocation-control'; - div.style = 'display: flex; justify-content: center; align-items: center;'; - - const icon = document.createElement('img'); - icon.className = 'sf-BMap-icon-current-location'; - icon.src = `${mediaUrl}/img/current-location.svg`; - icon.style = 'width: 16px; height: 16px; display: block;'; - div.appendChild(icon); - if (Utils.isDesktop()) { - setNodeStyle(div, 'height: 30px; width: 30px; line-height: 30px'); - } else { - setNodeStyle(div, 'height: 35px; width: 35px; line-height: 35px; opacity: 0.75'); - } - div.onclick = (e) => { - e.preventDefault(); - const geolocation = new BMap.Geolocation(); - div.className = 'sf-BMap-geolocation-control sf-BMap-geolocation-control-loading'; - geolocation.getCurrentPosition((result) => { - div.className = 'sf-BMap-geolocation-control'; - if (result) { - const point = result.point; - map.setCenter(point); - callback(null, point); - } else { - // Positioning failed - callback(true); - } - }); - }; - map.getContainer().appendChild(div); - return div; - }; - - return GeolocationControl; -} - -function setNodeStyle(dom, styleText) { - dom.style.cssText += styleText; -} diff --git a/frontend/src/metadata/views/map/index.css b/frontend/src/metadata/views/map/index.css index 11b00f6ea1e..22ff37c9430 100644 --- a/frontend/src/metadata/views/map/index.css +++ b/frontend/src/metadata/views/map/index.css @@ -1,85 +1,6 @@ .sf-metadata-view-map { + width: 100%; + height: 100%; display: flex; flex-direction: column; } - -.sf-metadata-view-map .sf-metadata-map-container { - width: 100%;; - height: 100%; - min-height: 0; -} - -.sf-metadata-view-map .custom-image-container { - padding: 4px; - background: #fff; - width: 80px; - height: 80px; - cursor: default; - border-radius: 4px; - box-shadow: 0 1px 3px 0 rgb(0 0 0 / 10%); - position: relative; -} - -.sf-metadata-view-map .custom-image-number { - position: absolute; - right: -15px; - top: -8px; - padding: 0 12px; - background: #20AD7E; - color: #fff; - border-radius: 10px; - text-align: center; - font-size: 14px; - line-height: 20px; -} - -.sf-metadata-view-map .custom-image-container .plugin-label-arrow { - position: absolute; - bottom: 5px; - transform: translate( -50%, 100%); - left: 50%; - color: #fff; - display: inline-block; - line-height: 16px; - height: 16px; -} - -.sf-metadata-view-map .custom-image-container .image-overlay-arrow { - bottom: 5px; - color: #fff; - display: inline-block; - height: 16px; - left: 50%; - line-height: 16px; - position: absolute; - transform: translate(-50%, 100%); -} - -.sf-metadata-view-map .custom-image-container::after { - content: ''; - position: absolute; - bottom: -10px; - left: 50%; - transform: translateX(-50%); - width: 0; - height: 0; - border-left: 10px solid transparent; - border-right: 10px solid transparent; - border-top: 10px solid #fff; -} - -.sf-metadata-view-map .sf-BMap-geolocation-control { - background-color: #ffffff; - box-shadow: 0 0 4px rgb(0 0 0 / 12%); - border-radius: 4px; - text-align: center; - color: #212529; -} - -.sf-metadata-view-map .sf-BMap-geolocation-control-loading { - opacity: 0.7; -} - -.sf-metadata-view-map .sf-BMap-geolocation-control:hover { - background-color: #f5f5f5; -} diff --git a/frontend/src/metadata/views/map/index.js b/frontend/src/metadata/views/map/index.js index ce1f75733e1..df78a5a9914 100644 --- a/frontend/src/metadata/views/map/index.js +++ b/frontend/src/metadata/views/map/index.js @@ -1,40 +1,26 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { CenteredLoading } from '@seafile/sf-metadata-ui-component'; -import loadBMap, { initMapInfo } from '../../../utils/map-utils'; -import { wgs84_to_gcj02, gcj02_to_bd09 } from '../../../utils/coord-transform'; -import { MAP_TYPE } from '../../../constants'; -import { isValidPosition } from '../../utils/validate'; -import { appAvatarURL, baiduMapKey, gettext, googleMapKey, mediaUrl, siteRoot, thumbnailSizeForGrid } from '../../../utils/constants'; -import { useMetadataView } from '../../hooks/metadata-view'; +import { getFileNameFromRecord, getFileTypeFromRecord, getImageLocationFromRecord, getParentDirFromRecord, getRecordIdFromRecord } from '../../utils/cell'; +import ClusterPhotos from './cluster-photos'; +import MapView from './map-view'; import { PREDEFINED_FILE_TYPE_OPTION_KEY } from '../../constants'; -import { getRecordIdFromRecord, getFileNameFromRecord, getImageLocationFromRecord, getParentDirFromRecord, - getFileTypeFromRecord -} from '../../utils/cell'; +import { useMetadataView } from '../../hooks/metadata-view'; import { Utils } from '../../../utils/utils'; -import customImageOverlay from './custom-image-overlay'; -import customAvatarOverlay from './custom-avatar-overlay'; -import { createBMapGeolocationControl } from './geolocation-control'; -import toaster from '../../../components/toast'; +import { gettext, siteRoot, thumbnailSizeForGrid } from '../../../utils/constants'; +import { isValidPosition } from '../../utils/validate'; +import { gcj02_to_bd09, wgs84_to_gcj02 } from '../../../utils/coord-transform'; +import { PRIVATE_FILE_TYPE } from '../../../constants'; import './index.css'; -const DEFAULT_POSITION = { lng: 104.195, lat: 35.861 }; -const DEFAULT_ZOOM = 4; -const BATCH_SIZE = 500; - const Map = () => { - const [isLoading, setIsLoading] = useState(true); - - const mapRef = useRef(null); - const clusterRef = useRef(null); - const batchIndexRef = useRef(0); + const [showCluster, setShowCluster] = useState(false); + const { metadata, viewID, updateCurrentPath } = useMetadataView(); - const { metadata } = useMetadataView(); + const clusterRef = useRef([]); - const mapInfo = useMemo(() => initMapInfo({ baiduMapKey, googleMapKey }), []); const repoID = window.sfMetadataContext.getSetting('repoID'); - const validImages = useMemo(() => { + const images = useMemo(() => { return metadata.rows .map(record => { const recordType = getFileTypeFromRecord(record); @@ -53,124 +39,31 @@ const Map = () => { return { id, src, lng: bdPosition.lng, lat: bdPosition.lat }; }) .filter(Boolean); - }, [repoID, metadata]); - - const addMapController = useCallback(() => { - var navigation = new window.BMap.NavigationControl(); - const GeolocationControl = createBMapGeolocationControl(window.BMap, (err, point) => { - if (!err && point) { - mapRef.current.setCenter({ lng: point.lng, lat: point.lat }); - } - }); - const geolocationControl = new GeolocationControl(); - mapRef.current.addControl(geolocationControl); - mapRef.current.addControl(navigation); - }, []); - - const renderMarkersBatch = useCallback(() => { - if (!validImages.length || !clusterRef.current) return; - - const startIndex = batchIndexRef.current * BATCH_SIZE; - const endIndex = Math.min(startIndex + BATCH_SIZE, validImages.length); - const batchMarkers = []; - - for (let i = startIndex; i < endIndex; i++) { - const image = validImages[i]; - const { lng, lat } = image; - const point = new window.BMap.Point(lng, lat); - const marker = customImageOverlay(point, image.src); - batchMarkers.push(marker); - } + }, [repoID, metadata.rows]); - clusterRef.current.addMarkers(batchMarkers); + const openCluster = useCallback((clusterIds) => { + clusterRef.current = clusterIds; + updateCurrentPath(`/${PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES}/${viewID}/${gettext('Location')}`); + setShowCluster(true); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [viewID, updateCurrentPath]); - if (endIndex < validImages.length) { - batchIndexRef.current += 1; - setTimeout(renderMarkersBatch, 20); // Schedule the next batch - } - }, [validImages]); + const closeCluster = useCallback(() => { + clusterRef.current = []; + updateCurrentPath(`/${PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES}/${viewID}`); + setShowCluster(false); + }, [viewID, updateCurrentPath]); - const initializeClusterer = useCallback(() => { - if (mapRef.current && !clusterRef.current) { - clusterRef.current = new window.BMapLib.MarkerClusterer(mapRef.current); - } - }, []); - - const initializeUserMarker = useCallback(() => { - if (!window.BMap) return; - - const imageUrl = `${mediaUrl}/img/marker.png`; - const addMarker = (lng, lat) => { - const gcPosition = wgs84_to_gcj02(lng, lat); - const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat); - const point = new window.BMap.Point(bdPosition.lng, bdPosition.lat); - const avatarMarker = customAvatarOverlay(point, appAvatarURL, imageUrl); - mapRef.current.addOverlay(avatarMarker); - }; - - if (!navigator.geolocation) { - addMarker(DEFAULT_POSITION.lng, DEFAULT_POSITION.lat); - return; - } - navigator.geolocation.getCurrentPosition( - position => addMarker(position.coords.longitude, position.coords.latitude), - () => { - addMarker(DEFAULT_POSITION.lng, DEFAULT_POSITION.lat); - toaster.danger(gettext('Failed to get user location')); - } - ); + useEffect(() => { + updateCurrentPath(`/${PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES}/${viewID}`); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const renderBaiduMap = useCallback(() => { - setIsLoading(false); - if (!window.BMap.Map) return; - let mapCenter = window.sfMetadataContext.localStorage.getItem('map-center') || DEFAULT_POSITION; - // ask for user location - if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition((userInfo) => { - mapCenter = { lng: userInfo.coords.longitude, lat: userInfo.coords.latitude }; - window.sfMetadataContext.localStorage.setItem('map-center', mapCenter); - }); - } - if (!isValidPosition(mapCenter?.lng, mapCenter?.lat)) return; - - const gcPosition = wgs84_to_gcj02(mapCenter.lng, mapCenter.lat); - const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat); - const { lng, lat } = bdPosition; - - mapRef.current = new window.BMap.Map('sf-metadata-map-container', { enableMapClick: false }); - const point = new window.BMap.Point(lng, lat); - mapRef.current.centerAndZoom(point, DEFAULT_ZOOM); - mapRef.current.enableScrollWheelZoom(true); - - addMapController(); - initializeUserMarker(); - initializeClusterer(); - - batchIndexRef.current = 0; // Reset batch index - renderMarkersBatch(); - }, [addMapController, initializeClusterer, initializeUserMarker, renderMarkersBatch]); - - useEffect(() => { - if (mapInfo.type === MAP_TYPE.B_MAP) { - window.renderMap = renderBaiduMap; - loadBMap(mapInfo.key).then(() => renderBaiduMap()); - return () => { - window.renderMap = null; - }; - } - return; - }, [mapInfo, renderBaiduMap]); + if (showCluster) { + return (); + } - return ( -
- {isLoading ? ( - - ) : ( -
- )} -
- ); + return (); }; export default Map; diff --git a/frontend/src/metadata/views/map/map-view/control/geolocation-control/index.css b/frontend/src/metadata/views/map/map-view/control/geolocation-control/index.css new file mode 100644 index 00000000000..49359bb5bd7 --- /dev/null +++ b/frontend/src/metadata/views/map/map-view/control/geolocation-control/index.css @@ -0,0 +1,4 @@ +.sf-map-control-container.sf-map-geolocation-control { + width: 40px; + line-height: 40px; +} diff --git a/frontend/src/metadata/views/map/map-view/control/geolocation-control/index.js b/frontend/src/metadata/views/map/map-view/control/geolocation-control/index.js new file mode 100644 index 00000000000..62e05f53c3f --- /dev/null +++ b/frontend/src/metadata/views/map/map-view/control/geolocation-control/index.js @@ -0,0 +1,44 @@ +import classnames from 'classnames'; +import { Utils } from '../../../../../../utils/utils'; + +import './index.css'; + +export function createBMapGeolocationControl(BMapGL, callback) { + function GeolocationControl() { + this.defaultAnchor = window.BMAP_ANCHOR_BOTTOM_RIGHT; + this.defaultOffset = new BMapGL.Size(30, Utils.isDesktop() ? 30 : 90); + } + GeolocationControl.prototype = new BMapGL.Control(); + GeolocationControl.prototype.initialize = function (map) { + const div = document.createElement('div'); + let className = classnames('sf-map-control-container sf-map-geolocation-control d-flex align-items-center justify-content-center', { + 'sf-map-geolocation-control-mobile': !Utils.isDesktop() + }); + + const locationButton = document.createElement('div'); + locationButton.className = 'sf-map-control d-flex align-items-center justify-content-center'; + locationButton.innerHTML = ''; + div.appendChild(locationButton); + + div.className = className; + div.onclick = (e) => { + e.preventDefault(); + const geolocation = new BMapGL.Geolocation(); + div.className = classnames(className, 'sf-map-control-loading'); + geolocation.getCurrentPosition((result) => { + div.className = className; + if (result) { + const point = result.point; + callback(point); + } else { + // Positioning failed + callback(); + } + }); + }; + map.getContainer().appendChild(div); + return div; + }; + + return GeolocationControl; +} diff --git a/frontend/src/metadata/views/map/map-view/control/index.css b/frontend/src/metadata/views/map/map-view/control/index.css new file mode 100644 index 00000000000..c9157de9611 --- /dev/null +++ b/frontend/src/metadata/views/map/map-view/control/index.css @@ -0,0 +1,63 @@ +.sf-map-control-container { + height: 40px; + width: fit-content; + background-color: rgba(255, 255, 255, .9); + opacity: 1; + overflow: hidden; + border-radius: 6px; + box-shadow: -2px -2px 4px 2px rgba(0, 0, 0, 0.1); + line-height: 40px; +} + +.sf-map-control-container.sf-map-control-loading { + opacity: 0.7; +} + +.sf-map-control-container.sf-map-control-container-mobile { + height: 35px; + line-height: 35px; + opacity: .75; +} + +.sf-map-control-container .sf-map-control-divider { + height: 100%; + width: 1px; + position: relative; + background-color: inherit; +} + +.sf-map-control-container .sf-map-control-divider::before { + content: ''; + position: absolute; + top: 9px; + height: 22px; + width: 1px; + background-color: #ccc; +} + +.sf-map-control-container .sf-map-control { + height: 40px; + width: 40px; + color: #666; + background-color: inherit; + text-align: center; +} + +.sf-map-control-container.sf-map-control-container-mobile .sf-map-control { + height: 35px; + width: 35px; + opacity: .75; +} + +.sf-map-control-container .sf-map-control .sf-map-control-icon { + font-size: 18px; +} + +.sf-map-control-container .sf-map-control:not(.disabled):hover { + cursor: pointer; + color: #212529; +} + +.sf-map-control-container .sf-map-control.disabled { + color: #ccc; +} diff --git a/frontend/src/metadata/views/map/map-view/control/index.js b/frontend/src/metadata/views/map/map-view/control/index.js new file mode 100644 index 00000000000..8cd156d393c --- /dev/null +++ b/frontend/src/metadata/views/map/map-view/control/index.js @@ -0,0 +1,9 @@ +import { createBMapGeolocationControl } from './geolocation-control'; +import { createBMapZoomControl } from './zoom-control'; + +import './index.css'; + +export { + createBMapGeolocationControl, + createBMapZoomControl +}; diff --git a/frontend/src/metadata/views/map/map-view/control/zoom-control/index.css b/frontend/src/metadata/views/map/map-view/control/zoom-control/index.css new file mode 100644 index 00000000000..98deb1eabd0 --- /dev/null +++ b/frontend/src/metadata/views/map/map-view/control/zoom-control/index.css @@ -0,0 +1,3 @@ +.sf-map-control-container.sf-map-zoom-control-container .sf-map-control { + width: 55px; +} diff --git a/frontend/src/metadata/views/map/map-view/control/zoom-control/index.js b/frontend/src/metadata/views/map/map-view/control/zoom-control/index.js new file mode 100644 index 00000000000..79057f567e0 --- /dev/null +++ b/frontend/src/metadata/views/map/map-view/control/zoom-control/index.js @@ -0,0 +1,59 @@ +import classnames from 'classnames'; +import { Utils } from '../../../../../../utils/utils'; + +import './index.css'; + +export function createBMapZoomControl(BMapGL, { maxZoom, minZoom }, callback) { + function ZoomControl() { + this.defaultAnchor = window.BMAP_ANCHOR_BOTTOM_RIGHT; + this.defaultOffset = new BMapGL.Size(80, Utils.isDesktop() ? 30 : 90); + } + 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 d-flex align-items-center justify-content-center'; + const zoomInButton = document.createElement('div'); + zoomInButton.className = classnames(buttonClassName, { 'disabled': zoomLevel >= maxZoom }); + zoomInButton.innerHTML = ''; + div.appendChild(zoomInButton); + + const divider = document.createElement('div'); + divider.className = 'sf-map-control-divider'; + div.appendChild(divider); + + const zoomOutButton = document.createElement('div'); + zoomOutButton.className = classnames(buttonClassName, { 'disabled': zoomLevel <= minZoom }); + zoomOutButton.innerHTML = ''; + 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); + }; + + zoomInButton.onclick = (e) => { + e.preventDefault(); + const nextZoom = map.getZoom() + 1; + map.zoomTo(Math.min(nextZoom, maxZoom)); + }; + + zoomOutButton.onclick = (e) => { + e.preventDefault(); + const nextZoom = map.getZoom() - 1; + map.zoomTo(Math.max(nextZoom, minZoom)); + }; + + map.addEventListener('zoomend', updateButtonStates); + map.getContainer().appendChild(div); + return div; + }; + + return ZoomControl; +} diff --git a/frontend/src/metadata/views/map/map-view/index.css b/frontend/src/metadata/views/map/map-view/index.css new file mode 100644 index 00000000000..58f97737709 --- /dev/null +++ b/frontend/src/metadata/views/map/map-view/index.css @@ -0,0 +1,82 @@ +.sf-metadata-view-map #platform div:has(.custom-avatar-overlay) { + display: block !important; +} + +.sf-metadata-view-map #platform div:has(.custom-image-overlay) { + display: block !important; +} + +.sf-metadata-view-map .sf-metadata-map-container { + width: 100%; + height: 100%; +} + +.sf-metadata-view-map .custom-image-container { + width: 86px; + height: 86px; + display: flex; + align-items: center; + justify-content: center; + background-color: #fff; + padding: 3px; + border-radius: 6px; + position: relative; + cursor: default; +} + +.sf-metadata-view-map .custom-image-container img { + border-radius: 6px; +} + +.sf-metadata-view-map .custom-image-number { + position: absolute; + right: -16px; + top: -16px; + width: 32px; + height: 32px; + line-height: 32px; + background: #007bff; + color: #fff; + border-radius: 50%; + text-align: center; + font-size: 16px; + font-weight: 400; +} + +.sf-metadata-view-map .custom-image-container:active::before, +.sf-metadata-view-map .custom-image-container:active .custom-image-number::before, +.sf-metadata-view-map .custom-image-number:active::before, +.sf-metadata-view-map .custom-image-number:active .custom-image-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.4); + border-radius: 6px; +} + +.sf-metadata-view-map .custom-image-container:active .custom-image-number::before, +.sf-metadata-view-map .custom-image-number:active::before { + border-radius: 50%; +} + +.sf-metadata-view-map .custom-image-container::after { + content: ''; + position: absolute; + bottom: -10px; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 10px solid transparent; + border-right: 10px solid transparent; + border-top: 10px solid #fff; + border-radius: 2px; +} + +.sf-metadata-view-map .custom-image-container:active::after, +.sf-metadata-view-map .custom-image-number:active .custom-image-container::after { + border-top: 10px solid rgba(0, 0, 0, 0.4); +} diff --git a/frontend/src/metadata/views/map/map-view/index.js b/frontend/src/metadata/views/map/map-view/index.js new file mode 100644 index 00000000000..9b07c82a032 --- /dev/null +++ b/frontend/src/metadata/views/map/map-view/index.js @@ -0,0 +1,184 @@ +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import PropTypes from 'prop-types'; +import loadBMap, { initMapInfo } from '../../../../utils/map-utils'; +import { appAvatarURL, baiduMapKey, googleMapKey, mediaUrl } from '../../../../utils/constants'; +import { isValidPosition } from '../../../utils/validate'; +import { wgs84_to_gcj02, gcj02_to_bd09 } from '../../../../utils/coord-transform'; +import { MAP_TYPE as MAP_PROVIDER } from '../../../../constants'; +import { EVENT_BUS_TYPE, MAP_TYPE, STORAGE_MAP_CENTER_KEY, STORAGE_MAP_TYPE_KEY, STORAGE_MAP_ZOOM_KEY } from '../../../constants'; +import { createBMapGeolocationControl, createBMapZoomControl } from './control'; +import { customAvatarOverlay, customImageOverlay } from './overlay'; + +import './index.css'; + +const DEFAULT_POSITION = { lng: 104.195, lat: 35.861 }; +const DEFAULT_ZOOM = 4; +const BATCH_SIZE = 500; +const MAX_ZOOM = 21; +const MIN_ZOOM = 3; + +const MapView = ({ images, onOpenCluster }) => { + const mapInfo = useMemo(() => initMapInfo({ baiduMapKey, googleMapKey }), []); + + const mapRef = useRef(null); + const clusterRef = useRef(null); + const batchIndexRef = useRef(0); + + 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 ZoomControl = createBMapZoomControl(window.BMapGL, { maxZoom: MAX_ZOOM, minZoom: MIN_ZOOM }, 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 onClickMarker = useCallback((e, markers) => { + saveMapState(); + const imageIds = markers.map(marker => marker._id); + onOpenCluster(imageIds); + }, [onOpenCluster, saveMapState]); + + const renderMarkersBatch = useCallback(() => { + if (!images.length || !clusterRef.current) return; + + const startIndex = batchIndexRef.current * BATCH_SIZE; + const endIndex = Math.min(startIndex + BATCH_SIZE, images.length); + const batchMarkers = []; + + for (let i = startIndex; i < endIndex; i++) { + const image = images[i]; + const { lng, lat } = image; + const point = new window.BMapGL.Point(lng, lat); + const marker = customImageOverlay(point, image, { + callback: (e, markers) => onClickMarker(e, markers) + }); + batchMarkers.push(marker); + } + clusterRef.current.addMarkers(batchMarkers); + + if (endIndex < images.length) { + batchIndexRef.current += 1; + setTimeout(renderMarkersBatch, 20); // Schedule the next batch + } + }, [images, onClickMarker]); + + const initializeCluster = useCallback(() => { + if (mapRef.current && !clusterRef.current) { + clusterRef.current = new window.BMapLib.MarkerCluster(mapRef.current, { + callback: (e, markers) => onClickMarker(e, markers), + maxZoom: 21, + }); + } + }, [onClickMarker]); + + const renderBaiduMap = useCallback(() => { + if (!mapRef.current || !window.BMapGL.Map) return; + let { center, zoom } = loadMapState(); + let userPosition = { lng: 116.40396418840683, lat: 39.915106021711345 }; + // 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); + mapRef.current = new window.BMapGL.Map('sf-metadata-map-container', { + enableMapClick: false, + minZoom: MIN_ZOOM, + maxZoom: MAX_ZOOM, + mapType: getBMapType(mapTypeValue), + }); + + if (isValidPosition(center?.lng, center?.lat)) { + mapRef.current.centerAndZoom(center, zoom); + } + + mapRef.current.enableScrollWheelZoom(true); + addMapController(); + + initializeUserMarker(userPosition); + initializeCluster(); + + batchIndexRef.current = 0; + renderMarkersBatch(); + }, [addMapController, initializeCluster, initializeUserMarker, renderMarkersBatch, getBMapType, loadMapState]); + + useEffect(() => { + const modifyMapTypeSubscribe = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.MODIFY_MAP_TYPE, (newType) => { + window.sfMetadataContext.localStorage.setItem(STORAGE_MAP_TYPE_KEY, newType); + const mapType = getBMapType(newType); + mapRef.current && mapRef.current.setMapType(mapType); + }); + + return () => { + modifyMapTypeSubscribe(); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (mapInfo.type === MAP_PROVIDER.B_MAP) { + loadBMap(mapInfo.key).then(() => renderBaiduMap()); + return () => { + window.renderMap = null; + }; + } + return; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+
+
+ ); +}; + +MapView.propTypes = { + images: PropTypes.array, + onOpenCluster: PropTypes.func, +}; + +export default MapView; diff --git a/frontend/src/metadata/views/map/custom-avatar-overlay.js b/frontend/src/metadata/views/map/map-view/overlay/custom-avatar-overlay.js similarity index 93% rename from frontend/src/metadata/views/map/custom-avatar-overlay.js rename to frontend/src/metadata/views/map/map-view/overlay/custom-avatar-overlay.js index 72d97808da9..27b0545e70a 100644 --- a/frontend/src/metadata/views/map/custom-avatar-overlay.js +++ b/frontend/src/metadata/views/map/map-view/overlay/custom-avatar-overlay.js @@ -1,5 +1,5 @@ const customAvatarOverlay = (point, avatarUrl, bgUrl, width = 20, height = 25) => { - class AvatarOverlay extends window.BMap.Overlay { + class AvatarOverlay extends window.BMapGL.Overlay { constructor(point, avatarUrl, bgUrl, width, height) { super(); this._point = point; @@ -13,6 +13,7 @@ const customAvatarOverlay = (point, avatarUrl, bgUrl, width = 20, height = 25) = this._map = map; const divBox = document.createElement('div'); const divImg = new Image(); + divBox.className = 'custom-avatar-overlay'; divBox.style.position = 'absolute'; divBox.style.width = `${this._width}px`; divBox.style.height = `${this._height}px`; diff --git a/frontend/src/metadata/views/map/custom-image-overlay.js b/frontend/src/metadata/views/map/map-view/overlay/custom-image-overlay.js similarity index 50% rename from frontend/src/metadata/views/map/custom-image-overlay.js rename to frontend/src/metadata/views/map/map-view/overlay/custom-image-overlay.js index ddb61059b95..871e0046179 100644 --- a/frontend/src/metadata/views/map/custom-image-overlay.js +++ b/frontend/src/metadata/views/map/map-view/overlay/custom-image-overlay.js @@ -1,29 +1,28 @@ -import { Utils } from '../../../utils/utils'; +import { Utils } from '../../../../../utils/utils'; -const customImageOverlay = (center, imageUrl) => { - class ImageOverlay extends window.BMap.Overlay { - constructor(center, imageUrl) { - super(); +const customImageOverlay = (center, image, callback) => { + class ImageOverlay extends window.BMapLib.TextIconOverlay { + constructor(center, image, { callback } = {}) { + super(center, '', { styles: [] }); this._center = center; - this._imageUrl = imageUrl; + this._URL = image.src; + this._id = image.id; + this._callback = callback; } initialize(map) { this._map = map; const div = document.createElement('div'); div.style.position = 'absolute'; - div.style.width = '80px'; - div.style.height = '80px'; div.style.zIndex = 2000; map.getPanes().markerPane.appendChild(div); this._div = div; - const imageElement = ``; + const imageElement = ``; const htmlString = `
- ${this._imageUrl ? imageElement : '
'} - + ${this._URL ? imageElement : '
'}
`; const labelDocument = new DOMParser().parseFromString(htmlString, 'text/html'); @@ -31,12 +30,30 @@ const customImageOverlay = (center, imageUrl) => { this._div.append(label); const eventHandler = (event) => { - event.stopPropagation(); event.preventDefault(); + this._callback && this._callback(event, [{ _id: this._id }]); }; if (Utils.isDesktop()) { - this._div.addEventListener('click', eventHandler); + let clickTimeout; + this._div.addEventListener('click', (event) => { + if (clickTimeout) { + clearTimeout(clickTimeout); + clickTimeout = null; + return; + } + clickTimeout = setTimeout(() => { + eventHandler(event); + clickTimeout = null; + }, 300); + }); + this._div.addEventListener('dblclick', (e) => { + e.preventDefault(); + if (clickTimeout) { + clearTimeout(clickTimeout); + clickTimeout = null; + } + }); } else { this._div.addEventListener('touchend', eventHandler); } @@ -51,7 +68,7 @@ const customImageOverlay = (center, imageUrl) => { } getImageUrl() { - return imageUrl || ''; + return image.src || ''; } getPosition() { @@ -63,7 +80,7 @@ const customImageOverlay = (center, imageUrl) => { } } - return new ImageOverlay(center, imageUrl); + return new ImageOverlay(center, image, callback); }; export default customImageOverlay; diff --git a/frontend/src/metadata/views/map/map-view/overlay/index.js b/frontend/src/metadata/views/map/map-view/overlay/index.js new file mode 100644 index 00000000000..21f2d71fa90 --- /dev/null +++ b/frontend/src/metadata/views/map/map-view/overlay/index.js @@ -0,0 +1,7 @@ +import customAvatarOverlay from './custom-avatar-overlay'; +import customImageOverlay from './custom-image-overlay'; + +export { + customAvatarOverlay, + customImageOverlay +}; diff --git a/frontend/src/metadata/views/table/utils/grid-utils.js b/frontend/src/metadata/views/table/utils/grid-utils.js index a835e80223b..3e0d4d9857f 100644 --- a/frontend/src/metadata/views/table/utils/grid-utils.js +++ b/frontend/src/metadata/views/table/utils/grid-utils.js @@ -2,8 +2,7 @@ import dayjs from 'dayjs'; import { getCellValueByColumn, getFileNameFromRecord, getRecordIdFromRecord, isCellValueChanged } from '../../../utils/cell'; import { getColumnByIndex, getColumnOriginName } from '../../../utils/column'; import { CellType, NOT_SUPPORT_DRAG_COPY_COLUMN_TYPES, PRIVATE_COLUMN_KEY, TRANSFER_TYPES, - REG_NUMBER_DIGIT, REG_STRING_NUMBER_PARTS, COLUMN_RATE_MAX_NUMBER, - PASTE_SOURCE, + REG_NUMBER_DIGIT, REG_STRING_NUMBER_PARTS, RATE_MAX_NUMBER, PASTE_SOURCE, } from '../../../constants'; import { getGroupRecordByIndex } from './group-metrics'; import { convertCellValue } from './convert-utils'; @@ -594,7 +593,7 @@ class GridUtils { } _getRatingLeastSquares(numberList, data) { - const { rate_max_number = COLUMN_RATE_MAX_NUMBER[4].name } = data || {}; + const { rate_max_number = RATE_MAX_NUMBER[4].name } = data || {}; let slope; let intercept; let xAverage; diff --git a/frontend/src/pages/lib-content-view/lib-content-view.js b/frontend/src/pages/lib-content-view/lib-content-view.js index f79bf5744cf..bfb212641da 100644 --- a/frontend/src/pages/lib-content-view/lib-content-view.js +++ b/frontend/src/pages/lib-content-view/lib-content-view.js @@ -2150,6 +2150,7 @@ class LibContentView extends React.Component { }; updatePath = (path) => { + if (this.state.path === path) return; this.setState({ path }); }; diff --git a/frontend/src/utils/map-utils.js b/frontend/src/utils/map-utils.js index 1b68b04acd6..dfa09822257 100644 --- a/frontend/src/utils/map-utils.js +++ b/frontend/src/utils/map-utils.js @@ -1,6 +1,8 @@ import { MAP_TYPE } from '../constants'; import { mediaUrl } from './constants'; +const STATIC_RESOURCE_VERSION = 0.1; + export const initMapInfo = ({ baiduMapKey, googleMapKey, mineMapKey }) => { if (baiduMapKey) return { type: MAP_TYPE.B_MAP, key: baiduMapKey }; if (googleMapKey) return { type: MAP_TYPE.G_MAP, key: googleMapKey }; @@ -30,8 +32,8 @@ export const loadMapSource = (type, key, callback) => { export default function loadBMap(ak) { return new Promise((resolve, reject) => { asyncLoadBaiduJs(ak) - .then(() => asyncLoadJs(`${mediaUrl}/js/map/text-icon-overlay.js`)) - .then(() => asyncLoadJs(`${mediaUrl}/js/map/marker-clusterer.js`)) + .then(() => asyncLoadJs(`${mediaUrl}/js/map/text-icon-overlay.js?v=${STATIC_RESOURCE_VERSION}`)) + .then(() => asyncLoadJs(`${mediaUrl}/js/map/marker-cluster.js?v=${STATIC_RESOURCE_VERSION}`)) .then(() => resolve(true)) .catch((err) => reject(err)); }); @@ -39,16 +41,16 @@ export default function loadBMap(ak) { export function asyncLoadBaiduJs(ak) { return new Promise((resolve, reject) => { - if (typeof window.BMap !== 'undefined') { - resolve(window.BMap); + if (typeof window.BMapGL !== 'undefined') { + resolve(window.BMapGL); return; } window.renderMap = function () { - resolve(window.BMap); + resolve(window.BMapGL); }; let script = document.createElement('script'); script.type = 'text/javascript'; - script.src = `https://api.map.baidu.com/api?v=3.0&ak=${ak}&callback=renderMap`; + script.src = `https://api.map.baidu.com/api?type=webgl&v=1.0&ak=${ak}&callback=renderMap`; script.onerror = reject; document.body.appendChild(script); }); diff --git a/media/css/sf_font3/iconfont.css b/media/css/sf_font3/iconfont.css index def6c7fe635..b6650f591a4 100644 --- a/media/css/sf_font3/iconfont.css +++ b/media/css/sf_font3/iconfont.css @@ -1,11 +1,11 @@ @font-face { font-family: "sf3-font"; /* Project id 1230969 */ - src: url('./iconfont.eot?t=1733301127109'); /* IE9 */ - src: url('./iconfont.eot?t=1733301127109#iefix') format('embedded-opentype'), /* IE6-IE8 */ - url('./iconfont.woff2?t=1733301127109') format('woff2'), - url('./iconfont.woff?t=1733301127109') format('woff'), - url('./iconfont.ttf?t=1733301127109') format('truetype'), - url('./iconfont.svg?t=1733301127109#sf3-font') format('svg'); + src: url('./iconfont.eot?t=1736476800596'); /* IE9 */ + src: url('./iconfont.eot?t=1736476800596#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('./iconfont.woff2?t=1736476800596') format('woff2'), + url('./iconfont.woff?t=1736476800596') format('woff'), + url('./iconfont.ttf?t=1736476800596') format('truetype'), + url('./iconfont.svg?t=1736476800596#sf3-font') format('svg'); } .sf3-font { @@ -16,6 +16,30 @@ -moz-osx-font-smoothing: grayscale; } +.sf3-font-zoom-out:before { + content: "\e630"; +} + +.sf3-font-current-location:before { + content: "\e62e"; +} + +.sf3-font-zoom-in:before { + content: "\e62f"; +} + +.sf3-font-ai:before { + content: "\e854"; +} + +.sf3-font-time:before { + content: "\e852"; +} + +.sf3-font-description:before { + content: "\e853"; +} + .sf3-font-hi:before { content: "\e603"; } @@ -24,10 +48,6 @@ content: "\e851"; } -.sf3-font-current-location:before { - content: "\e850"; -} - .sf3-font-ai_generated:before { content: "\e84f"; } diff --git a/media/css/sf_font3/iconfont.eot b/media/css/sf_font3/iconfont.eot index 4bb9fed7ba7..bfefade4d69 100644 Binary files a/media/css/sf_font3/iconfont.eot and b/media/css/sf_font3/iconfont.eot differ diff --git a/media/css/sf_font3/iconfont.svg b/media/css/sf_font3/iconfont.svg index d40bdac5381..e3a48dd6bf4 100644 --- a/media/css/sf_font3/iconfont.svg +++ b/media/css/sf_font3/iconfont.svg @@ -14,12 +14,22 @@ /> + + + + + + + + + + + + - - @@ -188,7 +198,7 @@ - + @@ -224,7 +234,7 @@ - + diff --git a/media/css/sf_font3/iconfont.ttf b/media/css/sf_font3/iconfont.ttf index 0a1dae1a5d6..6e2143aaa54 100644 Binary files a/media/css/sf_font3/iconfont.ttf and b/media/css/sf_font3/iconfont.ttf differ diff --git a/media/css/sf_font3/iconfont.woff b/media/css/sf_font3/iconfont.woff index 77a4a0b7a7f..0d71528c2b1 100644 Binary files a/media/css/sf_font3/iconfont.woff and b/media/css/sf_font3/iconfont.woff differ diff --git a/media/css/sf_font3/iconfont.woff2 b/media/css/sf_font3/iconfont.woff2 index 48dbc69dbe0..f576b8fab31 100644 Binary files a/media/css/sf_font3/iconfont.woff2 and b/media/css/sf_font3/iconfont.woff2 differ diff --git a/media/js/map/marker-clusterer.js b/media/js/map/marker-cluster.js similarity index 75% rename from media/js/map/marker-clusterer.js rename to media/js/map/marker-cluster.js index 66cc930defe..42fc3695092 100644 --- a/media/js/map/marker-clusterer.js +++ b/media/js/map/marker-cluster.js @@ -1,6 +1,6 @@ /** - * @fileoverview MarkerClusterer标记聚合器用来解决加载大量点要素到地图上产生覆盖现象的问题,并提高性能。 - * 主入口类是MarkerClusterer, + * @fileoverview MarkerCluster标记聚合器用来解决加载大量点要素到地图上产生覆盖现象的问题,并提高性能。 + * 主入口类是MarkerCluster, * 基于Baidu Map API 1.2。 * * @author Baidu Map Api Group @@ -15,11 +15,11 @@ var BMapLib = window.BMapLib = BMapLib || {}; /** * 获取一个扩展的视图范围,把上下左右都扩大一样的像素值。 - * @param {Map} map BMap.Map的实例化对象 - * @param {BMap.Bounds} bounds BMap.Bounds的实例化对象 + * @param {Map} map BMapGL.Map的实例化对象 + * @param {BMapGL.Bounds} bounds BMapGL.Bounds的实例化对象 * @param {Number} gridSize 要扩大的像素值 * - * @return {BMap.Bounds} 返回扩大后的视图范围。 + * @return {BMapGL.Bounds} 返回扩大后的视图范围。 */ var getExtendedBounds = function(map, bounds, gridSize){ bounds = cutBoundsInRange(bounds); @@ -31,21 +31,22 @@ var BMapLib = window.BMapLib = BMapLib || {}; pixelSW.y += gridSize; var newNE = map.pixelToPoint(pixelNE); var newSW = map.pixelToPoint(pixelSW); - return new BMap.Bounds(newSW, newNE); + if (!newSW || !newNE) return null; + return new BMapGL.Bounds(newSW, newNE); }; /** * 按照百度地图支持的世界范围对bounds进行边界处理 - * @param {BMap.Bounds} bounds BMap.Bounds的实例化对象 + * @param {BMapGL.Bounds} bounds BMapGL.Bounds的实例化对象 * - * @return {BMap.Bounds} 返回不越界的视图范围 + * @return {BMapGL.Bounds} 返回不越界的视图范围 */ var cutBoundsInRange = function (bounds) { var maxX = getRange(bounds.getNorthEast().lng, -180, 180); var minX = getRange(bounds.getSouthWest().lng, -180, 180); - var maxY = getRange(bounds.getNorthEast().lat, -74, 74); - var minY = getRange(bounds.getSouthWest().lat, -74, 74); - return new BMap.Bounds(new BMap.Point(minX, minY), new BMap.Point(maxX, maxY)); + var maxY = getRange(bounds.getNorthEast().lat, -90, 90); + var minY = getRange(bounds.getSouthWest().lat, -90, 90); + return new BMapGL.Bounds(new BMapGL.Point(minX, minY), new BMapGL.Point(maxX, maxY)); }; /** @@ -97,11 +98,11 @@ var BMapLib = window.BMapLib = BMapLib || {}; }; /** - *@exports MarkerClusterer as BMapLib.MarkerClusterer + *@exports MarkerCluster as BMapLib.MarkerCluster */ - var MarkerClusterer = + var MarkerCluster = /** - * MarkerClusterer + * MarkerCluster * @class 用来解决加载大量点要素到地图上产生覆盖现象的问题,并提高性能 * @constructor * @param {Map} map 地图的一个实例。 @@ -110,10 +111,10 @@ var BMapLib = window.BMapLib = BMapLib || {}; * girdSize {Number} 聚合计算时网格的像素大小,默认60
* maxZoom {Number} 最大的聚合级别,大于该级别就不进行相应的聚合
* minClusterSize {Number} 最小的聚合数量,小于该数量的不能成为一个聚合,默认为2
- * isAverangeCenter {Boolean} 聚合点的落脚位置是否是所有聚合在内点的平均值,默认为否,落脚在聚合内的第一个点
+ * isAvgCenter {Boolean} 聚合点的落脚位置是否是所有聚合在内点的平均值,默认为否,落脚在聚合内的第一个点
* styles {Array} 自定义聚合后的图标风格,请参考TextIconOverlay类
*/ - BMapLib.MarkerClusterer = function(map, options){ + BMapLib.MarkerCluster = function(map, options){ if (!map){ return; } @@ -123,13 +124,14 @@ var BMapLib = window.BMapLib = BMapLib || {}; var opts = options || {}; this._gridSize = opts["gridSize"] || 60; - this._maxZoom = opts["maxZoom"] || 18; + this._maxZoom = opts["maxZoom"] || 21; this._minClusterSize = opts["minClusterSize"] || 2; this._isAverageCenter = false; if (opts['isAverageCenter'] != undefined) { this._isAverageCenter = opts['isAverageCenter']; } this._styles = opts["styles"] || []; + this._callback = opts["callback"] || function(){}; var that = this; this._map.addEventListener("zoomend",function(){ @@ -140,8 +142,8 @@ var BMapLib = window.BMapLib = BMapLib || {}; // that._redraw(); // }); - var mkrs = opts["markers"]; - isArray(mkrs) && this.addMarkers(mkrs); + var markers = opts["markers"]; + isArray(markers) && this.addMarkers(markers); }; /** @@ -150,7 +152,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * * @return 无返回值。 */ - MarkerClusterer.prototype.addMarkers = function(markers){ + MarkerCluster.prototype.addMarkers = function(markers){ for(var i = 0, len = markers.length; i } markers 需要被删除的marker数组 + * @param {Array} markers 需要被删除的marker数组 * * @return {Boolean} 删除成功返回true,否则返回false */ - MarkerClusterer.prototype.removeMarkers = function(markers) { + MarkerCluster.prototype.removeMarkers = function(markers) { var success = false; for (var i = 0; i < markers.length; i++) { var r = this._removeMarker(markers[i]); @@ -320,7 +322,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * 从地图上彻底清除所有的标记 * @return 无返回值 */ - MarkerClusterer.prototype.clearMarkers = function() { + MarkerCluster.prototype.clearMarkers = function() { this._clearLastClusters(); this._removeMarkersFromMap(); this._markers = []; @@ -330,7 +332,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * 重新生成,比如改变了属性等 * @return 无返回值 */ - MarkerClusterer.prototype._redraw = function () { + MarkerCluster.prototype._redraw = function () { this._clearLastClusters(); this._createClusters(); }; @@ -339,7 +341,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * 获取网格大小 * @return {Number} 网格大小 */ - MarkerClusterer.prototype.getGridSize = function() { + MarkerCluster.prototype.getGridSize = function() { return this._gridSize; }; @@ -348,7 +350,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * @param {Number} size 网格大小 * @return 无返回值 */ - MarkerClusterer.prototype.setGridSize = function(size) { + MarkerCluster.prototype.setGridSize = function(size) { this._gridSize = size; this._redraw(); }; @@ -357,7 +359,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * 获取聚合的最大缩放级别。 * @return {Number} 聚合的最大缩放级别。 */ - MarkerClusterer.prototype.getMaxZoom = function() { + MarkerCluster.prototype.getMaxZoom = function() { return this._maxZoom; }; @@ -366,7 +368,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * @param {Number} maxZoom 聚合的最大缩放级别 * @return 无返回值 */ - MarkerClusterer.prototype.setMaxZoom = function(maxZoom) { + MarkerCluster.prototype.setMaxZoom = function(maxZoom) { this._maxZoom = maxZoom; this._redraw(); }; @@ -375,7 +377,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * 获取聚合的样式风格集合 * @return {Array} 聚合的样式风格集合 */ - MarkerClusterer.prototype.getStyles = function() { + MarkerCluster.prototype.getStyles = function() { return this._styles; }; @@ -384,7 +386,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * @param {Array} styles 样式风格数组 * @return 无返回值 */ - MarkerClusterer.prototype.setStyles = function(styles) { + MarkerCluster.prototype.setStyles = function(styles) { this._styles = styles; this._redraw(); }; @@ -393,7 +395,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * 获取单个聚合的最小数量。 * @return {Number} 单个聚合的最小数量。 */ - MarkerClusterer.prototype.getMinClusterSize = function() { + MarkerCluster.prototype.getMinClusterSize = function() { return this._minClusterSize; }; @@ -402,7 +404,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * @param {Number} size 单个聚合的最小数量。 * @return 无返回值。 */ - MarkerClusterer.prototype.setMinClusterSize = function(size) { + MarkerCluster.prototype.setMinClusterSize = function(size) { this._minClusterSize = size; this._redraw(); }; @@ -411,7 +413,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * 获取单个聚合的落脚点是否是聚合内所有标记的平均中心。 * @return {Boolean} true或false。 */ - MarkerClusterer.prototype.isAverageCenter = function() { + MarkerCluster.prototype.isAverageCenter = function() { return this._isAverageCenter; }; @@ -419,7 +421,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * 获取聚合的Map实例。 * @return {Map} Map的示例。 */ - MarkerClusterer.prototype.getMap = function() { + MarkerCluster.prototype.getMap = function() { return this._map; }; @@ -427,7 +429,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * 获取所有的标记数组。 * @return {Array} 标记数组。 */ - MarkerClusterer.prototype.getMarkers = function() { + MarkerCluster.prototype.getMarkers = function() { return this._markers; }; @@ -435,7 +437,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * 获取聚合的总数量。 * @return {Number} 聚合的总数量。 */ - MarkerClusterer.prototype.getClustersCount = function() { + MarkerCluster.prototype.getClustersCount = function() { var count = 0; for(var i = 0, cluster; cluster = this._clusters[i]; i++){ cluster.isReal() && count++; @@ -443,24 +445,28 @@ var BMapLib = window.BMapLib = BMapLib || {}; return count; }; + MarkerCluster.prototype.getCallback = function() { + return this._callback; + } + /** * @ignore * Cluster * @class 表示一个聚合对象,该聚合,包含有N个标记,这N个标记组成的范围,并有予以显示在Map上的TextIconOverlay等。 * @constructor - * @param {MarkerClusterer} markerClusterer 一个标记聚合器示例。 + * @param {MarkerCluster} markerCluster 一个标记聚合器示例。 */ - function Cluster(markerClusterer){ - this._markerClusterer = markerClusterer; - this._map = markerClusterer.getMap(); - this._minClusterSize = markerClusterer.getMinClusterSize(); - this._isAverageCenter = markerClusterer.isAverageCenter(); + function Cluster(markerCluster){ + this._markerCluster = markerCluster; + this._map = markerCluster.getMap(); + this._minClusterSize = markerCluster.getMinClusterSize(); + this._isAverageCenter = markerCluster.isAverageCenter(); this._center = null;//落脚位置 this._markers = [];//这个Cluster中所包含的markers this._gridBounds = null;//以中心点为准,向四边扩大gridSize个像素的范围,也即网格范围 this._isReal = false; //真的是个聚合 - this._clusterMarker = new BMapLib.TextIconOverlay(this._center, this._markers.length, {"styles":this._markerClusterer.getStyles()}); + this._clusterMarker = new BMapLib.TextIconOverlay(this._center, this._markers.length, {"styles":this._markerCluster.getStyles()}); //this._map.addOverlay(this._clusterMarker); } @@ -482,7 +488,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; var l = this._markers.length + 1; var lat = (this._center.lat * (l - 1) + marker.getPosition().lat) / l; var lng = (this._center.lng * (l - 1) + marker.getPosition().lng) / l; - this._center = new BMap.Point(lng, lat); + this._center = new BMapGL.Point(lng, lat); this.updateGridBounds(); }//计算新的Center } @@ -545,8 +551,8 @@ var BMapLib = window.BMapLib = BMapLib || {}; * @return 无返回值。 */ Cluster.prototype.updateGridBounds = function() { - var bounds = new BMap.Bounds(this._center, this._center); - this._gridBounds = getExtendedBounds(this._map, bounds, this._markerClusterer.getGridSize()); + var bounds = new BMapGL.Bounds(this._center, this._center); + this._gridBounds = getExtendedBounds(this._map, bounds, this._markerCluster.getGridSize()); }; /** @@ -554,7 +560,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * @return 无返回值。 */ Cluster.prototype.updateClusterMarker = function () { - if (this._map.getZoom() > this._markerClusterer.getMaxZoom()) { + if (this._map.getZoom() > this._markerCluster.getMaxZoom()) { this._clusterMarker && this._map.removeOverlay(this._clusterMarker); for (var i = 0, marker; marker = this._markers[i]; i++) { this._map.addOverlay(marker); @@ -575,10 +581,29 @@ var BMapLib = window.BMapLib = BMapLib || {}; var thatMap = this._map; var thatBounds = this.getBounds(); - this._clusterMarker.addEventListener("click", function(event){ - thatMap.setViewport(thatBounds); + let clickTimeout; + this._clusterMarker.addEventListener("click", (event) => { + if (clickTimeout) { + clearTimeout(clickTimeout); + clickTimeout = null; + return; + } + clickTimeout = setTimeout(() => { + if (this._markerCluster && typeof this._markerCluster.getCallback() === 'function') { + const markers = this._markers; + this._markerCluster.getCallback()(event, markers); + } + clickTimeout = null; + }, 300); // Delay to differentiate between single and double click }); + this._clusterMarker.addEventListener("dblclick", (event) => { + if (clickTimeout) { + clearTimeout(clickTimeout); + clickTimeout = null; + } + // Do nothing on double click + }); }; /** @@ -596,10 +621,10 @@ var BMapLib = window.BMapLib = BMapLib || {}; /** * 获取该聚合所包含的所有标记的最小外接矩形的范围。 - * @return {BMap.Bounds} 计算出的范围。 + * @return {BMapGL.Bounds} 计算出的范围。 */ Cluster.prototype.getBounds = function() { - var bounds = new BMap.Bounds(this._center,this._center); + var bounds = new BMapGL.Bounds(this._center, this._center); for (var i = 0, marker; marker = this._markers[i]; i++) { bounds.extend(marker.getPosition()); } @@ -608,7 +633,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; /** * 获取该聚合的落脚点。 - * @return {BMap.Point} 该聚合的落脚点。 + * @return {BMapGL.Point} 该聚合的落脚点。 */ Cluster.prototype.getCenter = function() { return this._center; diff --git a/media/js/map/text-icon-overlay.js b/media/js/map/text-icon-overlay.js index 725e04efe22..c713ea3670d 100644 --- a/media/js/map/text-icon-overlay.js +++ b/media/js/map/text-icon-overlay.js @@ -211,7 +211,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * @returns {string} 驼峰化处理后的字符串 */ baidu.string.toCamelCase = function (source) { - //提前判断,提高getStyle等的效率 thanks xianwei + //提前判断,提高getStyle等的效率 if (source.indexOf('-') < 0 && source.indexOf('_') < 0) { return source; } @@ -649,7 +649,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; * @grammar obj.dispatchEvent(event, options) * @param {baidu.lang.Event|String} event Event对象,或事件名称(1.1.1起支持) * @param {Object} options 扩展参数,所含属性键值会扩展到Event对象上(1.2起支持) - * @remark 处理会调用通过addEventListenr绑定的自定义事件回调函数之外,还会调用直接绑定到对象上面的自定义事件。例如:
+ * @remark 处理会调用通过addEventListener绑定的自定义事件回调函数之外,还会调用直接绑定到对象上面的自定义事件。例如:
myobj.onMyEvent = function(){}
myobj.addEventListener("onMyEvent", function(){}); */ @@ -754,27 +754,28 @@ var BMapLib = window.BMapLib = BMapLib || {}; (!this._styles.length) && this._setupDefaultStyles(); }; - T.lang.inherits(TextIconOverlay, BMap.Overlay, "TextIconOverlay"); + T.lang.inherits(TextIconOverlay, BMapGL.Overlay, "TextIconOverlay"); TextIconOverlay.prototype._setupDefaultStyles = function(){ var sizes = [53, 56, 66, 78, 90]; for(var i = 0, size; size = sizes[i]; i++){ this._styles.push({ url:_IMAGE_PATH + i + '.' + _IMAGE_EXTENSION, - size: new BMap.Size(size, size) + size: new BMapGL.Size(size, size) }); }//for循环的简洁写法 }; /** - *继承Overlay的intialize方法,自定义覆盖物时必须。 - *@param {Map} map BMap.Map的实例化对象。 + *继承Overlay的initialize方法,自定义覆盖物时必须。 + *@param {Map} map BMapGL.Map的实例化对象。 *@return {HTMLElement} 返回覆盖物对应的HTML元素。 */ TextIconOverlay.prototype.initialize = function(map){ this._map = map; - this._domElement = document.createElement('div'); + this._domElement = document.createElement('div'); + this._domElement.className = 'custom-image-overlay'; // this._updateCss(); // this._updateText(); this._updatePosition(); @@ -859,21 +860,20 @@ var BMapLib = window.BMapLib = BMapLib || {}; var style = this.getStyleByText(this._text, this._styles); var newStyle = { url: imageUrl, - size: {width: 72, height: 72} + size: { width: 86, height: 86 } } if (imageUrl) { - style = Object.assign(style, {url: imageUrl, size: {width: 72, height: 72}}) + style = Object.assign(style, { url: imageUrl, size: { width: 86, height: 86 } }) } - const customImageNumber = `${this._text}`; + const customImageNumber = `${this._text < 1000 ? this._text : '1k+'}`; this._domElement.style.cssText = this.buildImageCssText(newStyle); - const imageElement = `` + const imageElement = `` const htmlString = ` -
- ${this._text > 1 ? customImageNumber : ''} - ${imageUrl ? imageElement : '
'} - -
+
+ ${this._text > 1 ? customImageNumber : ''} + ${imageUrl ? imageElement : '
'} +
` const labelDocument = new DOMParser().parseFromString(htmlString, 'text/html'); const label = labelDocument.body.firstElementChild; @@ -887,14 +887,14 @@ var BMapLib = window.BMapLib = BMapLib || {}; var textColor = style['textColor'] || 'black'; var textSize = style['textSize'] || 10; - var csstext = []; + var cssText = []; - csstext.push('height:' + size.height + 'px; line-height:' + size.height + 'px;'); - csstext.push('width:' + size.width + 'px; text-align:center;'); + cssText.push('height:' + size.height + 'px; line-height:' + size.height + 'px;'); + cssText.push('width:' + size.width + 'px; text-align:center;'); - csstext.push('cursor:pointer; color:' + textColor + '; position:absolute; font-size:' + + cssText.push('cursor:pointer; color:' + textColor + '; position:absolute; font-size:' + textSize + 'px; font-family:Arial,sans-serif; font-weight:bold'); - return csstext.join(''); + return cssText.join(''); }; /** @@ -936,34 +936,34 @@ var BMapLib = window.BMapLib = BMapLib || {}; var textColor = style['textColor'] || 'black'; var textSize = style['textSize'] || 10; - var csstext = []; + var cssText = []; if (T.browser["ie"] < 7) { - csstext.push('filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(' + + cssText.push('filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(' + 'sizingMethod=scale,src="' + url + '");'); } else { - csstext.push('background-image:url(' + url + ');'); + cssText.push('background-image:url(' + url + ');'); var backgroundPosition = '0 0'; - (offset instanceof BMap.Size) && (backgroundPosition = offset.width + 'px' + ' ' + offset.height + 'px'); - csstext.push('background-position:' + backgroundPosition + ';'); + (offset instanceof BMapGL.Size) && (backgroundPosition = offset.width + 'px' + ' ' + offset.height + 'px'); + cssText.push('background-position:' + backgroundPosition + ';'); } - if (size instanceof BMap.Size){ - if (anchor instanceof BMap.Size) { + if (size instanceof BMapGL.Size){ + if (anchor instanceof BMapGL.Size) { if (anchor.height > 0 && anchor.height < size.height) { - csstext.push('height:' + (size.height - anchor.height) + 'px; padding-top:' + anchor.height + 'px;'); + cssText.push('height:' + (size.height - anchor.height) + 'px; padding-top:' + anchor.height + 'px;'); } if(anchor.width > 0 && anchor.width < size.width){ - csstext.push('width:' + (size.width - anchor.width) + 'px; padding-left:' + anchor.width + 'px;'); + cssText.push('width:' + (size.width - anchor.width) + 'px; padding-left:' + anchor.width + 'px;'); } } else { - csstext.push('height:' + size.height + 'px; line-height:' + size.height + 'px;'); - csstext.push('width:' + size.width + 'px; text-align:center;'); + cssText.push('height:' + size.height + 'px; line-height:' + size.height + 'px;'); + cssText.push('width:' + size.width + 'px; text-align:center;'); } } - csstext.push('cursor:pointer; color:' + textColor + '; position:absolute; font-size:' + + cssText.push('cursor:pointer; color:' + textColor + '; position:absolute; font-size:' + textSize + 'px; font-family:Arial,sans-serif; font-weight:bold'); - return csstext.join(''); + return cssText.join(''); }; @@ -998,9 +998,9 @@ var BMapLib = window.BMapLib = BMapLib || {}; *
"target:{BMapLib.TextIconOverlay} 事件目标 - *
"point : {BMap.Point} 最新添加上的节点BMap.Point对象 + *
"point : {BMapGL.Point} 最新添加上的节点BMap.Point对象 - *
"pixel:{BMap.pixel} 最新添加上的节点BMap.Pixel对象 + *
"pixel:{BMapGL.pixel} 最新添加上的节点BMap.Pixel对象 * @@ -1024,9 +1024,9 @@ var BMapLib = window.BMapLib = BMapLib || {}; *
"target:{BMapLib.TextIconOverlay} 事件目标 - *
"point : {BMap.Point} 最新添加上的节点BMap.Point对象 + *
"point : {BMapGL.Point} 最新添加上的节点BMap.Point对象 - *
"pixel:{BMap.pixel} 最新添加上的节点BMap.Pixel对象 + *
"pixel:{BMapGL.pixel} 最新添加上的节点BMap.Pixel对象 * @@ -1057,7 +1057,7 @@ var BMapLib = window.BMapLib = BMapLib || {}; var y = e.clientY || e.pageY; if (e && be && x && y && elem){ var offset = T.dom.getPosition(map.getContainer()); - be.pixel = new BMap.Pixel(x - offset.left, y - offset.top); + be.pixel = new BMapGL.Pixel(x - offset.left, y - offset.top); be.point = map.pixelToPoint(be.pixel); } return be; @@ -1074,4 +1074,4 @@ var BMapLib = window.BMapLib = BMapLib || {}; }); }; -})(); \ No newline at end of file +})();