@progress/kendo-charts
Version:
Kendo UI platform-independent Charts library
952 lines (758 loc) • 24.7 kB
JavaScript
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;