From 3b384be610ceb45ef1551fbebca3106eebe06ea2 Mon Sep 17 00:00:00 2001 From: Aries <urchinzhou@gmail.com> Date: Mon, 18 Nov 2024 17:36:49 +0800 Subject: [PATCH] Feature/add map view (#7034) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add map * use custom baidu js * use custom baidu plugin * optimize user location * optimize code * show tips when request user location failed * optimize code * feat: optimize code * feat: optimize code * feat: optimize code * feat: optimize code --------- Co-authored-by: zhouwenxuan <aries@Mac.local> Co-authored-by: 杨国璇 <ygx@Hello-word.local> --- frontend/src/assets/icons/map.svg | 20 + .../popover/view-popover/add-view/index.js | 6 + .../metadata/components/view-toolbar/index.js | 9 + .../view-toolbar/map-view-toolbar/index.js | 52 + frontend/src/metadata/constants/view.js | 16 + .../src/metadata/metadata-tree-view/index.js | 3 + frontend/src/metadata/metadata-view/view.js | 4 + frontend/src/metadata/store/data-processor.js | 7 +- .../src/metadata/store/operations/apply.js | 6 +- .../metadata/utils/cell/cell-format-utils.js | 68 +- frontend/src/metadata/utils/cell/core.js | 8 + frontend/src/metadata/utils/cell/index.js | 47 +- .../views/map/custom-avatar-overlay.js | 46 + .../views/map/custom-image-overlay.js | 69 ++ .../metadata/views/map/geolocation-control.js | 50 + frontend/src/metadata/views/map/index.css | 85 ++ frontend/src/metadata/views/map/index.js | 171 +++ frontend/src/utils/map-utils.js | 39 + media/favicons/map.png | Bin 0 -> 1236 bytes media/img/current-location.svg | 12 + media/img/marker.png | Bin 0 -> 1797 bytes media/js/map/marker-clusterer.js | 617 ++++++++++ media/js/map/text-icon-overlay.js | 1077 +++++++++++++++++ 23 files changed, 2331 insertions(+), 81 deletions(-) create mode 100644 frontend/src/assets/icons/map.svg create mode 100644 frontend/src/metadata/components/view-toolbar/map-view-toolbar/index.js create mode 100644 frontend/src/metadata/views/map/custom-avatar-overlay.js create mode 100644 frontend/src/metadata/views/map/custom-image-overlay.js create mode 100644 frontend/src/metadata/views/map/geolocation-control.js create mode 100644 frontend/src/metadata/views/map/index.css create mode 100644 frontend/src/metadata/views/map/index.js create mode 100644 media/favicons/map.png create mode 100644 media/img/current-location.svg create mode 100644 media/img/marker.png create mode 100644 media/js/map/marker-clusterer.js create mode 100644 media/js/map/text-icon-overlay.js 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#999999;} +</style> +<title>map</title> +<g id="map"> + <g id="形状结合" transform="translate(1.000000, 1.000000)"> + <path class="st0" d="M29.5,6.8C29.8,7,30,7.4,30,7.8v17.6c0,0.5-0.3,1-0.8,1.2l-8.8,3.3c-0.3,0.1-0.6,0.1-0.8,0L10,26.7l-8.3,3.2 + c-0.4,0.1-0.8,0.1-1.2-0.1c-0.3-0.2-0.5-0.6-0.5-1V11.2c0-0.5,0.3-1,0.8-1.2l4-1.5c-0.2,1.1-0.2,2.3,0,3.4l-2,0.9v13.4l5.6-2.4 + l0-5.4l2.8,3.5v2.7l7.5,2.5v-5.2l2.8-3.5l0,8l5.6-2.2V10.4l-1.9,0.8c0.1-1.1,0.1-2.2-0.2-3.3l3.2-1.2C28.7,6.5,29.1,6.6,29.5,6.8z + M8.3,2.7c3.7-3.6,9.6-3.6,13.3,0c3.3,3.1,3.7,8,1.1,11.7l-6.2,7.7c-0.1,0.1-0.2,0.2-0.3,0.3c-0.9,0.7-2.1,0.6-2.8-0.3l-6.1-7.6 + C4.7,10.8,5.2,5.9,8.3,2.7z M19.4,4.8c-2.4-2.2-6.4-2.2-8.8,0c-2.2,2-2.4,5.1-0.7,7.4l4.1,5.5c0.5,0.7,1.5,0.7,2,0l4.1-5.5 + C21.8,9.9,21.6,6.8,19.4,4.8z M15,5c2.2,0,4,1.8,4,4c0,1.4-0.8,2.7-2,3.5s-2.8,0.7-4,0s-2-2-2-3.5C11,6.8,12.8,5,15,5z M15,8 + c-0.6,0-1,0.4-1,1s0.4,1,1,1s1-0.4,1-1S15.6,8,15,8z"/> + </g> +</g> +</svg> 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 && ( + <MapViewToolBar + readOnly={readOnly} + view={view} + collaborators={collaborators} + modifyFilters={modifyFilters} + /> + )} </div> ); }; 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 ( + <> + <div className="sf-metadata-tool-left-operations"> + <FilterSetter + isNeedSubmit={true} + wrapperClass="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-filter" + filtersClassName="sf-metadata-filters" + target="sf-metadata-filter-popover" + readOnly={readOnly} + filterConjunction={view.filter_conjunction} + basicFilters={view.basic_filters} + filters={view.filters} + columns={filterColumns} + modifyFilters={modifyFilters} + collaborators={collaborators} + viewType={viewType} + /> + </div> + <div className="sf-metadata-tool-right-operations"></div> + </> + ); +}; + +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 <Kanban />; } + case VIEW_TYPE.MAP: { + return <Map />; + } 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 = `<img src=${this._imageUrl} width="72" height="72" />`; + const htmlString = + ` + <div class="custom-image-container"> + ${this._imageUrl ? imageElement : '<div class="empty-custom-image-wrapper"></div>'} + <i class='sf3-font image-overlay-arrow'></i> + </div> + `; + 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 ( + <div className="w-100 h-100 sf-metadata-view-map"> + {isLoading ? ( + <CenteredLoading /> + ) : ( + <div className="sf-metadata-map-container" ref={mapRef} id="sf-metadata-map-container"></div> + )} + </div> + ); +}; + +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 0000000000000000000000000000000000000000..a797129ee44f05d38336bd20ff09d86c654412f1 GIT binary patch literal 1236 zcmV;_1S|WAP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800004XF*Lt006O% z3;baP0000uWmrjOO-%qQ0000800D<-00aO40096102%-Q00002paK8{000010000W zpaTE|000010000W00000k4`|z000C>Nkl<ZSP8|KeQ4HI6vsc$gIbo&nJu+^iIv(W zWz*)Oq8cG(X-VPCSt(W|6cwcVFDmMfT8l~vLUM^>Mq?(GQc*E62sK<&*LnwS&6efV z*_Q6D^L@^KY(CvRdxAC({O<4GbI-Zw-1BztjUg!M8qX<8iE$HMj7#7Ks)=#PWK=PX z5=vwBv4rq8Y7BY)K*<cQ8{-H=nLZ@3a|T(sG}4C6>hXlxgDMUJ(p(-NaG$5zhsE2F zRP&*^F(csd8{zg`xOB8E2|pZ$_xHo<9q?yMh^ud0j3>IZMp|%B9=y2_&OIwkeykPl z+Xx@jhUY`rv=x{=1wLL5u^6<Rg7TfPr{0&GF&>r`z&XR9qZ0~WhaEqM#QhtWmI9-O z!@d%@I1_dqfCcN|w`Rzn=nL*X2xHHKils1X68zEx(_exnvq*ms?~g$9GQET!wFU2f z%l%%x0={_6zdcVvr#Vt{qM0xX78*|kBg`_xheo5*|7j^Od#W>S*$4H<pzIFF9Sdz8 z{!O#m`SP7UCmO*mm&4T)VB@`T_<7j7((%b{pIMM$fwpJ{oNR~e(a!eW@BS|^GYcMm z*Z<{<Ai_m7msnk0k`Ki*o#wJE|0lOW1xn|^qt~0+%yea++YU}R+e9<1pr!#PnSf_* z^>KvLKcW%O@xP(25pu@#&?WbRE?80M!j+&6$1TtQaJFKzNaoq_)Hciazg(CEsxO@8 z7I*LXTNC`kXK>wX$s$i52Q8pf6XccoZgVb#<=4VU%Z62V!ToQ;m8CFun$vw)>x!-~ zh741p(^7Gj6{Y5rpbSa{K?Nd)14q19XTGoa8jA9)(B#ARpJ44ySo38>2@e$boG=@{ zf*-rB)<Y5Q1ziv#Rlup_3RasHuIP1ZP+vGf?IDYwO7ExBrmetH3#j>P&8+rUa8@h@ z<+ncdg;NcfUk)d%Ql<-OE1(5i+syw~>lK_+EiP(S`lr79zIX$~yW?j-6j)OP)xW}X zpZN0G7g&w3Pw@0dpc?ST0w1T_{wE7$jez%-L8;Y-&3k)9RMx<&+kGyxpQ!KQ@?Od; zNG8@e&rUv-*rIhX&(g5N+_Ci=xFOpD^CGiQeTb`XTv`gqt*Y(gGJGM8C3wsJz88*W zbY`T2lnSV*ESUw5SXrK&)w9fIbAt{Y)rL>Mw*$!nus4h}KAj^EUIR1DS^^zhAqd_F zx?XY8R!b+PR|JjCejg~DZ0To*m7uA`d~eFAXgxc}FDtT8Z$8zA7cFj7ppqe_1++-J zX}+z_Z$R>OyX{tI`y6wLD&yQ-zYRqT>sBPt_3WK$=i7DAaiKP7fvpeuL9>20=xel- ztuK%=x8;~JSHaCw?A&i~p_P^;svG@5P=WrK{?B<mOw2=P2u!vfCRJxhye^kj@x&`t y55>U-$+Q=8Tu)@ZQceD*L2-w>d{aWE$^Hi>-a_LRC1i;J0000<MNUMnLSTYM^E=A` literal 0 HcmV?d00001 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#212529;} +</style> +<path class="st0" d="M16,1c8.3,0,15,6.7,15,15s-6.7,15-15,15S1,24.3,1,16S7.7,1,16,1z M17.5,4.7v3c0,0.8-0.7,1.5-1.5,1.5 + s-1.5-0.7-1.5-1.5v-3c-5,0.7-9,4.7-9.7,9.7h3c0.8,0,1.5,0.7,1.5,1.5c0,0.8-0.7,1.5-1.5,1.5h-3c0.7,5.1,4.7,9.1,9.7,9.8v-3.1 + c0-0.8,0.7-1.5,1.5-1.5c0.8,0,1.5,0.7,1.5,1.5v3.1c5.1-0.7,9.1-4.7,9.8-9.8h-3.1c-0.8,0-1.5-0.7-1.5-1.5s0.7-1.5,1.5-1.5h3.1 + C26.6,9.4,22.5,5.4,17.5,4.7z M16,12c2.2,0,4,1.8,4,4s-1.8,4-4,4s-4-1.8-4-4S13.8,12,16,12z"/> +</svg> diff --git a/media/img/marker.png b/media/img/marker.png new file mode 100644 index 0000000000000000000000000000000000000000..38a59009410365459c0cf68fcedb5f6618bdd65a GIT binary patch literal 1797 zcmaJ?c~BE~7!9X@L{UUAUTYAgV6s^t2?+#<B#{7#Tq30^gk&KBvb!V;2`Ez)l&W|D zQLqIHUR4xSO1%}Tj1G#5#RC;oR0;|zYNc9b=thC|kJ6pl-QV}kd+$4Uc0-tS5!r@f zLn4vL;vhi;@t#lI)2u9sXKgFH$7tZf7(5b9#C0+aOyVoh1Q-yjWl3-ZEK_8rHNn0l zlG$8UR16*?3FXRBHC<-J(DiDJV3SC`{(4L%Pl0hD0ZvjOJnE~{^;AHm;8B+{B@77` z2rE@VSsFMpOByB5N|AFERDVCfSI;E`)G#gs^y*YZ%hmI!pLMy!+2{tTz-I`a!lQmk zDn=3p1fm)kVA2^hIfDfOEH66b#o%~x762X$hyg+#AcIYV7+fZs%YcC4i%LY(C=$65 z0?}|R!s1btIF4~aP^Z(;b)IxolLSH>4hLj-fF2$+0zuPeBDhRXL$uB#3IbRw*QhXD zg(84aQI>$F<2)*n>6a4J*r+U`9c~lRFi<bUK#0yTmNWvCNd6zHR*#~!cm(`UzW*t# zjmpGeFap-1=^8mPxI|}TD2yAZfn_+Vi9*rTkuHWQQ5@AOQ49!-WYQr(B9p5Sqvtb2 zBH@Y=EiOakuvoyO5*l=sO2LH$B93SwOT=diIS?ddczdt}LJmv7;X@)8TL_JC1*kk- z4I}snS24<EGsff^mqCpYkp-|ul>sY68dME@E}E+vI~T#2dc$1Bn7OcoV{$<v8PGV| ze~o&?MeL99IJ&rmF}nINLhQSSSnPtubJ~dSMJE>UqxAQhi$j!C_;%|LCMC{B%8}4U z!6fEHt68gN{ZSYYa3<5sk!SUo|9Vdad+SC{Psi^4u8+n2)XCO<;ZwrF@PyEW+NR%+ zLOVOk%BBs)y35)^Yfp<ssqb5P%^BUjRW;8kU8m>{$BHE%$@niViTfRF0<|rU%3uSU zxBg*x*wucMlt7&gcj}FfW7}R9o;Mr1+9udhz2eF1Ik+Qb7wL!CBc(CD-gx!7-_%t| zeq>$c7FUYP`~pCyDO}dN2bbPRjovMeT)FAtirlWOA&d4*oaYn!q_V1Jf~H7(Ir~%4 zwZ`060?QdYZ`nI#nDu`hkCA*fzCOEyCt)6SUasI}2M;s^+-Nd=Prk65)E9?(ljF%f zU}`lxIbnI3A^X#bxeR29ERW*Wx7?}x0=F|gFK2*X#$0*7>3Oen5?*)6BH4L@E~!Go zZ%Ds+DRucX?%lzzm7+O@gU(U|+sVBBWstC>uku#Xy8Q(ctIYTfoXb|*Ur5oS)yH#I zJ9o1S>z}j?-FCVb_OymJ`@kwk8`GEB_o7o`8|&V@jWaj-v%Riv3Ha#52ZiruS)85Y zP&X&!-NKm;m2WzHiki?qKSNZogJ<JfgEDQaMQrZ$HOPd$`8l*S_kG(tW%j~=1ZnpN zHkJ)5zbc*AHTB0!<S>^hPx{QBnU>_#MilOSAD!z_YCbi1-=dc419chakU&-V71pd$ zXaQbqGH`30n;Wx}bzr<aA%EeOlhhR{>x<W&d;8$*yY^cfG6xw8bn)^!?|4l)TY9{5 zoW<ey7Rj{7r?3!{n;TCqJ9MSKE5h|fS^s#hHGA{*Z`?m-P+AN34Om_XIr9Cpt8QVN z+wb_mGw3GGy>^l9XB3l9clTX4bUrQZYQ5N2>bIac`~3WdwsmQ)5m?Vs+pIMd2TQ+| zTOMv{lPsMl$!;1yJ;ZjOt=|d9iiubHe9415JfoU}Io7#pJ@$9bn3lX9T)!=2=C3yp zC8BG$2b$^|c*{!Yo?Bntfzn{lD6k;5gjHL+!P<It+R;*%mb%F^7N7C*JIr>sw#&C} z(0OCQ-k$z*XZ96Vd}pN>9lGq&awLZKbNqo_adG7rhaQ=mJ{8}NzCX$QaQ~{<hD|5k zX&;LCKXuKRXW{A+u!G(C*Mqwt<<IRD;kmr~A3Lf(6=6XJ`-;MlgZ`$rq-4`z^0(P} QImRE8SSS_LER0|KH<U`fzW@LL literal 0 HcmV?d00001 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标记聚合器用来解决加载大量点要素到地图上产生覆盖现象的问题,并提高性能。 + * 主入口类是<a href="symbols/BMapLib.MarkerClusterer.html">MarkerClusterer</a>, + * 基于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 可选参数,可选项包括:<br /> + * markers {Array<Marker>} 要聚合的标记数组<br /> + * girdSize {Number} 聚合计算时网格的像素大小,默认60<br /> + * maxZoom {Number} 最大的聚合级别,大于该级别就不进行相应的聚合<br /> + * minClusterSize {Number} 最小的聚合数量,小于该数量的不能成为一个聚合,默认为2<br /> + * isAverangeCenter {Boolean} 聚合点的落脚位置是否是所有聚合在内点的平均值,默认为否,落脚在聚合内的第一个点<br /> + * styles {Array<IconStyle>} 自定义聚合后的图标风格,请参考TextIconOverlay类<br /> + */ + 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<Marker>} markers 要聚合的标记数组 + * + * @return 无返回值。 + */ + MarkerClusterer.prototype.addMarkers = function(markers){ + for(var i = 0, len = markers.length; i <len ; i++){ + this._pushMarkerTo(markers[i]); + } + this._createClusters(); + }; + + /** + * 把一个标记添加到要聚合的标记数组中 + * @param {BMap.Marker} marker 要添加的标记 + * + * @return 无返回值。 + */ + MarkerClusterer.prototype._pushMarkerTo = function(marker){ + var index = indexOf(marker, this._markers); + if(index === -1){ + marker.isInCluster = false; + this._markers.push(marker);//Marker拖放后enableDragging不做变化,忽略 + } + }; + + /** + * 添加一个聚合的标记。 + * @param {BMap.Marker} marker 要聚合的单个标记。 + * @return 无返回值。 + */ + MarkerClusterer.prototype.addMarker = function(marker) { + this._pushMarkerTo(marker); + this._createClusters(); + }; + + /** + * 根据所给定的标记,创建聚合点,并且遍历所有聚合点 + * @return 无返回值 + */ + MarkerClusterer.prototype._createClusters = function(){ + var mapBounds = this._map.getBounds(); + var extendedBounds = getExtendedBounds(this._map, mapBounds, this._gridSize); + for(var i = 0, marker; marker = this._markers[i]; i++){ + if(!marker.isInCluster && extendedBounds.containsPoint(marker.getPosition()) ){ + this._addToClosestCluster(marker); + } + } + + var len = this._markers.length; + for (var i = 0; i < len; i++) { + if(this._clusters[i]){ + this._clusters[i].render(); + } + } + }; + + /** + * 根据标记的位置,把它添加到最近的聚合中 + * @param {BMap.Marker} marker 要进行聚合的单个标记 + * + * @return 无返回值。 + */ + MarkerClusterer.prototype._addToClosestCluster = function (marker){ + var distance = 4000000; + var clusterToAddTo = null; + var position = marker.getPosition(); + for(var i = 0, cluster; cluster = this._clusters[i]; i++){ + var center = cluster.getCenter(); + if(center){ + var d = this._map.getDistance(center, marker.getPosition()); + if(d < distance){ + distance = d; + clusterToAddTo = cluster; + } + } + } + + if (clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)){ + clusterToAddTo.addMarker(marker); + } else { + var cluster = new Cluster(this); + cluster.addMarker(marker); + this._clusters.push(cluster); + } + }; + + /** + * 清除上一次的聚合的结果 + * @return 无返回值。 + */ + MarkerClusterer.prototype._clearLastClusters = function(){ + for(var i = 0, cluster; cluster = this._clusters[i]; i++){ + cluster.remove(); + } + this._clusters = [];//置空Cluster数组 + this._removeMarkersFromCluster();//把Marker的cluster标记设为false + }; + + /** + * 清除某个聚合中的所有标记 + * @return 无返回值 + */ + MarkerClusterer.prototype._removeMarkersFromCluster = function(){ + for(var i = 0, marker; marker = this._markers[i]; i++){ + marker.isInCluster = false; + } + }; + + /** + * 把所有的标记从地图上清除 + * @return 无返回值 + */ + MarkerClusterer.prototype._removeMarkersFromMap = function(){ + for(var i = 0, marker; marker = this._markers[i]; i++){ + marker.isInCluster = false; + this._map.removeOverlay(marker); + } + }; + + /** + * 删除单个标记 + * @param {BMap.Marker} marker 需要被删除的marker + * + * @return {Boolean} 删除成功返回true,否则返回false + */ + MarkerClusterer.prototype._removeMarker = function(marker) { + var index = indexOf(marker, this._markers); + if (index === -1) { + return false; + } + this._map.removeOverlay(marker); + this._markers.splice(index, 1); + return true; + }; + + /** + * 删除单个标记 + * @param {BMap.Marker} marker 需要被删除的marker + * + * @return {Boolean} 删除成功返回true,否则返回false + */ + MarkerClusterer.prototype.removeMarker = function(marker) { + var success = this._removeMarker(marker); + if (success) { + this._clearLastClusters(); + this._createClusters(); + } + return success; + }; + + /** + * 删除一组标记 + * @param {Array<BMap.Marker>} 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<IconStyle>} 聚合的样式风格集合 + */ + MarkerClusterer.prototype.getStyles = function() { + return this._styles; + }; + + /** + * 设置聚合的样式风格集合 + * @param {Array<IconStyle>} 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<Marker>} 标记数组。 + */ + 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继承。 + * 主入口类是<a href="symbols/BMapLib.TextIconOverlay.html">TextIconOverlay</a>, + * 基于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)。<br /> + * baidu.dom._styleFixer和baidu.dom._styleFilter可以为本模块提供支持。<br /> + * 其中_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. 不支持跨浏览器的鼠标滚轮事件监听器添加<br> + 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的类应该直接或者间接调用它的构造函数。<br>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绑定的自定义事件回调函数之外,还会调用直接绑定到对象上面的自定义事件。例如:<br> + myobj.onMyEvent = function(){}<br> + 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 可选参数,可选项包括:<br /> + *"<b>styles</b>":{Array<IconStyle>} 一组图标风格。单个图表风格包括以下几个属性:<br /> + * url {String} 图片的url地址。(必选)<br /> + * size {Size} 图片的大小。(必选)<br /> + * anchor {Size} 图标定位在地图上的位置相对于图标左上角的偏移值,默认偏移值为图标的中心位置。(可选)<br /> + * offset {Size} 图片相对于可视区域的偏移值,此功能的作用等同于CSS中的background-position属性。(可选)<br /> + * textSize {Number} 文字的大小。(可选,默认10)<br /> + * textColor {String} 文字的颜色。(可选,默认black)<br /> + */ + 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<IconStyle>} 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 = `<span class="custom-image-number">${this._text}</span>`; + this._domElement.style.cssText = this.buildImageCssText(newStyle); + const imageElement = `<img src=${imageUrl} width="72" height="72" />` + const htmlString = ` + <div class="custom-image-container"> + ${this._text > 1 ? customImageNumber : ''} + ${imageUrl ? imageElement : '<div class="empty-custom-image-wrapper"></div>'} + <i class='plugin-label-arrow dtable-font dtable-icon-drop-down'></i> + </div> + ` + 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参数,包括以下返回值: + + * <br />"<b>type</b> : {String} 事件类型 + + * <br />"<b>target</b>:{BMapLib.TextIconOverlay} 事件目标 + + * + + */ + + /** + + * 当鼠标进入该覆盖物区域时会触发该事件 + + * @name TextIconOverlay#mouseover + + * @event + * @param {Event Object} e 回调函数会返回event参数,包括以下返回值: + + * <br />"<b>type</b> : {String} 事件类型 + + * <br />"<b>target</b>:{BMapLib.TextIconOverlay} 事件目标 + + * <br />"<b>point</b> : {BMap.Point} 最新添加上的节点BMap.Point对象 + + * <br />"<b>pixel</b>:{BMap.pixel} 最新添加上的节点BMap.Pixel对象 + + * + + * @example <b>参考示例:</b><br /> + + * myTextIconOverlay.addEventListener("mouseover", function(e) { alert(e.point); }); + + */ + + /** + + * 当鼠标离开该覆盖物区域时会触发该事件 + + * @name TextIconOverlay#mouseout + + * @event + + * @param {Event Object} e 回调函数会返回event参数,包括以下返回值: + + * <br />"<b>type</b> : {String} 事件类型 + + * <br />"<b>target</b>:{BMapLib.TextIconOverlay} 事件目标 + + * <br />"<b>point</b> : {BMap.Point} 最新添加上的节点BMap.Point对象 + + * <br />"<b>pixel</b>:{BMap.pixel} 最新添加上的节点BMap.Pixel对象 + + * + + * @example <b>参考示例:</b><br /> + + * 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