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