import { isString, flash, isNil, extend, isFunction, isNumber, now } from '../core/util'; import { on, off, createEl, stopPropagation } from '../core/util/dom'; import Browser from '../core/Browser'; import Handler from '../handler/Handler'; import Handlerable from '../handler/Handlerable'; import DragHandler from '../handler/Drag'; import Coordinate from '../geo/Coordinate'; import Point from '../geo/Point'; import UIComponent, { UIComponentAlignOptionsType, UIComponentOptionsType } from './UIComponent'; import type { Map } from '../map'; import { MapStateCache } from '../map/MapStateCache'; /** * @property {Object} options - construct options * @property {String} [options.containerClass=null] - css class name applied to UIMarker's DOM container * @property {Boolean} [options.draggable=false] - if the marker can be dragged. * @property {Number} [options.single=false] - if the marker is a global single one. * @property {String|HTMLElement} options.content - content of the marker, can be a string type HTML code or a HTMLElement. * @property {Number} [options.altitude=0] - altitude. * @property {Number} [options.minZoom=0] - the minimum zoom to display . * @property {Number} [options.maxZoom=null] - the maximum zoom to display. * @property {String} [options.horizontalAlignment=middle] - horizontal Alignment 'middle','left','right' * @property {String} [options.verticalAlignment=middle] - vertical Alignment 'middle','top','bottom' * @memberOf ui.UIMarker * @instance */ const options: UIMarkerOptionsType = { 'containerClass': null, 'eventsPropagation': true, 'draggable': false, 'single': false, 'content': null, 'altitude': 0, 'minZoom': 0, 'maxZoom': null, 'horizontalAlignment': 'middle', 'verticalAlignment': 'middle' }; const domEvents = /** * mousedown event * @event ui.UIMarker#mousedown * @type {Object} * @property {String} type - mousedown * @property {UIMarker} target - the uimarker fires event * @property {Coordinate} coordinate - coordinate of the event * @property {Point} containerPoint - container point of the event * @property {Point} viewPoint - view point of the event * @property {Event} domEvent - dom event */ 'mousedown ' + /** * mouseup event * @event ui.UIMarker#mouseup * @type {Object} * @property {String} type - mouseup * @property {UIMarker} target - the uimarker fires event * @property {Coordinate} coordinate - coordinate of the event * @property {Point} containerPoint - container point of the event * @property {Point} viewPoint - view point of the event * @property {Event} domEvent - dom event */ 'mouseup ' + /** * mouseenter event * @event ui.UIMarker#mouseenter * @type {Object} * @property {String} type - mouseenter * @property {UIMarker} target - the uimarker fires event * @property {Coordinate} coordinate - coordinate of the event * @property {Point} containerPoint - container point of the event * @property {Point} viewPoint - view point of the event * @property {Event} domEvent - dom event */ 'mouseenter ' + /** * mouseover event * @event ui.UIMarker#mouseover * @type {Object} * @property {String} type - mouseover * @property {UIMarker} target - the uimarker fires event * @property {Coordinate} coordinate - coordinate of the event * @property {Point} containerPoint - container point of the event * @property {Point} viewPoint - view point of the event * @property {Event} domEvent - dom event */ 'mouseover ' + /** * mouseout event * @event ui.UIMarker#mouseout * @type {Object} * @property {String} type - mouseout * @property {UIMarker} target - the uimarker fires event * @property {Coordinate} coordinate - coordinate of the event * @property {Point} containerPoint - container point of the event * @property {Point} viewPoint - view point of the event * @property {Event} domEvent - dom event */ 'mouseout ' + /** * mousemove event * @event ui.UIMarker#mousemove * @type {Object} * @property {String} type - mousemove * @property {UIMarker} target - the uimarker fires event * @property {Coordinate} coordinate - coordinate of the event * @property {Point} containerPoint - container point of the event * @property {Point} viewPoint - view point of the event * @property {Event} domEvent - dom event */ 'mousemove ' + /** * click event * @event ui.UIMarker#click * @type {Object} * @property {String} type - click * @property {UIMarker} target - the uimarker fires event * @property {Coordinate} coordinate - coordinate of the event * @property {Point} containerPoint - container point of the event * @property {Point} viewPoint - view point of the event * @property {Event} domEvent - dom event */ 'click ' + /** * dblclick event * @event ui.UIMarker#dblclick * @type {Object} * @property {String} type - dblclick * @property {UIMarker} target - the uimarker fires event * @property {Coordinate} coordinate - coordinate of the event * @property {Point} containerPoint - container point of the event * @property {Point} viewPoint - view point of the event * @property {Event} domEvent - dom event */ 'dblclick ' + /** * contextmenu event * @event ui.UIMarker#contextmenu * @type {Object} * @property {String} type - contextmenu * @property {UIMarker} target - the uimarker fires event * @property {Coordinate} coordinate - coordinate of the event * @property {Point} containerPoint - container point of the event * @property {Point} viewPoint - view point of the event * @property {Event} domEvent - dom event */ 'contextmenu ' + /** * keypress event * @event ui.UIMarker#keypress * @type {Object} * @property {String} type - keypress * @property {UIMarker} target - the uimarker fires event * @property {Coordinate} coordinate - coordinate of the event * @property {Point} containerPoint - container point of the event * @property {Point} viewPoint - view point of the event * @property {Event} domEvent - dom event */ 'keypress ' + /** * touchstart event * @event ui.UIMarker#touchstart * @type {Object} * @property {String} type - touchstart * @property {UIMarker} target - the uimarker fires event * @property {Coordinate} coordinate - coordinate of the event * @property {Point} containerPoint - container point of the event * @property {Point} viewPoint - view point of the event * @property {Event} domEvent - dom event */ 'touchstart ' + /** * touchmove event * @event ui.UIMarker#touchmove * @type {Object} * @property {String} type - touchmove * @property {UIMarker} target - the uimarker fires event * @property {Coordinate} coordinate - coordinate of the event * @property {Point} containerPoint - container point of the event * @property {Point} viewPoint - view point of the event * @property {Event} domEvent - dom event */ 'touchmove ' + /** * touchend event * @event ui.UIMarker#touchend * @type {Object} * @property {String} type - touchend * @property {UIMarker} target - the uimarker fires event * @property {Coordinate} coordinate - coordinate of the event * @property {Point} containerPoint - container point of the event * @property {Point} viewPoint - view point of the event * @property {Event} domEvent - dom event */ 'touchend'; /** * * @classdesc * Class for UI Marker, a html based marker positioned by geographic coordinate.
* * @category ui * @extends ui.UIComponent * @mixes Handlerable * @memberOf ui * @example * var dom = document.createElement('div'); * dom.innerHTML = 'hello ui marker'; * var marker = new maptalks.ui.UIMarker([0, 0], { * draggable : true, * content : dom * }).addTo(map); */ class UIMarker extends Handlerable(UIComponent) { //@internal _markerCoord: Coordinate; options: UIMarkerOptionsType; //@internal _owner: Map; //@internal _mousedownEvent: MouseEvent; //@internal _mouseupEvent: MouseEvent; //@internal _touchstartTime: number; /** * As it's renderered by HTMLElement such as a DIV, it:
* 1. always on the top of all the map layers
* 2. can't be snapped as it's not drawn on the canvas.
* @param {Coordinate} coordinate - UIMarker's coordinates * @param {Object} options - options defined in [UIMarker]{@link UIMarker#options} */ constructor(coordinate: Coordinate | Array, options: UIMarkerOptionsType) { super(options); this._markerCoord = new Coordinate(coordinate as Coordinate); } // TODO: obtain class in super //@internal _getClassName() { return 'UIMarker'; } /** * Sets the coordinates * @param {Coordinate} coordinates - UIMarker's coordinate * @returns {UIMarker} this * @fires UIMarker#positionchange */ setCoordinates(coordinates: Coordinate) { if (!coordinates) { return this; } if (!(coordinates instanceof Coordinate)) { try { coordinates = new Coordinate(coordinates); } catch (error) { console.error(error); return this; } } this._markerCoord = coordinates; /** * positionchange event. * * @event ui.UIMarker#positionchange * @type {Object} * @property {String} type - positionchange * @property {UIMarker} target - ui marker */ this.fire('positionchange'); if (this.isVisible()) { this._onlyUpdatePosition = true; this._coordinate = this._markerCoord; this._setPosition(); this._collides(); this._onlyUpdatePosition = false; } return this; } /** * Gets the coordinates * @return {Coordinate} coordinates */ getCoordinates() { return this._markerCoord; } //accord with isSupport for tooltip getCenter() { return this.getCoordinates(); } // for infowindow getAltitude(): number { const coordinates = this.getCoordinates() || {}; if (isNumber((coordinates as Coordinate).z)) { return (coordinates as Coordinate).z; } return this.options.altitude || 0; } setAltitude(alt: number) { if (isNumber(alt) && this._markerCoord) { this._markerCoord.z = alt; if (this._updatePosition) { this._updatePosition(); this._collides(); } } return this; } /** * Sets the content of the UIMarker * @param {String|HTMLElement} content - UIMarker's content * @returns {UIMarker} this * @fires UIMarker#contentchange */ setContent(content: string | HTMLElement) { const old = this.options['content']; this.options['content'] = content; /** * contentchange event. * * @event ui.UIMarker#contentchange * @type {Object} * @property {String} type - contentchange * @property {UIMarker} target - ui marker * @property {String|HTMLElement} old - old content * @property {String|HTMLElement} new - new content */ this.fire('contentchange', { 'old': old, 'new': content }); if (this.isVisible()) { this.show(); } return this; } /** * Gets the content of the UIMarker * @return {String|HTMLElement} content */ getContent(): string | HTMLElement { return this.options['content']; } onAdd() { if (this._owner && !this._owner.isMap) { const owner = this._owner as any; throw new Error('UIMarker Can only be added to the map, but owner is:' + owner.getJSONType && owner.getJSONType()); } this.show(); return this; } /** * Show the UIMarker * @returns {UIMarker} this * @fires UIMarker#showstart * @fires UIMarker#showend */ show(): this { return super.show(this._markerCoord); } /** * Flash the UIMarker, show and hide by certain internal for times of count. * * @param {Number} [interval=100] - interval of flash, in millisecond (ms) * @param {Number} [count=4] - flash times * @param {Function} [cb=null] - callback function when flash ended * @param {*} [context=null] - callback context * @return {UIMarker} this */ flash(interval: number, count: number, cb?: (arg: any) => void, context?: any) { return flash.call(this, interval, count, cb, context); } /** * A callback method to build UIMarker's HTMLElement * @protected * @param {Map} map - map to be built on * @return {HTMLElement} UIMarker's HTMLElement */ buildOn(): HTMLElement { const oldDom = this.getDOM(); this._bindDomEvents(oldDom, 'off'); let dom; const content = this.options['content']; const isStr = isString(content); if (isStr || isFunction(content)) { dom = createEl('div'); if (isStr) { dom.innerHTML = this.options['content']; } else { //dymatic render dom content content.bind(this)(dom); } } else { dom = this.options['content']; } if (this.options['containerClass']) { dom.className = this.options['containerClass']; } this._registerDOMEvents(dom); this._bindDomEvents(dom, 'on'); this._appendCustomClass(dom); return dom; } /** * Gets UIMarker's HTMLElement's position offset, it's caculated dynamically accordiing to its actual size. * @protected * @return {Point} offset */ getOffset() { const size = this.getSize(); //default is middle let offsetX = -size.width / 2, offsetY = -size.height / 2; const { horizontalAlignment, verticalAlignment } = this.options; if (horizontalAlignment === 'left') { offsetX = -size.width; } else if (horizontalAlignment === 'right') { offsetX = 0; } if (verticalAlignment === 'top') { offsetY = -size.height; } else if (verticalAlignment === 'bottom') { offsetY = 0; } return new Point(offsetX, offsetY); } /** * Gets UIMarker's transform origin for animation transform * @protected * @return {Point} transform origin */ getTransformOrigin() { return 'center center'; } onDomRemove() { const dom = this.getDOM(); this._removeDOMEvents(dom); } /** * Whether the uimarker is being dragged. * @returns {Boolean} */ isDragging(): boolean { if (this['draggable']) { return this['draggable'].isDragging(); } return false; } //@internal _registerDOMEvents(dom: HTMLElement) { on(dom, domEvents, this._onDomEvents, this); } //@internal _onDomEvents(e: MouseEvent, type?: string) { const event = this.getMap()._parseEvent(e, e.type); type = type || e.type; if (type === 'mousedown') { this._mousedownEvent = e; } if (type === 'mouseup') { this._mouseupEvent = e; } if (type === 'click' && this._mouseClickPositionIsChange()) { return; } if (type === 'touchstart') { this._touchstartTime = now(); } this.fire(type, event); // Mobile device simulation click event if (type === 'touchend' && Browser.touch) { const clickTimeThreshold = this.getMap().options.clickTimeThreshold || 280; if (now() - this._touchstartTime < clickTimeThreshold) { this._onDomEvents(e, 'click'); } } } //@internal _removeDOMEvents(dom: HTMLElement) { off(dom, domEvents, this._onDomEvents); } //@internal _mouseClickPositionIsChange() { const { x: x1, y: y1 } = this._mousedownEvent || {}; const { x: x2, y: y2 } = this._mouseupEvent || {}; return (x1 !== x2 || y1 !== y2); } /** * Get the connect points of panel for connector lines. * @private */ //@internal _getConnectPoints() { const map = this.getMap(); const containerPoint = map.coordToContainerPoint(this.getCoordinates()); const size = this.getSize(), width = size.width, height = size.height; const anchors = [ //top center map.containerPointToCoordinate( containerPoint.add(-width / 2, 0) ), //middle right map.containerPointToCoordinate( containerPoint.add(width / 2, 0) ), //bottom center map.containerPointToCoordinate( containerPoint.add(0, height / 2) ), //middle left map.containerPointToCoordinate( containerPoint.add(0, -height / 2) ) ]; return anchors; } //@internal _getViewPoint() { let alt = 0; if (this._owner) { const altitude = this.getAltitude(); if (altitude > 0) { alt = this._meterToPoint(this._coordinate, altitude); } } return this.getMap().coordToViewPoint(this._coordinate, undefined, alt) ._add(this.options['dx'], this.options['dy']); } //@internal _getDefaultEvents() { return extend({}, super._getDefaultEvents(), { 'zooming zoomend': this.onZoomFilter }); } //@internal _setPosition() { //show/hide zoomFilter this.onZoomFilter(); super._setPosition(); } onZoomFilter() { const dom = this.getDOM(); if (!dom) return; if (!this.isVisible() && dom.style.display !== 'none') { dom.style.display = 'none'; } else if (this.isVisible() && dom.style.display === 'none') { dom.style.display = ''; } } isVisible() { const map = this.getMap(); if (!map) { return false; } if (!this.options['visible']) { return false; } const cache = MapStateCache[map.id]; const zoom = cache ? cache.zoom : map.getZoom(); const { minZoom, maxZoom } = this.options; if (!isNil(minZoom) && zoom < minZoom || (!isNil(maxZoom) && zoom > maxZoom)) { return false; } const dom = this.getDOM(); return dom && true; } isSupportZoomFilter() { return true; } } UIMarker.mergeOptions(options); const EVENTS = Browser.touch ? 'touchstart mousedown' : 'mousedown'; class UIMarkerDragHandler extends Handler { //@internal _lastCoord: Coordinate; //@internal _lastPoint: Point; //@internal _dragHandler: DragHandler; //@internal _isDragging: boolean; target: UIMarker; constructor(target: UIMarker) { super(target); } addHooks() { this.target.on(EVENTS, this._startDrag, this); } removeHooks() { this.target.off(EVENTS, this._startDrag, this); } //@internal _startDrag(param) { const domEvent = param['domEvent']; if (domEvent.touches && domEvent.touches.length > 1 || domEvent.button === 2) { return; } if (this.isDragging()) { return; } this.target.on('click', this._endDrag, this); this._lastCoord = param['coordinate']; this._lastPoint = param['containerPoint']; this._prepareDragHandler(); this._dragHandler.onMouseDown(param['domEvent']); /** * drag start event * @event ui.UIMarker#dragstart * @type {Object} * @property {String} type - dragstart * @property {UIMarker} target - the uimarker fires event * @property {Coordinate} coordinate - coordinate of the event * @property {Point} containerPoint - container point of the event * @property {Point} viewPoint - view point of the event * @property {Event} domEvent - dom event */ this.target.fire('dragstart', param); } //@internal _prepareDragHandler() { this._dragHandler = new DragHandler(this.target.getDOM(), { 'cancelOn': this._cancelOn.bind(this), 'ignoreMouseleave': true }); this._dragHandler.on('mousedown', this._onMouseDown, this); this._dragHandler.on('dragging', this._dragging, this); this._dragHandler.on('mouseup', this._endDrag, this); this._dragHandler.enable(); } //@internal _cancelOn(domEvent) { const target = domEvent.srcElement || domEvent.target, tagName = target.tagName.toLowerCase(); if (tagName === 'button' || tagName === 'input' || tagName === 'select' || tagName === 'option' || tagName === 'textarea') { return true; } return false; } //@internal _onMouseDown(param) { stopPropagation(param['domEvent']); } //@internal _dragging(param) { const target = this.target, map = target.getMap(), eventParam = map._parseEvent(param['domEvent']), domEvent = eventParam['domEvent']; const touchEvent = domEvent as TouchEvent; if (touchEvent.touches && touchEvent.touches.length > 1) { return; } if (!this._isDragging) { this._isDragging = true; return; } const coord = eventParam['coordinate'], point = eventParam['containerPoint']; if (!this._lastCoord) { this._lastCoord = coord; } if (!this._lastPoint) { this._lastPoint = point; } const coordOffset = coord.sub(this._lastCoord), pointOffset = point.sub(this._lastPoint); this._lastCoord = coord; this._lastPoint = point; this.target.setCoordinates(this.target.getCoordinates().add(coordOffset)); eventParam['coordOffset'] = coordOffset; eventParam['pointOffset'] = pointOffset; /** * dragging event * @event ui.UIMarker#dragging * @type {Object} * @property {String} type - dragging * @property {UIMarker} target - the uimarker fires event * @property {Coordinate} coordinate - coordinate of the event * @property {Point} containerPoint - container point of the event * @property {Point} viewPoint - view point of the event * @property {Event} domEvent - dom event */ target.fire('dragging', eventParam); } //@internal _endDrag(param) { const target = this.target, map = target.getMap(); if (this._dragHandler) { target.off('click', this._endDrag, this); this._dragHandler.disable(); delete this._dragHandler; } delete this._lastCoord; delete this._lastPoint; this._isDragging = false; if (!map) { return; } const eventParam = map._parseEvent(param['domEvent']); /** * dragend event * @event ui.UIMarker#dragend * @type {Object} * @property {String} type - dragend * @property {UIMarker} target - the uimarker fires event * @property {Coordinate} coordinate - coordinate of the event * @property {Point} containerPoint - container point of the event * @property {Point} viewPoint - view point of the event * @property {Event} domEvent - dom event */ if (target && target._mouseClickPositionIsChange && target._mouseClickPositionIsChange()) { target.fire('dragend', eventParam); } } isDragging() { if (!this._isDragging) { return false; } return true; } } UIMarker.addInitHook('addHandler', 'draggable', UIMarkerDragHandler); export default UIMarker; export type UIMarkerOptionsType = { containerClass?: string; eventsPropagation?: boolean; draggable?: boolean; single?: boolean; content?: string | HTMLElement; altitude?: number; minZoom?: number; maxZoom?: number; } & UIComponentOptionsType & UIComponentAlignOptionsType;