UNPKG

@progress/kendo-charts

Version:

Kendo UI platform-independent Charts library

952 lines (758 loc) 24.7 kB
import { geometry as g, throttle } from '@progress/kendo-drawing'; import { addClass, setDefaultOptions, valueOrDefault, defined, Observable, mousewheelDelta, limitValue, deepExtend, elementOffset, isArray, round, now, on, off, getSupportedFeatures, } from '../common'; import { EPSG3857 } from './crs'; import { Attribution } from './attribution'; import { Navigator } from './navigator'; import { ZoomControl } from './zoom'; import { Location } from './location'; import { Extent } from './extent'; import { Tooltip } from './tooltip/tooltip'; import { TileLayer } from './layers/tile'; import { BubbleLayer } from './layers/bubble'; import { ShapeLayer } from './layers/shape'; import { MarkerLayer } from './layers/marker'; import { removeChildren, proxy, setDefaultEvents, convertToHtml, renderPos } from './utils'; import { Scroller } from './scroller/scroller'; import MapService from './../services/map-service'; import { CENTER_CHANGE, INIT, ZOOM_CHANGE } from './constants'; let math = Math, min = math.min, pow = math.pow, Point = g.Point, MARKER = "marker", LOCATION = "location", FRICTION = 0.9, FRICTION_MOBILE = 0.93, MOUSEWHEEL = 'wheel', MOUSEWHEEL_THROTTLE = 50, VELOCITY_MULTIPLIER = 5, DEFAULT_ZOOM_RATE = 1; const layersMap = { bubble: BubbleLayer, shape: ShapeLayer, tile: TileLayer, [MARKER]: MarkerLayer }; class Map extends Observable { constructor(element, options = {}, themeOptions = {}, context = {}) { super(); this._init(element, options, themeOptions, context); } destroy() { this.scroller.destroy(); if (this._tooltip) { this._tooltip.destroy(); } if (this.navigator) { this.navigator.destroy(); } if (this.attribution) { this.attribution.destroy(); } if (this.zoomControl) { this.zoomControl.destroy(); } if (isArray(this.markers)) { this.markers.forEach(markerLayer => { markerLayer.destroy(); }); } else { this.markers.destroy(); } for (let i = 0; i < this.layers.length; i++) { this.layers[i].destroy(); } off(this.element, MOUSEWHEEL, this._mousewheelHandler); super.destroy(); } // eslint-disable-next-line no-unused-vars _init(element, options = {}, themeOptions = {}, context = {}) { this.support = getSupportedFeatures(); this.context = context; this.initObserver(context); this.initServices(context); this._notifyObserver(INIT); this._initOptions(options); this._setEvents(options); this.crs = new EPSG3857(); this._initElement(element); this._viewOrigin = this._getOrigin(); this._tooltip = this._createTooltip(); this._initScroller(); this._initMarkers(); this._initControls(); this._initLayers(); this._reset(); const mousewheelThrottled = throttle(this._mousewheel.bind(this), MOUSEWHEEL_THROTTLE); this._mousewheelHandler = (e) => { e.preventDefault(); mousewheelThrottled(e); }; on(this.element, MOUSEWHEEL, this._mousewheelHandler); } _initOptions(options) { this.options = deepExtend({}, this.options, options); } _initElement(element) { this.element = element; addClass(element, "k-map"); element.style.position = "relative"; element.setAttribute("data-role", "map"); removeChildren(element); const div = convertToHtml("<div />"); this.element.appendChild(div); } initServices(context = {}) { this.widgetService = new MapService(this, context); } initObserver(context = {}) { this.observers = []; this.addObserver(context.observer); } addObserver(observer) { if (observer) { this.observers.push(observer); } } removeObserver(observer) { const index = this.observers.indexOf(observer); if (index >= 0) { this.observers.splice(index, 1); } } requiresHandlers(eventNames) { const observers = this.observers; for (let idx = 0; idx < observers.length; idx++) { if (observers[idx].requiresHandlers(eventNames)) { return true; } } } trigger(name, args = {}) { args.sender = this; const observers = this.observers; let isDefaultPrevented = false; for (let idx = 0; idx < observers.length; idx++) { if (observers[idx].trigger(name, args)) { isDefaultPrevented = true; } } if (!isDefaultPrevented) { super.trigger(name, args); } return isDefaultPrevented; } _notifyObserver(name, args = {}) { args.sender = this; const observers = this.observers; let isDefaultPrevented = false; for (let idx = 0; idx < observers.length; idx++) { if (observers[idx].trigger(name, args)) { isDefaultPrevented = true; } } return isDefaultPrevented; } zoom(level) { let options = this.options; let result; if (defined(level)) { const zoomLevel = math.round(limitValue(level, options.minZoom, options.maxZoom)); if (options.zoom !== zoomLevel) { options.zoom = zoomLevel; this.widgetService.notify(ZOOM_CHANGE, { zoom: options.zoom }); this._reset(); } result = this; } else { result = options.zoom; } return result; } center(center) { let result; if (center) { const current = Location.create(center); const previous = Location.create(this.options.center); if (!current.equals(previous)) { this.options.center = current.toArray(); this.widgetService.notify(CENTER_CHANGE, { center: this.options.center }); this._reset(); } result = this; } else { result = Location.create(this.options.center); } return result; } extent(extent) { let result; if (extent) { this._setExtent(extent); result = this; } else { result = this._getExtent(); } return result; } setOptions(options = {}) { const element = this.element; this.destroy(); removeChildren(element); this._init(element, options, {}, this.context); this._reset(); } locationToLayer(location, zoom) { let clamp = !this.options.wraparound; const locationObject = Location.create(location); return this.crs.toPoint(locationObject, this._layerSize(zoom), clamp); } layerToLocation(point, zoom) { let clamp = !this.options.wraparound; const pointObject = Point.create(point); return this.crs.toLocation(pointObject, this._layerSize(zoom), clamp); } locationToView(location) { const locationObject = Location.create(location); let origin = this.locationToLayer(this._viewOrigin); let point = this.locationToLayer(locationObject); return point.translateWith(origin.scale(-1)); } viewToLocation(point, zoom) { const origin = this.locationToLayer(this._getOrigin(), zoom); const pointObject = Point.create(point); const pointResult = pointObject.clone().translateWith(origin); return this.layerToLocation(pointResult, zoom); } eventOffset(e) { let x; let y; let offset = elementOffset(this.element); if ((e.x && e.x[LOCATION]) || (e.y && e.y[LOCATION])) { x = e.x[LOCATION] - offset.left; y = e.y[LOCATION] - offset.top; } else { let event = e.originalEvent || e; x = valueOrDefault(event.pageX, event.clientX) - offset.left; y = valueOrDefault(event.pageY, event.clientY) - offset.top; } const point = new g.Point(x, y); return point; } eventToView(e) { let cursor = this.eventOffset(e); return this.locationToView(this.viewToLocation(cursor)); } eventToLayer(e) { return this.locationToLayer(this.eventToLocation(e)); } eventToLocation(e) { let cursor = this.eventOffset(e); return this.viewToLocation(cursor); } viewSize() { let element = this.element; let scale = this._layerSize(); let width = element.clientWidth; if (!this.options.wraparound) { width = min(scale, width); } return { width: width, height: min(scale, element.clientHeight) }; } exportVisual() { this._reset(); return false; } hideTooltip() { if (this._tooltip) { this._tooltip.hide(); } } _setOrigin(origin, zoom) { let size = this.viewSize(), topLeft; const originLocation = this._origin = Location.create(origin); topLeft = this.locationToLayer(originLocation, zoom); topLeft.x += size.width / 2; topLeft.y += size.height / 2; this.options.center = this.layerToLocation(topLeft, zoom).toArray(); this.widgetService.notify(CENTER_CHANGE, { center: this.options.center }); return this; } _getOrigin(invalidate) { let size = this.viewSize(), topLeft; if (invalidate || !this._origin) { topLeft = this.locationToLayer(this.center()); topLeft.x -= size.width / 2; topLeft.y -= size.height / 2; this._origin = this.layerToLocation(topLeft); } return this._origin; } _setExtent(newExtent) { let raw = Extent.create(newExtent); let se = raw.se.clone(); if (this.options.wraparound && se.lng < 0 && newExtent.nw.lng > 0) { se.lng = 180 + (180 + se.lng); } const extent = new Extent(raw.nw, se); this.center(extent.center()); let width = this.element.clientWidth; let height = this.element.clientHeight; let zoom; for (zoom = this.options.maxZoom; zoom >= this.options.minZoom; zoom--) { let topLeft = this.locationToLayer(extent.nw, zoom); let bottomRight = this.locationToLayer(extent.se, zoom); let layerWidth = math.abs(bottomRight.x - topLeft.x); let layerHeight = math.abs(bottomRight.y - topLeft.y); if (layerWidth <= width && layerHeight <= height) { break; } } this.zoom(zoom); } _getExtent() { let nw = this._getOrigin(); let bottomRight = this.locationToLayer(nw); let size = this.viewSize(); bottomRight.x += size.width; bottomRight.y += size.height; let se = this.layerToLocation(bottomRight); return new Extent(nw, se); } _zoomAround(pivot, level) { this._setOrigin(this.layerToLocation(pivot, level), level); this.zoom(level); } _initControls() { let controls = this.options.controls; if (controls.attribution) { this._createAttribution(controls.attribution); } if (!this.support.mobileOS) { if (controls.navigator) { this._createNavigator(controls.navigator); } if (controls.zoom) { this._createZoomControl(controls.zoom); } } } _createControlElement(options, defaultPosition) { let pos = options.position || defaultPosition; let posSelector = '.' + renderPos(pos).replace(' ', '.'); let wrap = this.element.querySelector('.k-map-controls' + posSelector) || []; if (wrap.length === 0) { let div = document.createElement("div"); addClass(div, 'k-map-controls ' + renderPos(pos)); wrap = div; this.element.appendChild(wrap); } let div = document.createElement("div"); wrap.appendChild(div); return div; } _createAttribution(options) { let element = this._createControlElement(options, 'bottomRight'); this.attribution = new Attribution(element, options); } _createNavigator(options) { let element = this._createControlElement(options, 'topLeft'); let navigator = this.navigator = new Navigator(element, deepExtend({}, options, { icons: this.options.icons })); this._navigatorPan = this._navigatorPan.bind(this); navigator.bind('pan', this._navigatorPan); this._navigatorCenter = this._navigatorCenter.bind(this); navigator.bind('center', this._navigatorCenter); } _navigatorPan(e) { let scroller = this.scroller; let x = scroller.scrollLeft + e.x; let y = scroller.scrollTop - e.y; let bounds = this._virtualSize; let width = this.element.clientWidth; let height = this.element.clientHeight; // TODO: Move limits to scroller x = limitValue(x, bounds.x.min, bounds.x.max - width); y = limitValue(y, bounds.y.min, bounds.y.max - height); this.scroller.one('scroll', proxy(this._scrollEnd, this)); this.scroller.scrollTo(-x, -y); } _navigatorCenter() { this.center(this.options.center); } _createZoomControl(options) { let element = this._createControlElement(options, 'topLeft'); let zoomControl = this.zoomControl = new ZoomControl(element, options, this.options.icons); this._zoomControlChange = this._zoomControlChange.bind(this); zoomControl.bind('change', this._zoomControlChange); } _zoomControlChange(e) { if (!this.trigger('zoomStart', { originalEvent: e })) { this.zoom(this.zoom() + e.delta); this.trigger('zoomEnd', { originalEvent: e }); } } _initScroller() { let friction = this.support.mobileOS ? FRICTION_MOBILE : FRICTION; let zoomable = this.options.zoomable !== false; let scroller = this.scroller = new Scroller(this.element.children[0], { friction: friction, velocityMultiplier: VELOCITY_MULTIPLIER, zoom: zoomable, mousewheelScrolling: false, supportDoubleTap: true }); scroller.bind('scroll', proxy(this._scroll, this)); scroller.bind('scrollEnd', proxy(this._scrollEnd, this)); scroller.userEvents.bind('gesturestart', proxy(this._scaleStart, this)); scroller.userEvents.bind('gestureend', proxy(this._scale, this)); scroller.userEvents.bind('doubleTap', proxy(this._doubleTap, this)); scroller.userEvents.bind('tap', proxy(this._tap, this)); this.scrollElement = scroller.scrollElement; } _initLayers() { let defs = this.options.layers, layers = this.layers = []; for (let i = 0; i < defs.length; i++) { let options = defs[i]; const layer = this._createLayer(options); layers.push(layer); } } _createLayer(options) { let type = options.type || 'shape'; let layerDefaults = this.options.layerDefaults[type]; let layerOptions = type === MARKER ? deepExtend({}, this.options.markerDefaults, options, { icons: this.options.icons }) : deepExtend({}, layerDefaults, options); let layerConstructor = layersMap[type]; let layer = new layerConstructor(this, layerOptions); if (type === MARKER) { this.markers = layer; } return layer; } _createTooltip() { return new Tooltip(this.widgetService, this.options.tooltip); } /* eslint-disable arrow-body-style */ _initMarkers() { const markerLayers = (this.options.layers || []).filter(x => { return x && x.type === MARKER; }); if (markerLayers.length > 0) { // render the markers from options.layers // instead of options.markers return; } this.markers = new MarkerLayer(this, deepExtend({}, this.options.markerDefaults, { icons: this.options.icons })); this.markers.add(this.options.markers); } /* eslint-enable arrow-body-style */ _scroll(e) { let origin = this.locationToLayer(this._viewOrigin).round(); let movable = e.sender.movable; let offset = new g.Point(movable.x, movable.y).scale(-1).scale(1 / movable.scale); origin.x += offset.x; origin.y += offset.y; this._scrollOffset = offset; this._tooltip.offset = offset; this.hideTooltip(); this._setOrigin(this.layerToLocation(origin)); this.trigger('pan', { originalEvent: e, origin: this._getOrigin(), center: this.center() }); } _scrollEnd(e) { if (!this._scrollOffset || !this._panComplete()) { return; } this._scrollOffset = null; this._panEndTimestamp = now(); this.trigger('panEnd', { originalEvent: e, origin: this._getOrigin(), center: this.center() }); } _panComplete() { return now() - (this._panEndTimestamp || 0) > 50; } _scaleStart(e) { if (this.trigger('zoomStart', { originalEvent: e })) { let touch = e.touches[1]; if (touch) { touch.cancel(); } } } _scale(e) { let scale = this.scroller.movable.scale; let zoom = this._scaleToZoom(scale); let gestureCenter = new g.Point(e.center.x, e.center.y); let centerLocation = this.viewToLocation(gestureCenter, zoom); let centerPoint = this.locationToLayer(centerLocation, zoom); let originPoint = centerPoint.translate(-gestureCenter.x, -gestureCenter.y); this._zoomAround(originPoint, zoom); this.trigger('zoomEnd', { originalEvent: e }); } _scaleToZoom(scaleDelta) { let scale = this._layerSize() * scaleDelta; let tiles = scale / this.options.minSize; let zoom = math.log(tiles) / math.log(2); return math.round(zoom); } _reset() { if (this.attribution) { this.attribution.filter(this.center(), this.zoom()); } this._viewOrigin = this._getOrigin(true); this._resetScroller(); this.hideTooltip(); this.trigger('beforeReset'); this.trigger('reset'); } _resetScroller() { let scroller = this.scroller; let x = scroller.dimensions.x; let y = scroller.dimensions.y; let scale = this._layerSize(); let nw = this.extent().nw; let topLeft = this.locationToLayer(nw).round(); scroller.movable.round = true; scroller.reset(); scroller.userEvents.cancel(); let zoom = this.zoom(); scroller.dimensions.forcedMinScale = pow(2, this.options.minZoom - zoom); scroller.dimensions.maxScale = pow(2, this.options.maxZoom - zoom); let xBounds = { min: -topLeft.x, max: scale - topLeft.x }; let yBounds = { min: -topLeft.y, max: scale - topLeft.y }; if (this.options.wraparound) { xBounds.max = 20 * scale; xBounds.min = -xBounds.max; } if (this.options.pannable === false) { let viewSize = this.viewSize(); xBounds.min = yBounds.min = 0; xBounds.max = viewSize.width; yBounds.max = viewSize.height; } x.makeVirtual(); y.makeVirtual(); x.virtualSize(xBounds.min, xBounds.max); y.virtualSize(yBounds.min, yBounds.max); this._virtualSize = { x: xBounds, y: yBounds }; } // kept for API compatibility, not used _renderLayers() { } _layerSize(zoom) { const newZoom = valueOrDefault(zoom, this.options.zoom); return this.options.minSize * pow(2, newZoom); } _tap(e) { if (!this._panComplete()) { return; } let cursor = this.eventOffset(e); this.hideTooltip(); this.trigger('click', { originalEvent: e, location: this.viewToLocation(cursor) }); } _doubleTap(e) { let options = this.options; if (options.zoomable !== false) { if (!this.trigger('zoomStart', { originalEvent: e })) { let toZoom = this.zoom() + DEFAULT_ZOOM_RATE; let cursor = this.eventOffset(e); let location = this.viewToLocation(cursor); let postZoom = this.locationToLayer(location, toZoom); let origin = postZoom.translate(-cursor.x, -cursor.y); this._zoomAround(origin, toZoom); this.trigger('zoomEnd', { originalEvent: e }); } } } _mousewheel(e) { let delta = mousewheelDelta(e) > 0 ? -1 : 1; let options = this.options; let fromZoom = this.zoom(); let toZoom = limitValue(fromZoom + delta, options.minZoom, options.maxZoom); if (options.zoomable !== false && toZoom !== fromZoom) { if (!this.trigger('zoomStart', { originalEvent: e })) { let cursor = this.eventOffset(e); let location = this.viewToLocation(cursor); let postZoom = this.locationToLayer(location, toZoom); let origin = postZoom.translate(-cursor.x, -cursor.y); this._zoomAround(origin, toZoom); this.trigger('zoomEnd', { originalEvent: e }); } } } _toDocumentCoordinates(point) { const offset = elementOffset(this.element); return { left: round(point.x + offset.left), top: round(point.y + offset.top) }; } } setDefaultOptions(Map, { name: 'Map', controls: { attribution: true, navigator: { panStep: 100 }, zoom: true }, layers: [], layerDefaults: { shape: { style: { fill: { color: '#fff' }, stroke: { color: '#aaa', width: 0.5 } } }, bubble: { style: { fill: { color: '#fff', opacity: 0.5 }, stroke: { color: '#aaa', width: 0.5 } } }, marker: { shape: 'pinTarget', tooltip: { position: 'top' } } }, center: [ 0, 0 ], icons: { type: "font", svgIcons: {} }, zoom: 3, minSize: 256, minZoom: 1, maxZoom: 19, markers: [], markerDefaults: { shape: 'pinTarget', tooltip: { position: 'top' } }, wraparound: true, // If set to true, GeoJSON layer "Point" features will be rendered as markers. // Otherwise, the points will be rendered as circles. // Defaults to `true` for KUI/jQuery, `false` everywhere else. renderPointsAsMarkers: false }); setDefaultEvents(Map, [ 'beforeReset', 'click', 'markerActivate', 'markerClick', 'markerCreated', // Events for implementing custom tooltips. 'markerMouseEnter', 'markerMouseLeave', 'pan', 'panEnd', 'reset', 'shapeClick', 'shapeCreated', 'shapeFeatureCreated', 'shapeMouseEnter', 'shapeMouseLeave', 'zoomEnd', 'zoomStart' ]); export default Map;