diff --git a/frontend/src/assets/icons/map.svg b/frontend/src/assets/icons/map.svg new file mode 100644 index 0000000000..f1a2418d99 --- /dev/null +++ b/frontend/src/assets/icons/map.svg @@ -0,0 +1,20 @@ + + + + +map + + + + + + diff --git a/frontend/src/metadata/components/popover/view-popover/add-view/index.js b/frontend/src/metadata/components/popover/view-popover/add-view/index.js index 76bb25119a..beb594d8f2 100644 --- a/frontend/src/metadata/components/popover/view-popover/add-view/index.js +++ b/frontend/src/metadata/components/popover/view-popover/add-view/index.js @@ -19,6 +19,10 @@ const VIEW_OPTIONS = [ { key: 'kanban', type: VIEW_TYPE.KANBAN, + }, + { + key: 'map', + type: VIEW_TYPE.MAP, } ]; @@ -49,6 +53,8 @@ const AddView = ({ target, toggle, onOptionClick }) => { return gettext('Gallery'); case VIEW_TYPE.KANBAN: return gettext('Kanban'); + case VIEW_TYPE.MAP: + return gettext('Map'); default: return type; } diff --git a/frontend/src/metadata/components/view-toolbar/index.js b/frontend/src/metadata/components/view-toolbar/index.js index 0f65e75056..f890f9b074 100644 --- a/frontend/src/metadata/components/view-toolbar/index.js +++ b/frontend/src/metadata/components/view-toolbar/index.js @@ -5,6 +5,7 @@ import TableViewToolbar from './table-view-toolbar'; import GalleryViewToolbar from './gallery-view-toolbar'; import FaceRecognitionViewToolbar from './face-recognition'; import KanbanViewToolBar from './kanban-view-toolbar'; +import MapViewToolBar from './map-view-toolbar'; import './index.css'; @@ -110,6 +111,14 @@ const ViewToolBar = ({ viewId, isCustomPermission, showDetail, closeDetail }) => closeDetail={closeDetail} /> )} + {viewType === VIEW_TYPE.MAP && ( + + )} ); }; 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 new file mode 100644 index 0000000000..4be657a74f --- /dev/null +++ b/frontend/src/metadata/components/view-toolbar/map-view-toolbar/index.js @@ -0,0 +1,52 @@ +import React, { useMemo } from 'react'; +import PropTypes from 'prop-types'; +import { PRIVATE_COLUMN_KEY } from '../../../constants'; +import { FilterSetter } from '../../data-process-setter'; + +const MapViewToolBar = ({ + readOnly, + view, + collaborators, + modifyFilters, +}) => { + const viewType = useMemo(() => view.type, [view]); + const viewColumns = useMemo(() => { + if (!view) return []; + return view.columns; + }, [view]); + + const filterColumns = useMemo(() => { + return viewColumns.filter(c => c.key !== PRIVATE_COLUMN_KEY.FILE_TYPE); + }, [viewColumns]); + + return ( + <> +
+ +
+
+ + ); +}; + +MapViewToolBar.propTypes = { + readOnly: PropTypes.bool, + view: PropTypes.object, + collaborators: PropTypes.array, + modifyFilters: PropTypes.func, +}; + +export default MapViewToolBar; diff --git a/frontend/src/metadata/constants/view.js b/frontend/src/metadata/constants/view.js index 51bcd26afc..1d0a27d511 100644 --- a/frontend/src/metadata/constants/view.js +++ b/frontend/src/metadata/constants/view.js @@ -9,6 +9,7 @@ export const VIEW_TYPE = { GALLERY: 'gallery', FACE_RECOGNITION: 'face_recognition', KANBAN: 'kanban', + MAP: 'map', }; export const FACE_RECOGNITION_VIEW_ID = '_face_recognition'; @@ -18,6 +19,7 @@ export const VIEW_TYPE_ICON = { [VIEW_TYPE.GALLERY]: 'image', [VIEW_TYPE.FACE_RECOGNITION]: 'face-recognition-view', [VIEW_TYPE.KANBAN]: 'kanban', + [VIEW_TYPE.MAP]: 'map', 'image': 'image' }; @@ -52,24 +54,38 @@ export const VIEW_TYPE_DEFAULT_BASIC_FILTER = { filter_term: [] }, ], + [VIEW_TYPE.MAP]: [ + { + column_key: PRIVATE_COLUMN_KEY.IS_DIR, + filter_predicate: FILTER_PREDICATE_TYPE.IS, + filter_term: 'file' + }, { + column_key: PRIVATE_COLUMN_KEY.FILE_TYPE, + filter_predicate: FILTER_PREDICATE_TYPE.IS_ANY_OF, + filter_term: ['_picture'] + }, + ], }; export const VIEW_TYPE_DEFAULT_SORTS = { [VIEW_TYPE.TABLE]: [], [VIEW_TYPE.GALLERY]: [{ column_key: PRIVATE_COLUMN_KEY.FILE_CTIME, sort_type: SORT_TYPE.DOWN }], [VIEW_TYPE.KANBAN]: [], + [VIEW_TYPE.MAP]: [], }; export const VIEW_SORT_COLUMN_RULES = { [VIEW_TYPE.TABLE]: (column) => SORT_COLUMN_OPTIONS.includes(column.type), [VIEW_TYPE.GALLERY]: (column) => GALLERY_SORT_COLUMN_OPTIONS.includes(column.type) || GALLERY_SORT_PRIVATE_COLUMN_KEYS.includes(column.key), [VIEW_TYPE.KANBAN]: (column) => SORT_COLUMN_OPTIONS.includes(column.type), + [VIEW_TYPE.MAP]: () => {}, }; export const VIEW_FIRST_SORT_COLUMN_RULES = { [VIEW_TYPE.TABLE]: (column) => SORT_COLUMN_OPTIONS.includes(column.type), [VIEW_TYPE.GALLERY]: (column) => GALLERY_FIRST_SORT_COLUMN_OPTIONS.includes(column.type) || GALLERY_FIRST_SORT_PRIVATE_COLUMN_KEYS.includes(column.key), [VIEW_TYPE.KANBAN]: (column) => SORT_COLUMN_OPTIONS.includes(column.type), + [VIEW_TYPE.MAP]: () => {}, }; export const KANBAN_SETTINGS_KEYS = { diff --git a/frontend/src/metadata/metadata-tree-view/index.js b/frontend/src/metadata/metadata-tree-view/index.js index 9f2ebe919b..5fd485c926 100644 --- a/frontend/src/metadata/metadata-tree-view/index.js +++ b/frontend/src/metadata/metadata-tree-view/index.js @@ -32,6 +32,9 @@ const updateFavicon = (type) => { case VIEW_TYPE.KANBAN: favicon.href = `${mediaUrl}favicons/kanban.png`; break; + case VIEW_TYPE.MAP: + favicon.href = `${mediaUrl}favicons/map.png`; + break; default: favicon.href = `${mediaUrl}favicons/favicon.png`; } diff --git a/frontend/src/metadata/metadata-view/view.js b/frontend/src/metadata/metadata-view/view.js index cc1a3bd62a..623265a937 100644 --- a/frontend/src/metadata/metadata-view/view.js +++ b/frontend/src/metadata/metadata-view/view.js @@ -4,6 +4,7 @@ import Table from '../views/table'; import Gallery from '../views/gallery'; import FaceRecognition from '../views/face-recognition'; import Kanban from '../views/kanban'; +import Map from '../views/map'; import { useMetadataView } from '../hooks/metadata-view'; import { gettext } from '../../utils/constants'; import { VIEW_TYPE } from '../constants'; @@ -27,6 +28,9 @@ const View = () => { case VIEW_TYPE.KANBAN: { return ; } + case VIEW_TYPE.MAP: { + return ; + } default: return null; } diff --git a/frontend/src/metadata/store/data-processor.js b/frontend/src/metadata/store/data-processor.js index 2e89097e72..8de8f98d13 100644 --- a/frontend/src/metadata/store/data-processor.js +++ b/frontend/src/metadata/store/data-processor.js @@ -7,9 +7,9 @@ import { getRowsByIds } from '../utils/table'; import { isGroupView } from '../utils/view'; import { username } from '../../utils/constants'; import { COLUMN_DATA_OPERATION_TYPE, OPERATION_TYPE } from './operations'; -import { CellType, PRIVATE_COLUMN_KEY } from '../constants'; +import { CellType } from '../constants'; import { getCellValueByColumn, getOption, isValidCellValue, checkIsPredefinedOption, getColumnOptionIdsByNames, - getColumnOptionNamesByIds + getColumnOptionNamesByIds, geRecordIdFromRecord, } from '../utils/cell'; // const DEFAULT_COMPUTER_PROPERTIES_CONTROLLER = { @@ -181,7 +181,8 @@ class DataProcessor { const newOptionNames = getColumnOptionNamesByIds(newColumn, oldOptionIds); row[columnOriginalName] = newOptionNames ? newOptionNames : null; } - table.id_row_map[row[PRIVATE_COLUMN_KEY.ID]] = row; + const id = geRecordIdFromRecord(row); + table.id_row_map[id] = row; } } } diff --git a/frontend/src/metadata/store/operations/apply.js b/frontend/src/metadata/store/operations/apply.js index b09cc4f5f2..5f95501a0d 100644 --- a/frontend/src/metadata/store/operations/apply.js +++ b/frontend/src/metadata/store/operations/apply.js @@ -1,10 +1,11 @@ import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; -import { PRIVATE_COLUMN_KEY, UTC_FORMAT_DEFAULT } from '../../constants'; +import { UTC_FORMAT_DEFAULT } from '../../constants'; import { OPERATION_TYPE } from './constants'; import Column from '../../model/metadata/column'; import View from '../../model/metadata/view'; import { getColumnOriginName } from '../../utils/column'; +import { geRecordIdFromRecord } from '../../utils/cell'; dayjs.extend(utc); @@ -141,8 +142,9 @@ export default function apply(data, operation) { let id_row_map = {}; data.rows.forEach(row => { delete row[columnOriginName]; + const id = geRecordIdFromRecord(row); rows.push(row); - id_row_map[row[PRIVATE_COLUMN_KEY.ID]] = row; + id_row_map[id] = row; }); data.id_row_map = id_row_map; delete data.key_column_map[column_key]; diff --git a/frontend/src/metadata/utils/cell/cell-format-utils.js b/frontend/src/metadata/utils/cell/cell-format-utils.js index 4c182dd5e7..ed6f7f8810 100644 --- a/frontend/src/metadata/utils/cell/cell-format-utils.js +++ b/frontend/src/metadata/utils/cell/cell-format-utils.js @@ -13,40 +13,6 @@ const getAutoTimeDisplayString = (autoTime) => { return date.format('YYYY-MM-DD HH:mm:ss'); }; -export const getClientCellValueDisplayString = (record, column, { collaborators = [] } = {}) => { - const cellValue = getCellValueByColumn(record, column); - const { type } = column; - if (type === CellType.CTIME || type === CellType.MTIME) { - return getAutoTimeDisplayString(cellValue); - } - return getCellValueDisplayString(record, column, { collaborators }); -}; - -export const getFormatRecordData = (columns, recordData) => { - let keyColumnMap = {}; - columns.forEach(column => { - keyColumnMap[column.key] = column; - }); - return convertedToRecordData(recordData, keyColumnMap); -}; - -export const getFormattedRecordsData = (recordsData, columns, excludesColumnTypes) => { - let keyColumnMap = {}; - columns.forEach(column => { - keyColumnMap[column.key] = column; - }); - return recordsData.map(recordData => { - let formattedRecordsData = convertedToRecordData(recordData, keyColumnMap, excludesColumnTypes); - if (recordData._id) { - formattedRecordsData._id = recordData._id; - } - if (Object.prototype.hasOwnProperty.call(recordData, '_archived')) { - formattedRecordsData._archived = recordData._archived ? 'true' : 'false'; - } - return formattedRecordsData; - }); -}; - // { [column.key]: cellValue } -> { [column.name]: cellValue } // { [option-column.key]: option.id } -> { [option-column.name]: option.name } function convertedToRecordData(originRecordData, keyColumnMap, excludesColumnTypes = []) { @@ -80,3 +46,37 @@ function convertedToRecordData(originRecordData, keyColumnMap, excludesColumnTyp }); return recordData; } + +export const getClientCellValueDisplayString = (record, column, { collaborators = [] } = {}) => { + const cellValue = getCellValueByColumn(record, column); + const { type } = column; + if (type === CellType.CTIME || type === CellType.MTIME) { + return getAutoTimeDisplayString(cellValue); + } + return getCellValueDisplayString(record, column, { collaborators }); +}; + +export const getFormatRecordData = (columns, recordData) => { + let keyColumnMap = {}; + columns.forEach(column => { + keyColumnMap[column.key] = column; + }); + return convertedToRecordData(recordData, keyColumnMap); +}; + +export const getFormattedRecordsData = (recordsData, columns, excludesColumnTypes) => { + let keyColumnMap = {}; + columns.forEach(column => { + keyColumnMap[column.key] = column; + }); + return recordsData.map(recordData => { + let formattedRecordsData = convertedToRecordData(recordData, keyColumnMap, excludesColumnTypes); + if (recordData._id) { + formattedRecordsData._id = recordData._id; + } + if (Object.prototype.hasOwnProperty.call(recordData, '_archived')) { + formattedRecordsData._archived = recordData._archived ? 'true' : 'false'; + } + return formattedRecordsData; + }); +}; diff --git a/frontend/src/metadata/utils/cell/core.js b/frontend/src/metadata/utils/cell/core.js index 868b55e3c7..b484d5c395 100644 --- a/frontend/src/metadata/utils/cell/core.js +++ b/frontend/src/metadata/utils/cell/core.js @@ -39,3 +39,11 @@ export const geRecordIdFromRecord = record => { export const getFileObjIdFromRecord = record => { return record ? record[PRIVATE_COLUMN_KEY.OBJ_ID] : ''; }; + +export const getImageLocationFromRecord = (record) => { + return record ? record[PRIVATE_COLUMN_KEY.LOCATION] : null; +}; + +export const getFileTypeFromRecord = (record) => { + return record ? record[PRIVATE_COLUMN_KEY.FILE_TYPE] : null; +}; diff --git a/frontend/src/metadata/utils/cell/index.js b/frontend/src/metadata/utils/cell/index.js index 89bbbf0b9f..a210323489 100644 --- a/frontend/src/metadata/utils/cell/index.js +++ b/frontend/src/metadata/utils/cell/index.js @@ -1,42 +1,5 @@ -export { - isValidCellValue, - getCellValueByColumn, - getParentDirFromRecord, - getFileNameFromRecord, - geRecordIdFromRecord, - getFileObjIdFromRecord, -} from './core'; - -export { - getCellValueDisplayString, - getCellValueStringResult, -} from './common'; - -export { - getDateDisplayString, - getPrecisionNumber, - getNumberDisplayString, - replaceNumberNotAllowInput, - formatStringToNumber, - formatTextToNumber, - checkIsPredefinedOption, - getOption, - getColumnOptionNameById, - getOptionName, - getMultipleOptionName, - getCollaborator, - getCollaboratorsNames, - getCollaboratorsName, - getCollaboratorEmailsByNames, - getLongtextDisplayString, - getGeolocationDisplayString, - getGeolocationByGranularity, - getFloatNumber, - getColumnOptionNamesByIds, - getColumnOptionIdsByNames, - decimalToExposureTime, -} from './column'; - -export { isCellValueChanged } from './cell-comparer'; - -export { getClientCellValueDisplayString } from './cell-format-utils'; +export * from './core'; +export * from './common'; +export * from './column'; +export * from './cell-comparer'; +export * from './cell-format-utils'; diff --git a/frontend/src/metadata/views/map/custom-avatar-overlay.js b/frontend/src/metadata/views/map/custom-avatar-overlay.js new file mode 100644 index 0000000000..72d97808da --- /dev/null +++ b/frontend/src/metadata/views/map/custom-avatar-overlay.js @@ -0,0 +1,46 @@ +const customAvatarOverlay = (point, avatarUrl, bgUrl, width = 20, height = 25) => { + class AvatarOverlay extends window.BMap.Overlay { + constructor(point, avatarUrl, bgUrl, width, height) { + super(); + this._point = point; + this._headerImg = avatarUrl; + this._bgUrl = bgUrl; + this._width = width; + this._height = height; + } + + initialize(map) { + this._map = map; + const divBox = document.createElement('div'); + const divImg = new Image(); + divBox.style.position = 'absolute'; + divBox.style.width = `${this._width}px`; + divBox.style.height = `${this._height}px`; + divBox.style.backgroundImage = `url(${this._bgUrl})`; + divBox.style.backgroundPosition = '.5px 0px'; + divBox.style.display = 'flex'; + divBox.style.padding = '2px 2.5px 0 2px'; + + divImg.src = this._headerImg; + divImg.style.width = '16px'; + divImg.style.height = '16px'; + divImg.style.borderRadius = '50%'; + divImg.style.display = 'block'; + divBox.appendChild(divImg); + + map.getPanes().markerPane.appendChild(divBox); + this._div = divBox; + return divBox; + } + + draw() { + const position = this._map.pointToOverlayPixel(this._point); + this._div.style.left = `${position.x - this._width / 2}px`; + this._div.style.top = `${position.y - (this._height * 7) / 10}px`; + } + } + + return new AvatarOverlay(point, avatarUrl, bgUrl, width, height); +}; + +export default customAvatarOverlay; diff --git a/frontend/src/metadata/views/map/custom-image-overlay.js b/frontend/src/metadata/views/map/custom-image-overlay.js new file mode 100644 index 0000000000..ddb61059b9 --- /dev/null +++ b/frontend/src/metadata/views/map/custom-image-overlay.js @@ -0,0 +1,69 @@ +import { Utils } from '../../../utils/utils'; + +const customImageOverlay = (center, imageUrl) => { + class ImageOverlay extends window.BMap.Overlay { + constructor(center, imageUrl) { + super(); + this._center = center; + this._imageUrl = imageUrl; + } + + initialize(map) { + this._map = map; + const div = document.createElement('div'); + div.style.position = 'absolute'; + div.style.width = '80px'; + div.style.height = '80px'; + div.style.zIndex = 2000; + map.getPanes().markerPane.appendChild(div); + this._div = div; + + const imageElement = ``; + const htmlString = + ` +
+ ${this._imageUrl ? imageElement : '
'} + +
+ `; + const labelDocument = new DOMParser().parseFromString(htmlString, 'text/html'); + const label = labelDocument.body.firstElementChild; + this._div.append(label); + + const eventHandler = (event) => { + event.stopPropagation(); + event.preventDefault(); + }; + + if (Utils.isDesktop()) { + this._div.addEventListener('click', eventHandler); + } else { + this._div.addEventListener('touchend', eventHandler); + } + + return div; + } + + draw() { + const position = this._map.pointToOverlayPixel(this._center); + this._div.style.left = position.x - 40 + 'px'; // 40 is 1/2 container height + this._div.style.top = position.y - 88 + 'px'; // 80 is container height and 8 is icon height + } + + getImageUrl() { + return imageUrl || ''; + } + + getPosition() { + return center; + } + + getMap() { + return this._map || null; + } + } + + return new ImageOverlay(center, imageUrl); +}; + +export default customImageOverlay; diff --git a/frontend/src/metadata/views/map/geolocation-control.js b/frontend/src/metadata/views/map/geolocation-control.js new file mode 100644 index 0000000000..3a576253a4 --- /dev/null +++ b/frontend/src/metadata/views/map/geolocation-control.js @@ -0,0 +1,50 @@ +import { mediaUrl } from '../../../utils/constants'; +import { Utils } from '../../../utils/utils'; + +export function createBMapGeolocationControl(BMap, callback) { + function GeolocationControl() { + this.defaultAnchor = window.BMAP_ANCHOR_BOTTOM_RIGHT; + this.defaultOffset = new BMap.Size(10, Utils.isDesktop() ? 20 : 90); + } + GeolocationControl.prototype = new window.BMap.Control(); + GeolocationControl.prototype.initialize = function (map) { + const div = document.createElement('div'); + div.className = 'sf-BMap-geolocation-control'; + div.style = 'display: flex; justify-content: center; align-items: center;'; + + const icon = document.createElement('img'); + icon.className = 'sf-BMap-icon-current-location'; + icon.src = `${mediaUrl}/img/current-location.svg`; + icon.style = 'width: 16px; height: 16px; display: block;'; + div.appendChild(icon); + if (Utils.isDesktop()) { + setNodeStyle(div, 'height: 30px; width: 30px; line-height: 30px'); + } else { + setNodeStyle(div, 'height: 35px; width: 35px; line-height: 35px; opacity: 0.75'); + } + div.onclick = (e) => { + e.preventDefault(); + const geolocation = new BMap.Geolocation(); + div.className = 'sf-BMap-geolocation-control sf-BMap-geolocation-control-loading'; + geolocation.getCurrentPosition((result) => { + div.className = 'sf-BMap-geolocation-control'; + if (result) { + const point = result.point; + map.setCenter(point); + callback(null, point); + } else { + // Positioning failed + callback(true); + } + }); + }; + map.getContainer().appendChild(div); + return div; + }; + + return GeolocationControl; +} + +function setNodeStyle(dom, styleText) { + dom.style.cssText += styleText; +} diff --git a/frontend/src/metadata/views/map/index.css b/frontend/src/metadata/views/map/index.css new file mode 100644 index 0000000000..11b00f6ea1 --- /dev/null +++ b/frontend/src/metadata/views/map/index.css @@ -0,0 +1,85 @@ +.sf-metadata-view-map { + display: flex; + flex-direction: column; +} + +.sf-metadata-view-map .sf-metadata-map-container { + width: 100%;; + height: 100%; + min-height: 0; +} + +.sf-metadata-view-map .custom-image-container { + padding: 4px; + background: #fff; + width: 80px; + height: 80px; + cursor: default; + border-radius: 4px; + box-shadow: 0 1px 3px 0 rgb(0 0 0 / 10%); + position: relative; +} + +.sf-metadata-view-map .custom-image-number { + position: absolute; + right: -15px; + top: -8px; + padding: 0 12px; + background: #20AD7E; + color: #fff; + border-radius: 10px; + text-align: center; + font-size: 14px; + line-height: 20px; +} + +.sf-metadata-view-map .custom-image-container .plugin-label-arrow { + position: absolute; + bottom: 5px; + transform: translate( -50%, 100%); + left: 50%; + color: #fff; + display: inline-block; + line-height: 16px; + height: 16px; +} + +.sf-metadata-view-map .custom-image-container .image-overlay-arrow { + bottom: 5px; + color: #fff; + display: inline-block; + height: 16px; + left: 50%; + line-height: 16px; + position: absolute; + transform: translate(-50%, 100%); +} + +.sf-metadata-view-map .custom-image-container::after { + content: ''; + position: absolute; + bottom: -10px; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 10px solid transparent; + border-right: 10px solid transparent; + border-top: 10px solid #fff; +} + +.sf-metadata-view-map .sf-BMap-geolocation-control { + background-color: #ffffff; + box-shadow: 0 0 4px rgb(0 0 0 / 12%); + border-radius: 4px; + text-align: center; + color: #212529; +} + +.sf-metadata-view-map .sf-BMap-geolocation-control-loading { + opacity: 0.7; +} + +.sf-metadata-view-map .sf-BMap-geolocation-control:hover { + background-color: #f5f5f5; +} diff --git a/frontend/src/metadata/views/map/index.js b/frontend/src/metadata/views/map/index.js new file mode 100644 index 0000000000..3e0b7877df --- /dev/null +++ b/frontend/src/metadata/views/map/index.js @@ -0,0 +1,171 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { CenteredLoading } from '@seafile/sf-metadata-ui-component'; +import loadBMap, { initMapInfo } from '../../../utils/map-utils'; +import { wgs84_to_gcj02, gcj02_to_bd09 } from '../../../utils/coord-transform'; +import { MAP_TYPE } from '../../../constants'; +import { isValidPosition } from '../../utils/validate'; +import { appAvatarURL, baiduMapKey, gettext, googleMapKey, mediaUrl, siteRoot, thumbnailSizeForGrid } from '../../../utils/constants'; +import { useMetadataView } from '../../hooks/metadata-view'; +import { PREDEFINED_FILE_TYPE_OPTION_KEY } from '../../constants'; +import { geRecordIdFromRecord, getFileNameFromRecord, getImageLocationFromRecord, getParentDirFromRecord, + getFileTypeFromRecord +} from '../../utils/cell'; +import { Utils } from '../../../utils/utils'; +import customImageOverlay from './custom-image-overlay'; +import customAvatarOverlay from './custom-avatar-overlay'; +import { createBMapGeolocationControl } from './geolocation-control'; +import toaster from '../../../components/toast'; + +import './index.css'; + +const DEFAULT_POSITION = { lng: 104.195, lat: 35.861 }; +const DEFAULT_ZOOM = 4; +const BATCH_SIZE = 500; + +const Map = () => { + const [isLoading, setIsLoading] = useState(true); + + const mapRef = useRef(null); + const clusterRef = useRef(null); + const batchIndexRef = useRef(0); + + const { metadata } = useMetadataView(); + + const mapInfo = useMemo(() => initMapInfo({ baiduMapKey, googleMapKey }), []); + const repoID = window.sfMetadataContext.getSetting('repoID'); + + const validImages = useMemo(() => { + return metadata.rows + .map(record => { + const recordType = getFileTypeFromRecord(record); + if (recordType !== PREDEFINED_FILE_TYPE_OPTION_KEY.PICTURE) return null; + const id = geRecordIdFromRecord(record); + const fileName = getFileNameFromRecord(record); + const parentDir = getParentDirFromRecord(record); + const path = Utils.encodePath(Utils.joinPath(parentDir, fileName)); + const src = `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForGrid}${path}`; + const location = getImageLocationFromRecord(record); + if (!location) return null; + const { lng, lat } = location; + return isValidPosition(lng, lat) ? { id, src, lng, lat } : null; + }) + .filter(Boolean); + }, [repoID, metadata]); + + const addMapController = useCallback(() => { + var navigation = new window.BMap.NavigationControl(); + const GeolocationControl = createBMapGeolocationControl(window.BMap, (err, point) => { + if (!err && point) { + mapRef.current.setCenter({ lng: point.lng, lat: point.lat }); + } + }); + const geolocationControl = new GeolocationControl(); + mapRef.current.addControl(geolocationControl); + mapRef.current.addControl(navigation); + }, []); + + const renderMarkersBatch = useCallback(() => { + if (!validImages.length || !clusterRef.current) return; + + const startIndex = batchIndexRef.current * BATCH_SIZE; + const endIndex = Math.min(startIndex + BATCH_SIZE, validImages.length); + const batchMarkers = []; + + for (let i = startIndex; i < endIndex; i++) { + const image = validImages[i]; + const { lng, lat } = image; + const point = new window.BMap.Point(lng, lat); + const marker = customImageOverlay(point, image.src); + batchMarkers.push(marker); + } + + clusterRef.current.addMarkers(batchMarkers); + + if (endIndex < validImages.length) { + batchIndexRef.current += 1; + setTimeout(renderMarkersBatch, 20); // Schedule the next batch + } + }, [validImages]); + + const initializeClusterer = useCallback(() => { + if (mapRef.current && !clusterRef.current) { + clusterRef.current = new window.BMapLib.MarkerClusterer(mapRef.current); + } + }, []); + + const initializeUserMarker = useCallback(() => { + if (!window.BMap) return; + + const imageUrl = `${mediaUrl}/img/marker.png`; + const addMarker = (lng, lat) => { + const point = new window.BMap.Point(lng, lat); + const avatarMarker = customAvatarOverlay(point, appAvatarURL, imageUrl); + mapRef.current.addOverlay(avatarMarker); + }; + + if (!navigator.geolocation) { + addMarker(DEFAULT_POSITION.lng, DEFAULT_POSITION.lat); + return; + } + navigator.geolocation.getCurrentPosition( + position => addMarker(position.coords.longitude, position.coords.latitude), + () => { + addMarker(DEFAULT_POSITION.lng, DEFAULT_POSITION.lat); + toaster.danger(gettext('Failed to get user location')); + } + ); + }, []); + + const renderBaiduMap = useCallback(() => { + setIsLoading(false); + if (!window.BMap.Map) return; + let mapCenter = window.sfMetadataContext.localStorage.getItem('map-center') || DEFAULT_POSITION; + // ask for user location + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition((userInfo) => { + mapCenter = { lng: userInfo.coords.longitude, lat: userInfo.coords.latitude }; + window.sfMetadataContext.localStorage.setItem('map-center', mapCenter); + }); + } + if (!isValidPosition(mapCenter?.lng, mapCenter?.lat)) return; + + const gcPosition = wgs84_to_gcj02(mapCenter.lng, mapCenter.lat); + const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat); + const { lng, lat } = bdPosition; + + mapRef.current = new window.BMap.Map('sf-metadata-map-container', { enableMapClick: false }); + const point = new window.BMap.Point(lng, lat); + mapRef.current.centerAndZoom(point, DEFAULT_ZOOM); + mapRef.current.enableScrollWheelZoom(true); + + addMapController(); + initializeUserMarker(); + initializeClusterer(); + + batchIndexRef.current = 0; // Reset batch index + renderMarkersBatch(); + }, [addMapController, initializeClusterer, initializeUserMarker, renderMarkersBatch]); + + useEffect(() => { + if (mapInfo.type === MAP_TYPE.B_MAP) { + window.renderMap = renderBaiduMap; + loadBMap(mapInfo.key).then(() => renderBaiduMap()); + return () => { + window.renderMap = null; + }; + } + return; + }, [mapInfo, renderBaiduMap]); + + return ( +
+ {isLoading ? ( + + ) : ( +
+ )} +
+ ); +}; + +export default Map; diff --git a/frontend/src/utils/map-utils.js b/frontend/src/utils/map-utils.js index c98ef10af7..1b68b04acd 100644 --- a/frontend/src/utils/map-utils.js +++ b/frontend/src/utils/map-utils.js @@ -1,4 +1,5 @@ import { MAP_TYPE } from '../constants'; +import { mediaUrl } from './constants'; export const initMapInfo = ({ baiduMapKey, googleMapKey, mineMapKey }) => { if (baiduMapKey) return { type: MAP_TYPE.B_MAP, key: baiduMapKey }; @@ -25,3 +26,41 @@ export const loadMapSource = (type, key, callback) => { } callback && callback(); }; + +export default function loadBMap(ak) { + return new Promise((resolve, reject) => { + asyncLoadBaiduJs(ak) + .then(() => asyncLoadJs(`${mediaUrl}/js/map/text-icon-overlay.js`)) + .then(() => asyncLoadJs(`${mediaUrl}/js/map/marker-clusterer.js`)) + .then(() => resolve(true)) + .catch((err) => reject(err)); + }); +} + +export function asyncLoadBaiduJs(ak) { + return new Promise((resolve, reject) => { + if (typeof window.BMap !== 'undefined') { + resolve(window.BMap); + return; + } + window.renderMap = function () { + resolve(window.BMap); + }; + let script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = `https://api.map.baidu.com/api?v=3.0&ak=${ak}&callback=renderMap`; + script.onerror = reject; + document.body.appendChild(script); + }); +} + +export function asyncLoadJs(url) { + return new Promise((resolve, reject) => { + let script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = url; + document.body.appendChild(script); + script.onload = resolve; + script.onerror = reject; + }); +} diff --git a/media/favicons/map.png b/media/favicons/map.png new file mode 100644 index 0000000000..a797129ee4 Binary files /dev/null and b/media/favicons/map.png differ diff --git a/media/img/current-location.svg b/media/img/current-location.svg new file mode 100644 index 0000000000..1015ffb609 --- /dev/null +++ b/media/img/current-location.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/media/img/marker.png b/media/img/marker.png new file mode 100644 index 0000000000..38a5900941 Binary files /dev/null and b/media/img/marker.png differ diff --git a/media/js/map/marker-clusterer.js b/media/js/map/marker-clusterer.js new file mode 100644 index 0000000000..66cc930def --- /dev/null +++ b/media/js/map/marker-clusterer.js @@ -0,0 +1,617 @@ +/** + * @fileoverview MarkerClusterer标记聚合器用来解决加载大量点要素到地图上产生覆盖现象的问题,并提高性能。 + * 主入口类是MarkerClusterer, + * 基于Baidu Map API 1.2。 + * + * @author Baidu Map Api Group + * @version 1.2 + */ + +/** + * @namespace BMap的所有library类均放在BMapLib命名空间下 + */ +var BMapLib = window.BMapLib = BMapLib || {}; +(function(){ + + /** + * 获取一个扩展的视图范围,把上下左右都扩大一样的像素值。 + * @param {Map} map BMap.Map的实例化对象 + * @param {BMap.Bounds} bounds BMap.Bounds的实例化对象 + * @param {Number} gridSize 要扩大的像素值 + * + * @return {BMap.Bounds} 返回扩大后的视图范围。 + */ + var getExtendedBounds = function(map, bounds, gridSize){ + bounds = cutBoundsInRange(bounds); + var pixelNE = map.pointToPixel(bounds.getNorthEast()); + var pixelSW = map.pointToPixel(bounds.getSouthWest()); + pixelNE.x += gridSize; + pixelNE.y -= gridSize; + pixelSW.x -= gridSize; + pixelSW.y += gridSize; + var newNE = map.pixelToPoint(pixelNE); + var newSW = map.pixelToPoint(pixelSW); + return new BMap.Bounds(newSW, newNE); + }; + + /** + * 按照百度地图支持的世界范围对bounds进行边界处理 + * @param {BMap.Bounds} bounds BMap.Bounds的实例化对象 + * + * @return {BMap.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)); + }; + + /** + * 对单个值进行边界处理。 + * @param {Number} i 要处理的数值 + * @param {Number} min 下边界值 + * @param {Number} max 上边界值 + * + * @return {Number} 返回不越界的数值 + */ + var getRange = function (i, mix, max) { + mix && (i = Math.max(i, mix)); + max && (i = Math.min(i, max)); + return i; + }; + + /** + * 判断给定的对象是否为数组 + * @param {Object} source 要测试的对象 + * + * @return {Boolean} 如果是数组返回true,否则返回false + */ + var isArray = function (source) { + return '[object Array]' === Object.prototype.toString.call(source); + }; + + /** + * 返回item在source中的索引位置 + * @param {Object} item 要测试的对象 + * @param {Array} source 数组 + * + * @return {Number} 如果在数组内,返回索引,否则返回-1 + */ + var indexOf = function(item, source){ + var index = -1; + if(isArray(source)){ + if (source.indexOf) { + index = source.indexOf(item); + } else { + for (var i = 0, m; m = source[i]; i++) { + if (m === item) { + index = i; + break; + } + } + } + } + return index; + }; + + /** + *@exports MarkerClusterer as BMapLib.MarkerClusterer + */ + var MarkerClusterer = + /** + * MarkerClusterer + * @class 用来解决加载大量点要素到地图上产生覆盖现象的问题,并提高性能 + * @constructor + * @param {Map} map 地图的一个实例。 + * @param {Json Object} options 可选参数,可选项包括:
+ * markers {Array} 要聚合的标记数组
+ * girdSize {Number} 聚合计算时网格的像素大小,默认60
+ * maxZoom {Number} 最大的聚合级别,大于该级别就不进行相应的聚合
+ * minClusterSize {Number} 最小的聚合数量,小于该数量的不能成为一个聚合,默认为2
+ * isAverangeCenter {Boolean} 聚合点的落脚位置是否是所有聚合在内点的平均值,默认为否,落脚在聚合内的第一个点
+ * styles {Array} 自定义聚合后的图标风格,请参考TextIconOverlay类
+ */ + BMapLib.MarkerClusterer = function(map, options){ + if (!map){ + return; + } + this._map = map; + this._markers = []; + this._clusters = []; + + var opts = options || {}; + this._gridSize = opts["gridSize"] || 60; + this._maxZoom = opts["maxZoom"] || 18; + this._minClusterSize = opts["minClusterSize"] || 2; + this._isAverageCenter = false; + if (opts['isAverageCenter'] != undefined) { + this._isAverageCenter = opts['isAverageCenter']; + } + this._styles = opts["styles"] || []; + + var that = this; + this._map.addEventListener("zoomend",function(){ + that._redraw(); + }); + + // this._map.addEventListener("moveend",function(){ + // that._redraw(); + // }); + + var mkrs = opts["markers"]; + isArray(mkrs) && this.addMarkers(mkrs); + }; + + /** + * 添加要聚合的标记数组。 + * @param {Array} markers 要聚合的标记数组 + * + * @return 无返回值。 + */ + MarkerClusterer.prototype.addMarkers = function(markers){ + for(var i = 0, len = markers.length; i } markers 需要被删除的marker数组 + * + * @return {Boolean} 删除成功返回true,否则返回false + */ + MarkerClusterer.prototype.removeMarkers = function(markers) { + var success = false; + for (var i = 0; i < markers.length; i++) { + var r = this._removeMarker(markers[i]); + success = success || r; + } + + if (success) { + this._clearLastClusters(); + this._createClusters(); + } + return success; + }; + + /** + * 从地图上彻底清除所有的标记 + * @return 无返回值 + */ + MarkerClusterer.prototype.clearMarkers = function() { + this._clearLastClusters(); + this._removeMarkersFromMap(); + this._markers = []; + }; + + /** + * 重新生成,比如改变了属性等 + * @return 无返回值 + */ + MarkerClusterer.prototype._redraw = function () { + this._clearLastClusters(); + this._createClusters(); + }; + + /** + * 获取网格大小 + * @return {Number} 网格大小 + */ + MarkerClusterer.prototype.getGridSize = function() { + return this._gridSize; + }; + + /** + * 设置网格大小 + * @param {Number} size 网格大小 + * @return 无返回值 + */ + MarkerClusterer.prototype.setGridSize = function(size) { + this._gridSize = size; + this._redraw(); + }; + + /** + * 获取聚合的最大缩放级别。 + * @return {Number} 聚合的最大缩放级别。 + */ + MarkerClusterer.prototype.getMaxZoom = function() { + return this._maxZoom; + }; + + /** + * 设置聚合的最大缩放级别 + * @param {Number} maxZoom 聚合的最大缩放级别 + * @return 无返回值 + */ + MarkerClusterer.prototype.setMaxZoom = function(maxZoom) { + this._maxZoom = maxZoom; + this._redraw(); + }; + + /** + * 获取聚合的样式风格集合 + * @return {Array} 聚合的样式风格集合 + */ + MarkerClusterer.prototype.getStyles = function() { + return this._styles; + }; + + /** + * 设置聚合的样式风格集合 + * @param {Array} styles 样式风格数组 + * @return 无返回值 + */ + MarkerClusterer.prototype.setStyles = function(styles) { + this._styles = styles; + this._redraw(); + }; + + /** + * 获取单个聚合的最小数量。 + * @return {Number} 单个聚合的最小数量。 + */ + MarkerClusterer.prototype.getMinClusterSize = function() { + return this._minClusterSize; + }; + + /** + * 设置单个聚合的最小数量。 + * @param {Number} size 单个聚合的最小数量。 + * @return 无返回值。 + */ + MarkerClusterer.prototype.setMinClusterSize = function(size) { + this._minClusterSize = size; + this._redraw(); + }; + + /** + * 获取单个聚合的落脚点是否是聚合内所有标记的平均中心。 + * @return {Boolean} true或false。 + */ + MarkerClusterer.prototype.isAverageCenter = function() { + return this._isAverageCenter; + }; + + /** + * 获取聚合的Map实例。 + * @return {Map} Map的示例。 + */ + MarkerClusterer.prototype.getMap = function() { + return this._map; + }; + + /** + * 获取所有的标记数组。 + * @return {Array} 标记数组。 + */ + MarkerClusterer.prototype.getMarkers = function() { + return this._markers; + }; + + /** + * 获取聚合的总数量。 + * @return {Number} 聚合的总数量。 + */ + MarkerClusterer.prototype.getClustersCount = function() { + var count = 0; + for(var i = 0, cluster; cluster = this._clusters[i]; i++){ + cluster.isReal() && count++; + } + return count; + }; + + /** + * @ignore + * Cluster + * @class 表示一个聚合对象,该聚合,包含有N个标记,这N个标记组成的范围,并有予以显示在Map上的TextIconOverlay等。 + * @constructor + * @param {MarkerClusterer} markerClusterer 一个标记聚合器示例。 + */ + function Cluster(markerClusterer){ + this._markerClusterer = markerClusterer; + this._map = markerClusterer.getMap(); + this._minClusterSize = markerClusterer.getMinClusterSize(); + this._isAverageCenter = markerClusterer.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._map.addOverlay(this._clusterMarker); + } + + /** + * 向该聚合添加一个标记。 + * @param {Marker} marker 要添加的标记。 + * @return 无返回值。 + */ + Cluster.prototype.addMarker = function(marker){ + if(this.isMarkerInCluster(marker)){ + return false; + }//也可用marker.isInCluster判断,外面判断OK,这里基本不会命中 + + if (!this._center){ + this._center = marker.getPosition(); + this.updateGridBounds();// + } else { + if(this._isAverageCenter){ + 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.updateGridBounds(); + }//计算新的Center + } + + marker.isInCluster = true; + this._markers.push(marker); + }; + + /** + * 进行dom操作 + * @return 无返回值 + */ + Cluster.prototype.render = function(){ + var len = this._markers.length; + + if (len < this._minClusterSize) { + for (var i = 0; i < len; i++) { + this._map.addOverlay(this._markers[i]); + } + } else { + this._map.addOverlay(this._clusterMarker); + this._isReal = true; + this.updateClusterMarker(); + } + } + + /** + * 判断一个标记是否在该聚合中。 + * @param {Marker} marker 要判断的标记。 + * @return {Boolean} true或false。 + */ + Cluster.prototype.isMarkerInCluster= function(marker){ + if (this._markers.indexOf) { + return this._markers.indexOf(marker) != -1; + } else { + for (var i = 0, m; m = this._markers[i]; i++) { + if (m === marker) { + return true; + } + } + } + return false; + }; + + /** + * 判断一个标记是否在该聚合网格范围中。 + * @param {Marker} marker 要判断的标记。 + * @return {Boolean} true或false。 + */ + Cluster.prototype.isMarkerInClusterBounds = function(marker) { + return this._gridBounds.containsPoint(marker.getPosition()); + }; + + Cluster.prototype.isReal = function(marker) { + return this._isReal; + }; + + /** + * 更新该聚合的网格范围。 + * @return 无返回值。 + */ + Cluster.prototype.updateGridBounds = function() { + var bounds = new BMap.Bounds(this._center, this._center); + this._gridBounds = getExtendedBounds(this._map, bounds, this._markerClusterer.getGridSize()); + }; + + /** + * 更新该聚合的显示样式,也即TextIconOverlay。 + * @return 无返回值。 + */ + Cluster.prototype.updateClusterMarker = function () { + if (this._map.getZoom() > this._markerClusterer.getMaxZoom()) { + this._clusterMarker && this._map.removeOverlay(this._clusterMarker); + for (var i = 0, marker; marker = this._markers[i]; i++) { + this._map.addOverlay(marker); + } + return; + } + + if (this._markers.length < this._minClusterSize) { + this._clusterMarker.hide(); + return; + } + + this._clusterMarker.setPosition(this._center); + + // this._clusterMarker.setText(this._markers.length); + var imageUrl = this._markers[0].getImageUrl(); + this._clusterMarker.setText(this._markers.length, imageUrl); + + var thatMap = this._map; + var thatBounds = this.getBounds(); + this._clusterMarker.addEventListener("click", function(event){ + thatMap.setViewport(thatBounds); + }); + + }; + + /** + * 删除该聚合。 + * @return 无返回值。 + */ + Cluster.prototype.remove = function(){ + for (var i = 0, m; m = this._markers[i]; i++) { + this._markers[i].getMap() && this._map.removeOverlay(this._markers[i]) + }//清除散的标记点 + this._map.removeOverlay(this._clusterMarker); + this._markers.length = 0; + delete this._markers; + } + + /** + * 获取该聚合所包含的所有标记的最小外接矩形的范围。 + * @return {BMap.Bounds} 计算出的范围。 + */ + Cluster.prototype.getBounds = function() { + var bounds = new BMap.Bounds(this._center,this._center); + for (var i = 0, marker; marker = this._markers[i]; i++) { + bounds.extend(marker.getPosition()); + } + return bounds; + }; + + /** + * 获取该聚合的落脚点。 + * @return {BMap.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 new file mode 100644 index 0000000000..725e04efe2 --- /dev/null +++ b/media/js/map/text-icon-overlay.js @@ -0,0 +1,1077 @@ +/** + * @fileoverview 此类表示地图上的一个覆盖物,该覆盖物由文字和图标组成,从Overlay继承。 + * 主入口类是TextIconOverlay, + * 基于Baidu Map API 1.2。 + * + * @author Baidu Map Api Group + * @version 1.2 + */ + + + /** + * @namespace BMap的所有library类均放在BMapLib命名空间下 + */ +var BMapLib = window.BMapLib = BMapLib || {}; + +(function () { + + /** + * 声明baidu包 + */ + var T, + baidu = T = baidu || {version: "1.3.8"}; + + (function (){ + //提出guid,防止在与老版本Tangram混用时 + //在下一行错误的修改window[undefined] + baidu.guid = "$BAIDU$"; + + //Tangram可能被放在闭包中 + //一些页面级别唯一的属性,需要挂载在window[baidu.guid]上 + window[baidu.guid] = window[baidu.guid] || {}; + + /** + * @ignore + * @namespace baidu.dom 操作dom的方法。 + */ + baidu.dom = baidu.dom || {}; + + + /** + * 从文档中获取指定的DOM元素 + * @name baidu.dom.g + * @function + * @grammar baidu.dom.g(id) + * @param {string|HTMLElement} id 元素的id或DOM元素 + * @shortcut g,T.G + * @meta standard + * @see baidu.dom.q + * + * @returns {HTMLElement|null} 获取的元素,查找不到时返回null,如果参数不合法,直接返回参数 + */ + baidu.dom.g = function (id) { + if ('string' == typeof id || id instanceof String) { + return document.getElementById(id); + } else if (id && id.nodeName && (id.nodeType == 1 || id.nodeType == 9)) { + return id; + } + return null; + }; + + // 声明快捷方法 + baidu.g = baidu.G = baidu.dom.g; + + /** + * 获取目标元素所属的document对象 + * @name baidu.dom.getDocument + * @function + * @grammar baidu.dom.getDocument(element) + * @param {HTMLElement|string} element 目标元素或目标元素的id + * @meta standard + * @see baidu.dom.getWindow + * + * @returns {HTMLDocument} 目标元素所属的document对象 + */ + baidu.dom.getDocument = function (element) { + element = baidu.dom.g(element); + return element.nodeType == 9 ? element : element.ownerDocument || element.document; + }; + + /** + * @ignore + * @namespace baidu.lang 对语言层面的封装,包括类型判断、模块扩展、继承基类以及对象自定义事件的支持。 + */ + baidu.lang = baidu.lang || {}; + + /** + * 判断目标参数是否string类型或String对象 + * @name baidu.lang.isString + * @function + * @grammar baidu.lang.isString(source) + * @param {Any} source 目标参数 + * @shortcut isString + * @meta standard + * @see baidu.lang.isObject,baidu.lang.isNumber,baidu.lang.isArray,baidu.lang.isElement,baidu.lang.isBoolean,baidu.lang.isDate + * + * @returns {boolean} 类型判断结果 + */ + baidu.lang.isString = function (source) { + return '[object String]' == Object.prototype.toString.call(source); + }; + + // 声明快捷方法 + baidu.isString = baidu.lang.isString; + + /** + * 从文档中获取指定的DOM元素 + * **内部方法** + * + * @param {string|HTMLElement} id 元素的id或DOM元素 + * @meta standard + * @return {HTMLElement} DOM元素,如果不存在,返回null,如果参数不合法,直接返回参数 + */ + baidu.dom._g = function (id) { + if (baidu.lang.isString(id)) { + return document.getElementById(id); + } + return id; + }; + + // 声明快捷方法 + baidu._g = baidu.dom._g; + + /** + * @ignore + * @namespace baidu.browser 判断浏览器类型和特性的属性。 + */ + baidu.browser = baidu.browser || {}; + + if (/msie (\d+\.\d)/i.test(navigator.userAgent)) { + //IE 8下,以documentMode为准 + //在百度模板中,可能会有$,防止冲突,将$1 写成 \x241 + /** + * 判断是否为ie浏览器 + * @property ie ie版本号 + * @grammar baidu.browser.ie + * @meta standard + * @shortcut ie + * @see baidu.browser.firefox,baidu.browser.safari,baidu.browser.opera,baidu.browser.chrome,baidu.browser.maxthon + */ + baidu.browser.ie = baidu.ie = document.documentMode || + RegExp['\x241']; + } + + /** + * 获取目标元素的computed style值。如果元素的样式值不能被浏览器计算,则会返回空字符串(IE) + * + * @author berg + * @name baidu.dom.getComputedStyle + * @function + * @grammar baidu.dom.getComputedStyle(element, key) + * @param {HTMLElement|string} element 目标元素或目标元素的id + * @param {string} key 要获取的样式名 + * + * @see baidu.dom.getStyle + * + * @returns {string} 目标元素的computed style值 + */ + + baidu.dom.getComputedStyle = function(element, key){ + element = baidu.dom._g(element); + var doc = baidu.dom.getDocument(element), + styles; + if (doc.defaultView && doc.defaultView.getComputedStyle) { + styles = doc.defaultView.getComputedStyle(element, null); + if (styles) { + return styles[key] || styles.getPropertyValue(key); + } + } + return ''; + }; + + /** + * 提供给setStyle与getStyle使用 + */ + baidu.dom._styleFixer = baidu.dom._styleFixer || {}; + + /** + * 提供给setStyle与getStyle使用 + */ + baidu.dom._styleFilter = baidu.dom._styleFilter || []; + + /** + * 为获取和设置样式的过滤器 + * @private + * @meta standard + */ + baidu.dom._styleFilter.filter = function (key, value, method) { + for (var i = 0, filters = baidu.dom._styleFilter, filter; filter = filters[i]; i++) { + if (filter = filter[method]) { + value = filter(key, value); + } + } + return value; + }; + + /** + * @ignore + * @namespace baidu.string 操作字符串的方法。 + */ + baidu.string = baidu.string || {}; + + /** + * 将目标字符串进行驼峰化处理 + * @name baidu.string.toCamelCase + * @function + * @grammar baidu.string.toCamelCase(source) + * @param {string} source 目标字符串 + * @remark + * 支持单词以“-_”分隔 + * @meta standard + * + * @returns {string} 驼峰化处理后的字符串 + */ + baidu.string.toCamelCase = function (source) { + //提前判断,提高getStyle等的效率 thanks xianwei + if (source.indexOf('-') < 0 && source.indexOf('_') < 0) { + return source; + } + return source.replace(/[-_][^-_]/g, function (match) { + return match.charAt(1).toUpperCase(); + }); + }; + + /** + * 获取目标元素的样式值 + * @name baidu.dom.getStyle + * @function + * @grammar baidu.dom.getStyle(element, key) + * @param {HTMLElement|string} element 目标元素或目标元素的id + * @param {string} key 要获取的样式名 + * @remark + * + * 为了精简代码,本模块默认不对任何浏览器返回值进行归一化处理(如使用getStyle时,不同浏览器下可能返回rgb颜色或hex颜色),也不会修复浏览器的bug和差异性(如设置IE的float属性叫styleFloat,firefox则是cssFloat)。
+ * baidu.dom._styleFixer和baidu.dom._styleFilter可以为本模块提供支持。
+ * 其中_styleFilter能对颜色和px进行归一化处理,_styleFixer能对display,float,opacity,textOverflow的浏览器兼容性bug进行处理。 + * @shortcut getStyle + * @meta standard + * @see baidu.dom.setStyle,baidu.dom.setStyles, baidu.dom.getComputedStyle + * + * @returns {string} 目标元素的样式值 + */ + baidu.dom.getStyle = function (element, key) { + var dom = baidu.dom; + + element = dom.g(element); + key = baidu.string.toCamelCase(key); + //computed style, then cascaded style, then explicitly set style. + var value = element.style[key] || + (element.currentStyle ? element.currentStyle[key] : "") || + dom.getComputedStyle(element, key); + + // 在取不到值的时候,用fixer进行修正 + if (!value) { + var fixer = dom._styleFixer[key]; + if(fixer){ + value = fixer.get ? fixer.get(element) : baidu.dom.getStyle(element, fixer); + } + } + + /* 检查结果过滤器 */ + if (fixer = dom._styleFilter) { + value = fixer.filter(key, value, 'get'); + } + + return value; + }; + + // 声明快捷方法 + baidu.getStyle = baidu.dom.getStyle; + + + if (/opera\/(\d+\.\d)/i.test(navigator.userAgent)) { + /** + * 判断是否为opera浏览器 + * @property opera opera版本号 + * @grammar baidu.browser.opera + * @meta standard + * @see baidu.browser.ie,baidu.browser.firefox,baidu.browser.safari,baidu.browser.chrome + */ + baidu.browser.opera = + RegExp['\x241']; + } + + /** + * 判断是否为webkit内核 + * @property isWebkit + * @grammar baidu.browser.isWebkit + * @meta standard + * @see baidu.browser.isGecko + */ + baidu.browser.isWebkit = /webkit/i.test(navigator.userAgent); + + /** + * 判断是否为gecko内核 + * @property isGecko + * @grammar baidu.browser.isGecko + * @meta standard + * @see baidu.browser.isWebkit + */ + baidu.browser.isGecko = /gecko/i.test(navigator.userAgent) && !/like gecko/i.test(navigator.userAgent); + + /** + * 判断是否严格标准的渲染模式 + * @property isStrict + * @grammar baidu.browser.isStrict + * @meta standard + */ + baidu.browser.isStrict = document.compatMode == "CSS1Compat"; + + /** + * 获取目标元素相对于整个文档左上角的位置 + * @name baidu.dom.getPosition + * @function + * @grammar baidu.dom.getPosition(element) + * @param {HTMLElement|string} element 目标元素或目标元素的id + * @meta standard + * + * @returns {Object} 目标元素的位置,键值为top和left的Object。 + */ + baidu.dom.getPosition = function (element) { + element = baidu.dom.g(element); + var doc = baidu.dom.getDocument(element), + browser = baidu.browser, + getStyle = baidu.dom.getStyle, + // Gecko 1.9版本以下用getBoxObjectFor计算位置 + // 但是某些情况下是有bug的 + // 对于这些有bug的情况 + // 使用递归查找的方式 + BUGGY_GECKO_BOX_OBJECT = browser.isGecko > 0 && + doc.getBoxObjectFor && + getStyle(element, 'position') == 'absolute' && + (element.style.top === '' || element.style.left === ''), + pos = {"left":0,"top":0}, + viewport = (browser.ie && !browser.isStrict) ? doc.body : doc.documentElement, + parent, + box; + + if(element == viewport){ + return pos; + } + + if(element.getBoundingClientRect){ // IE and Gecko 1.9+ + + //当HTML或者BODY有border width时, 原生的getBoundingClientRect返回值是不符合预期的 + //考虑到通常情况下 HTML和BODY的border只会设成0px,所以忽略该问题. + box = element.getBoundingClientRect(); + + pos.left = Math.floor(box.left) + Math.max(doc.documentElement.scrollLeft, doc.body.scrollLeft); + pos.top = Math.floor(box.top) + Math.max(doc.documentElement.scrollTop, doc.body.scrollTop); + + // IE会给HTML元素添加一个border,默认是medium(2px) + // 但是在IE 6 7 的怪异模式下,可以被html { border: 0; } 这条css规则覆盖 + // 在IE7的标准模式下,border永远是2px,这个值通过clientLeft 和 clientTop取得 + // 但是。。。在IE 6 7的怪异模式,如果用户使用css覆盖了默认的medium + // clientTop和clientLeft不会更新 + pos.left -= doc.documentElement.clientLeft; + pos.top -= doc.documentElement.clientTop; + + var htmlDom = doc.body, + // 在这里,不使用element.style.borderLeftWidth,只有computedStyle是可信的 + htmlBorderLeftWidth = parseInt(getStyle(htmlDom, 'borderLeftWidth')), + htmlBorderTopWidth = parseInt(getStyle(htmlDom, 'borderTopWidth')); + if(browser.ie && !browser.isStrict){ + pos.left -= isNaN(htmlBorderLeftWidth) ? 2 : htmlBorderLeftWidth; + pos.top -= isNaN(htmlBorderTopWidth) ? 2 : htmlBorderTopWidth; + } + } else { + // safari/opera/firefox + parent = element; + + do { + pos.left += parent.offsetLeft; + pos.top += parent.offsetTop; + + // safari里面,如果遍历到了一个fixed的元素,后面的offset都不准了 + if (browser.isWebkit > 0 && getStyle(parent, 'position') == 'fixed') { + pos.left += doc.body.scrollLeft; + pos.top += doc.body.scrollTop; + break; + } + + parent = parent.offsetParent; + } while (parent && parent != element); + + // 对body offsetTop的修正 + if(browser.opera > 0 || (browser.isWebkit > 0 && getStyle(element, 'position') == 'absolute')){ + pos.top -= doc.body.offsetTop; + } + + // 计算除了body的scroll + parent = element.offsetParent; + while (parent && parent != doc.body) { + pos.left -= parent.scrollLeft; + // see https://bugs.opera.com/show_bug.cgi?id=249965 + if (!browser.opera || parent.tagName != 'TR') { + pos.top -= parent.scrollTop; + } + parent = parent.offsetParent; + } + } + + return pos; + }; + + /** + * @ignore + * @namespace baidu.event 屏蔽浏览器差异性的事件封装。 + * @property target 事件的触发元素 + * @property pageX 鼠标事件的鼠标x坐标 + * @property pageY 鼠标事件的鼠标y坐标 + * @property keyCode 键盘事件的键值 + */ + baidu.event = baidu.event || {}; + + /** + * 事件监听器的存储表 + * @private + * @meta standard + */ + baidu.event._listeners = baidu.event._listeners || []; + + /** + * 为目标元素添加事件监听器 + * @name baidu.event.on + * @function + * @grammar baidu.event.on(element, type, listener) + * @param {HTMLElement|string|window} element 目标元素或目标元素id + * @param {string} type 事件类型 + * @param {Function} listener 需要添加的监听器 + * @remark + * + 1. 不支持跨浏览器的鼠标滚轮事件监听器添加
+ 2. 改方法不为监听器灌入事件对象,以防止跨iframe事件挂载的事件对象获取失败 + + * @shortcut on + * @meta standard + * @see baidu.event.un + * + * @returns {HTMLElement|window} 目标元素 + */ + baidu.event.on = function (element, type, listener) { + type = type.replace(/^on/i, ''); + element = baidu.dom._g(element); + + var realListener = function (ev) { + // 1. 这里不支持EventArgument, 原因是跨frame的事件挂载 + // 2. element是为了修正this + listener.call(element, ev); + }, + lis = baidu.event._listeners, + filter = baidu.event._eventFilter, + afterFilter, + realType = type; + type = type.toLowerCase(); + // filter过滤 + if(filter && filter[type]){ + afterFilter = filter[type](element, type, realListener); + realType = afterFilter.type; + realListener = afterFilter.listener; + } + + // 事件监听器挂载 + if (element.addEventListener) { + element.addEventListener(realType, realListener, false); + } else if (element.attachEvent) { + element.attachEvent('on' + realType, realListener); + } + + // 将监听器存储到数组中 + lis[lis.length] = [element, type, listener, realListener, realType]; + return element; + }; + + // 声明快捷方法 + baidu.on = baidu.event.on; + + /** + * 返回一个当前页面的唯一标识字符串。 + * @name baidu.lang.guid + * @function + * @grammar baidu.lang.guid() + * @version 1.1.1 + * @meta standard + * + * @returns {String} 当前页面的唯一标识字符串 + */ + + (function(){ + //不直接使用window,可以提高3倍左右性能 + var guid = window[baidu.guid]; + + baidu.lang.guid = function() { + return "TANGRAM__" + (guid._counter ++).toString(36); + }; + + guid._counter = guid._counter || 1; + })(); + + /** + * 所有类的实例的容器 + * key为每个实例的guid + * @meta standard + */ + + window[baidu.guid]._instances = window[baidu.guid]._instances || {}; + + /** + * 判断目标参数是否为function或Function实例 + * @name baidu.lang.isFunction + * @function + * @grammar baidu.lang.isFunction(source) + * @param {Any} source 目标参数 + * @version 1.2 + * @see baidu.lang.isString,baidu.lang.isObject,baidu.lang.isNumber,baidu.lang.isArray,baidu.lang.isElement,baidu.lang.isBoolean,baidu.lang.isDate + * @meta standard + * @returns {boolean} 类型判断结果 + */ + baidu.lang.isFunction = function (source) { + // chrome下,'function' == typeof /a/ 为true. + return '[object Function]' == Object.prototype.toString.call(source); + }; + + /** + * + * @ignore + * @class Tangram继承机制提供的一个基类,用户可以通过继承baidu.lang.Class来获取它的属性及方法。 + * @name baidu.lang.Class + * @grammar baidu.lang.Class(guid) + * @param {string} guid 对象的唯一标识 + * @meta standard + * @remark baidu.lang.Class和它的子类的实例均包含一个全局唯一的标识guid。guid是在构造函数中生成的,因此,继承自baidu.lang.Class的类应该直接或者间接调用它的构造函数。
baidu.lang.Class的构造函数中产生guid的方式可以保证guid的唯一性,及每个实例都有一个全局唯一的guid。 + * @meta standard + * @see baidu.lang.inherits,baidu.lang.Event + */ + baidu.lang.Class = function(guid) { + this.guid = guid || baidu.lang.guid(); + window[baidu.guid]._instances[this.guid] = this; + }; + window[baidu.guid]._instances = window[baidu.guid]._instances || {}; + + /** + * 释放对象所持有的资源,主要是自定义事件。 + * @name dispose + * @grammar obj.dispose() + */ + baidu.lang.Class.prototype.dispose = function(){ + delete window[baidu.guid]._instances[this.guid]; + + for(var property in this){ + if (!baidu.lang.isFunction(this[property])) { + delete this[property]; + } + } + this.disposed = true; + }; + + /** + * 重载了默认的toString方法,使得返回信息更加准确一些。 + * @return {string} 对象的String表示形式 + */ + baidu.lang.Class.prototype.toString = function(){ + return "[object " + (this._className || "Object" ) + "]"; + }; + + /** + * @ignore + * @class 自定义的事件对象。 + * @name baidu.lang.Event + * @grammar baidu.lang.Event(type[, target]) + * @param {string} type 事件类型名称。为了方便区分事件和一个普通的方法,事件类型名称必须以"on"(小写)开头。 + * @param {Object} [target]触发事件的对象 + * @meta standard + * @remark 引入该模块,会自动为Class引入3个事件扩展方法:addEventListener、removeEventListener和dispatchEvent。 + * @meta standard + * @see baidu.lang.Class + */ + baidu.lang.Event = function (type, target) { + this.type = type; + this.returnValue = true; + this.target = target || null; + this.currentTarget = null; + }; + + /** + * 注册对象的事件监听器。引入baidu.lang.Event后,Class的子类实例才会获得该方法。 + * @grammar obj.addEventListener(type, handler[, key]) + * @param {string} type 自定义事件的名称 + * @param {Function} handler 自定义事件被触发时应该调用的回调函数 + * @param {string} [key] 为事件监听函数指定的名称,可在移除时使用。如果不提供,方法会默认为它生成一个全局唯一的key。 + * @remark 事件类型区分大小写。如果自定义事件名称不是以小写"on"开头,该方法会给它加上"on"再进行判断,即"click"和"onclick"会被认为是同一种事件。 + */ + baidu.lang.Class.prototype.addEventListener = function (type, handler, key) { + if (!baidu.lang.isFunction(handler)) { + return; + } + + !this.__listeners && (this.__listeners = {}); + + var t = this.__listeners, id; + if (typeof key == "string" && key) { + if (/[^\w\-]/.test(key)) { + throw("nonstandard key:" + key); + } else { + handler.hashCode = key; + id = key; + } + } + type.indexOf("on") != 0 && (type = "on" + type); + + typeof t[type] != "object" && (t[type] = {}); + id = id || baidu.lang.guid(); + handler.hashCode = id; + t[type][id] = handler; + }; + + /** + * 移除对象的事件监听器。引入baidu.lang.Event后,Class的子类实例才会获得该方法。 + * @grammar obj.removeEventListener(type, handler) + * @param {string} type 事件类型 + * @param {Function|string} handler 要移除的事件监听函数或者监听函数的key + * @remark 如果第二个参数handler没有被绑定到对应的自定义事件中,什么也不做。 + */ + baidu.lang.Class.prototype.removeEventListener = function (type, handler) { + if (typeof handler != "undefined") { + if ( (baidu.lang.isFunction(handler) && ! (handler = handler.hashCode)) + || (! baidu.lang.isString(handler)) + ){ + return; + } + } + + !this.__listeners && (this.__listeners = {}); + + type.indexOf("on") != 0 && (type = "on" + type); + + var t = this.__listeners; + if (!t[type]) { + return; + } + if (typeof handler != "undefined") { + t[type][handler] && delete t[type][handler]; + } else { + for(var guid in t[type]){ + delete t[type][guid]; + } + } + }; + + /** + * 派发自定义事件,使得绑定到自定义事件上面的函数都会被执行。引入baidu.lang.Event后,Class的子类实例才会获得该方法。 + * @grammar obj.dispatchEvent(event, options) + * @param {baidu.lang.Event|String} event Event对象,或事件名称(1.1.1起支持) + * @param {Object} options 扩展参数,所含属性键值会扩展到Event对象上(1.2起支持) + * @remark 处理会调用通过addEventListenr绑定的自定义事件回调函数之外,还会调用直接绑定到对象上面的自定义事件。例如:
+ myobj.onMyEvent = function(){}
+ myobj.addEventListener("onMyEvent", function(){}); + */ + baidu.lang.Class.prototype.dispatchEvent = function (event, options) { + if (baidu.lang.isString(event)) { + event = new baidu.lang.Event(event); + } + !this.__listeners && (this.__listeners = {}); + + // 20100603 添加本方法的第二个参数,将 options extend到event中去传递 + options = options || {}; + for (var i in options) { + event[i] = options[i]; + } + + var i, t = this.__listeners, p = event.type; + event.target = event.target || this; + event.currentTarget = this; + + p.indexOf("on") != 0 && (p = "on" + p); + + baidu.lang.isFunction(this[p]) && this[p].apply(this, arguments); + + if (typeof t[p] == "object") { + for (i in t[p]) { + t[p][i].apply(this, arguments); + } + } + return event.returnValue; + }; + + + baidu.lang.inherits = function (subClass, superClass, className) { + var key, proto, + selfProps = subClass.prototype, + clazz = new Function(); + + clazz.prototype = superClass.prototype; + proto = subClass.prototype = new clazz(); + for (key in selfProps) { + proto[key] = selfProps[key]; + } + subClass.prototype.constructor = subClass; + subClass.superClass = superClass.prototype; + + // 类名标识,兼容Class的toString,基本没用 + if ("string" == typeof className) { + proto._className = className; + } + }; + // 声明快捷方法 + baidu.inherits = baidu.lang.inherits; + })(); + + + /** + + * 图片的路径 + + * @private + * @type {String} + + */ + var _IMAGE_PATH = 'http://api.map.baidu.com/library/TextIconOverlay/1.2/src/images/m'; + + /** + + * 图片的后缀名 + + * @private + * @type {String} + + */ + var _IMAGE_EXTENSION = 'png'; + + /** + *@exports TextIconOverlay as BMapLib.TextIconOverlay + */ + var TextIconOverlay = + /** + * TextIconOverlay + * @class 此类表示地图上的一个覆盖物,该覆盖物由文字和图标组成,从Overlay继承。文字通常是数字(0-9)或字母(A-Z ),而文字与图标之间有一定的映射关系。 + *该覆盖物适用于以下类似的场景:需要在地图上添加一系列覆盖物,这些覆盖物之间用不同的图标和文字来区分,文字可能表示了该覆盖物的某一属性值,根据该文字和一定的映射关系,自动匹配相应颜色和大小的图标。 + * + *@constructor + *@param {Point} position 表示一个经纬度坐标位置。 + *@param {String} text 表示该覆盖物显示的文字信息。 + *@param {Json Object} options 可选参数,可选项包括:
+ *"styles":{Array} 一组图标风格。单个图表风格包括以下几个属性:
+ * url {String} 图片的url地址。(必选)
+ * size {Size} 图片的大小。(必选)
+ * anchor {Size} 图标定位在地图上的位置相对于图标左上角的偏移值,默认偏移值为图标的中心位置。(可选)
+ * offset {Size} 图片相对于可视区域的偏移值,此功能的作用等同于CSS中的background-position属性。(可选)
+ * textSize {Number} 文字的大小。(可选,默认10)
+ * textColor {String} 文字的颜色。(可选,默认black)
+ */ + BMapLib.TextIconOverlay = function(position, text, options){ + this._position = position; + this._text = text; + this._options = options || {}; + this._styles = this._options['styles'] || []; + (!this._styles.length) && this._setupDefaultStyles(); + }; + + T.lang.inherits(TextIconOverlay, BMap.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) + }); + }//for循环的简洁写法 + }; + + /** + *继承Overlay的intialize方法,自定义覆盖物时必须。 + *@param {Map} map BMap.Map的实例化对象。 + *@return {HTMLElement} 返回覆盖物对应的HTML元素。 + */ + TextIconOverlay.prototype.initialize = function(map){ + this._map = map; + + this._domElement = document.createElement('div'); + // this._updateCss(); + // this._updateText(); + this._updatePosition(); + + this._bind(); + + this._map.getPanes().markerMouseTarget.appendChild(this._domElement); + return this._domElement; + }; + + /** + *继承Overlay的draw方法,自定义覆盖物时必须。 + *@return 无返回值。 + */ + TextIconOverlay.prototype.draw = function(){ + this._map && this._updatePosition(); + }; + + /** + *获取该覆盖物上的文字。 + *@return {String} 该覆盖物上的文字。 + */ + TextIconOverlay.prototype.getText = function(){ + return this._text; + }; + + /** + *设置该覆盖物上的文字。 + *@param {String} text 要设置的文字,通常是字母A-Z或数字0-9。 + *@return 无返回值。 + */ + TextIconOverlay.prototype.setText = function(text, imageUrl){ + if(text && (!this._text || (this._text.toString() != text.toString()))){ + this._text = text; + // this._updateText(); + this._updateCss(imageUrl); + this._updatePosition(); + } + }; + + /** + *获取该覆盖物的位置。 + *@return {Point} 该覆盖物的经纬度坐标。 + */ + TextIconOverlay.prototype.getPosition = function () { + return this._position; + }; + + /** + *设置该覆盖物的位置。 + *@param {Point} position 要设置的经纬度坐标。 + *@return 无返回值。 + */ + TextIconOverlay.prototype.setPosition = function (position) { + if(position && (!this._position || !this._position.equals(position))){ + this._position = position; + this._updatePosition(); + } + }; + + /** + *由文字信息获取风格数组的对应索引值。 + *内部默认的对应函数为文字转换为数字除以10的结果,比如文字8返回索引0,文字25返回索引2. + *如果需要自定义映射关系,请覆盖该函数。 + *@param {String} text 文字。 + *@param {Array} styles 一组图标风格。 + *@return {Number} 对应的索引值。 + */ + TextIconOverlay.prototype.getStyleByText = function(text, styles){ + var count = parseInt(text); + var index = parseInt(count / 10); + index = Math.max(0, index); + index = Math.min(index, styles.length - 1); + return styles[index]; + } + + /** + *更新相应的CSS。 + *@return 无返回值。 + */ + TextIconOverlay.prototype._updateCss = function(imageUrl){ + var style = this.getStyleByText(this._text, this._styles); + var newStyle = { + url: imageUrl, + size: {width: 72, height: 72} + } + if (imageUrl) { + style = Object.assign(style, {url: imageUrl, size: {width: 72, height: 72}}) + } + + const customImageNumber = `${this._text}`; + this._domElement.style.cssText = this.buildImageCssText(newStyle); + const imageElement = `` + const htmlString = ` +
+ ${this._text > 1 ? customImageNumber : ''} + ${imageUrl ? imageElement : '
'} + +
+ ` + const labelDocument = new DOMParser().parseFromString(htmlString, 'text/html'); + const label = labelDocument.body.firstElementChild; + this._domElement.append(label); + }; + + TextIconOverlay.prototype.buildImageCssText = function(style) { + //根据style来确定一些默认值 + var size = style['size']; + var anchor = style['anchor']; + var textColor = style['textColor'] || 'black'; + var textSize = style['textSize'] || 10; + + var csstext = []; + + 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:' + + textSize + 'px; font-family:Arial,sans-serif; font-weight:bold'); + return csstext.join(''); + }; + + /** + *更新覆盖物的显示文字。 + *@return 无返回值。 + */ + TextIconOverlay.prototype._updateText = function(){ + if (this._domElement) { + this._domElement.innerHTML = this._text; + } + }; + + /** + *调整覆盖物在地图上的位置更新覆盖物的显示文字。 + *@return 无返回值。 + */ + TextIconOverlay.prototype._updatePosition = function(){ + if (this._domElement && this._position) { + var style = this._domElement.style; + var pixelPosition= this._map.pointToOverlayPixel(this._position); + pixelPosition.x -= Math.ceil(parseInt(style.width) / 2); + pixelPosition.y -= Math.ceil(parseInt(style.height) + 8); + style.left = pixelPosition.x + "px"; + style.top = pixelPosition.y + "px"; + } + }; + + /** + * 为该覆盖物的HTML元素构建CSS + * @param {IconStyle} 一个图标的风格。 + * @return {String} 构建完成的CSSTEXT。 + */ + TextIconOverlay.prototype._buildCssText = function(style) { + //根据style来确定一些默认值 + var url = style['url']; + var size = style['size']; + var anchor = style['anchor']; + var offset = style['offset']; + var textColor = style['textColor'] || 'black'; + var textSize = style['textSize'] || 10; + + var csstext = []; + if (T.browser["ie"] < 7) { + csstext.push('filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(' + + 'sizingMethod=scale,src="' + url + '");'); + } else { + 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 + ';'); + } + + if (size instanceof BMap.Size){ + if (anchor instanceof BMap.Size) { + if (anchor.height > 0 && anchor.height < size.height) { + 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;'); + } + } else { + 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:' + + textSize + 'px; font-family:Arial,sans-serif; font-weight:bold'); + return csstext.join(''); + }; + + + /** + + * 当鼠标点击该覆盖物时会触发该事件 + + * @name TextIconOverlay#click + + * @event + + * @param {Event Object} e 回调函数会返回event参数,包括以下返回值: + + *
"type : {String} 事件类型 + + *
"target:{BMapLib.TextIconOverlay} 事件目标 + + * + + */ + + /** + + * 当鼠标进入该覆盖物区域时会触发该事件 + + * @name TextIconOverlay#mouseover + + * @event + * @param {Event Object} e 回调函数会返回event参数,包括以下返回值: + + *
"type : {String} 事件类型 + + *
"target:{BMapLib.TextIconOverlay} 事件目标 + + *
"point : {BMap.Point} 最新添加上的节点BMap.Point对象 + + *
"pixel:{BMap.pixel} 最新添加上的节点BMap.Pixel对象 + + * + + * @example 参考示例:
+ + * myTextIconOverlay.addEventListener("mouseover", function(e) { alert(e.point); }); + + */ + + /** + + * 当鼠标离开该覆盖物区域时会触发该事件 + + * @name TextIconOverlay#mouseout + + * @event + + * @param {Event Object} e 回调函数会返回event参数,包括以下返回值: + + *
"type : {String} 事件类型 + + *
"target:{BMapLib.TextIconOverlay} 事件目标 + + *
"point : {BMap.Point} 最新添加上的节点BMap.Point对象 + + *
"pixel:{BMap.pixel} 最新添加上的节点BMap.Pixel对象 + + * + + * @example 参考示例:
+ + * myTextIconOverlay.addEventListener("mouseout", function(e) { alert(e.point); }); + + */ + + + /** + * 为该覆盖物绑定一系列事件 + * 当前支持click mouseover mouseout + * @return 无返回值。 + */ + TextIconOverlay.prototype._bind = function(){ + if (!this._domElement){ + return; + } + + var me = this; + var map = this._map; + + var BaseEvent = T.lang.Event; + function eventExtend(e, be){ + var elem = e.srcElement || e.target; + var x = e.clientX || e.pageX; + 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.point = map.pixelToPoint(be.pixel); + } + return be; + }//给事件参数增加pixel和point两个值 + + T.event.on(this._domElement,"mouseover", function(e){ + me.dispatchEvent(eventExtend(e, new BaseEvent("onmouseover"))); + }); + T.event.on(this._domElement,"mouseout", function(e){ + me.dispatchEvent(eventExtend(e, new BaseEvent("onmouseout"))); + }); + T.event.on(this._domElement,"click", function(e){ + me.dispatchEvent(eventExtend(e, new BaseEvent("onclick"))); + }); + }; + +})(); \ No newline at end of file