@progress/kendo-charts
Version:
Kendo UI platform-independent Charts library
470 lines (375 loc) • 10.8 kB
JavaScript
import {
geometry as g,
throttle
} from '@progress/kendo-drawing';
import {
deepExtend,
round,
limitValue,
hashKey,
setDefaultOptions,
isFunction
} from '../../common';
import { removeChildren } from '../utils';
import { Layer } from './layer';
import TemplateService from '../../services/template-service';
let math = Math,
Point = g.Point;
function compileTemplate(template) {
if (isFunction(template)) {
return template;
}
return TemplateService.compile(template);
}
function roundPoint(point) {
return new Point(round(point.x), round(point.y));
}
function renderSize(size) {
let newSize = size;
if (typeof(size) !== "string") {
newSize += "px";
}
return newSize;
}
export class TileLayer extends Layer {
constructor(map, options) {
super(map, options);
if (typeof this.options.subdomains === 'string') {
this.options.subdomains = this.options.subdomains.split('');
}
let viewType = this._viewType();
this._view = new viewType(this.element, this.options);
}
destroy() {
super.destroy();
this._view.destroy();
this._view = null;
}
_beforeReset() {
let map = this.map;
let origin = map.locationToLayer(map.extent().nw).round();
this._view.viewOrigin(origin);
}
_reset() {
super._reset();
this._updateView();
this._view.reset();
}
_viewType() {
return TileView;
}
_activate() {
super._activate();
if (!this.support.mobileOS) {
if (!this._pan) {
this._pan = throttle(this._render.bind(this), 100);
}
this.map.bind('pan', this._pan);
}
}
_deactivate() {
super._deactivate();
if (this._pan) {
this.map.unbind('pan', this._pan);
}
}
_updateView() {
let view = this._view,
map = this.map,
extent = map.extent(),
extentToPoint = {
nw: map.locationToLayer(extent.nw).round(),
se: map.locationToLayer(extent.se).round()
};
view.center(map.locationToLayer(map.center()));
view.extent(extentToPoint);
view.zoom(map.zoom());
}
_resize() {
this._render();
}
_panEnd(e) {
super._panEnd(e);
this._render();
}
_render() {
this._updateView();
this._view.render();
}
}
setDefaultOptions(TileLayer, {
tileSize: 256,
subdomains: ['a', 'b', 'c'],
urlTemplate: '',
zIndex: 1
});
export class TileView {
constructor(element, options) {
this.element = element;
this._initOptions(options);
this.pool = new TilePool();
}
_initOptions(options) {
this.options = deepExtend({}, this.options, options);
}
center(center) {
this._center = center;
}
extent(extent) {
this._extent = extent;
}
viewOrigin(origin) {
this._viewOrigin = origin;
}
zoom(zoom) {
this._zoom = zoom;
}
pointToTileIndex(point) {
return new Point(math.floor(point.x / this.options.tileSize), math.floor(point.y / this.options.tileSize));
}
tileCount() {
let size = this.size(),
firstTileIndex = this.pointToTileIndex(this._extent.nw),
nw = this._extent.nw,
point = this.indexToPoint(firstTileIndex).translate(-nw.x, -nw.y);
return {
x: math.ceil((math.abs(point.x) + size.width) / this.options.tileSize),
y: math.ceil((math.abs(point.y) + size.height) / this.options.tileSize)
};
}
size() {
let nw = this._extent.nw,
se = this._extent.se,
diff = se.clone().translate(-nw.x, -nw.y);
return {
width: diff.x,
height: diff.y
};
}
indexToPoint(index) {
let x = index.x,
y = index.y;
return new Point(x * this.options.tileSize, y * this.options.tileSize);
}
subdomainText() {
let subdomains = this.options.subdomains;
return subdomains[this.subdomainIndex++ % subdomains.length];
}
destroy() {
removeChildren(this.element);
this.pool.empty();
}
reset() {
this.pool.reset();
this.subdomainIndex = 0;
this.render();
}
render() {
let size = this.tileCount(),
firstTileIndex = this.pointToTileIndex(this._extent.nw),
tile, x, y;
for (x = 0; x < size.x; x++) {
for (y = 0; y < size.y; y++) {
tile = this.createTile({
x: firstTileIndex.x + x,
y: firstTileIndex.y + y
});
if (!tile.visible) {
tile.show();
}
}
}
}
createTile(currentIndex) {
let options = this.tileOptions(currentIndex);
let tile = this.pool.get(this._center, options);
if (!tile.element.parentNode) {
this.element.append(tile.element);
}
return tile;
}
tileOptions(currentIndex) {
let index = this.wrapIndex(currentIndex),
point = this.indexToPoint(currentIndex),
origin = this._viewOrigin,
offset = point.clone().translate(-origin.x, -origin.y);
return {
index: index,
currentIndex: currentIndex,
point: point,
offset: roundPoint(offset),
zoom: this._zoom,
size: this.options.tileSize,
subdomain: this.subdomainText(),
urlTemplate: this.options.urlTemplate,
errorUrlTemplate: this.options.errorUrlTemplate
};
}
wrapIndex(index) {
let boundary = math.pow(2, this._zoom);
return {
x: this.wrapValue(index.x, boundary),
y: limitValue(index.y, 0, boundary - 1)
};
}
wrapValue(value, boundary) {
let remainder = math.abs(value) % boundary;
let wrappedValue = value;
if (value >= 0) {
wrappedValue = remainder;
} else {
wrappedValue = boundary - (remainder === 0 ? boundary : remainder);
}
return wrappedValue;
}
}
export class ImageTile {
constructor(id, options) {
this.id = id;
this.visible = true;
this._initOptions(options);
this.createElement();
this.show();
}
destroy() {
const element = this.element;
const parentNode = element ? element.parentNode : null;
if (element) {
if (parentNode) {
parentNode.removeChild(element);
}
this.element = null;
}
}
_initOptions(options) {
this.options = deepExtend({}, this.options, options);
}
createElement() {
let el = document.createElement("img");
const size = this.options.size + "px";
el.setAttribute("alt", "");
el.style.position = "absolute";
el.style.display = "block";
el.style.width = el.style.maxWidth = size;
el.style.height = el.style.maxHeight = size;
this.element = el;
// todo
// add on error handler
// this.element =
// $('<img style=\'position: absolute; display: block;\' alt=\'\' />')
// .css({
// width: this.options.size,
// height: this.options.size
// })
// .on('error', proxy(function(e) {
// if (this.errorUrl()) {
// e.target.setAttribute('src', this.errorUrl());
// } else {
// e.target.removeAttribute('src');
// }
// }, this));
}
show() {
let element = this.element;
element.style.top = renderSize(this.options.offset.y);
element.style.left = renderSize(this.options.offset.x);
let url = this.url();
if (url) {
element.setAttribute('src', url);
}
element.style.visibility = 'visible';
this.visible = true;
}
hide() {
this.element.style.visibility = 'hidden';
this.visible = false;
}
url() {
let urlResult = compileTemplate(this.options.urlTemplate);
return urlResult(this.urlOptions());
}
errorUrl() {
let urlResult = compileTemplate(this.options.errorUrlTemplate);
return urlResult(this.urlOptions());
}
urlOptions() {
let options = this.options;
return {
zoom: options.zoom,
subdomain: options.subdomain,
z: options.zoom,
x: options.index.x,
y: options.index.y,
s: options.subdomain,
quadkey: options.quadkey,
q: options.quadkey,
culture: options.culture,
c: options.culture
};
}
}
setDefaultOptions(ImageTile, {
urlTemplate: '',
errorUrlTemplate: ''
});
export class TilePool {
constructor() {
this._items = [];
}
get(center, options) {
if (this._items.length >= this.options.maxSize) {
this._remove(center);
}
return this._create(options);
}
empty() {
let items = this._items;
for (let i = 0; i < items.length; i++) {
items[i].destroy();
}
this._items = [];
}
reset() {
let items = this._items;
for (let i = 0; i < items.length; i++) {
items[i].hide();
}
}
_create(options) {
let items = this._items;
let tile;
let id = hashKey(options.point.toString() + options.offset.toString() + options.zoom + options.urlTemplate);
for (let i = 0; i < items.length; i++) {
if (items[i].id === id) {
tile = items[i];
break;
}
}
if (tile) {
tile.show();
} else {
tile = new ImageTile(id, options);
this._items.push(tile);
}
return tile;
}
_remove(center) {
let items = this._items;
let maxDist = -1;
let index = -1;
for (let i = 0; i < items.length; i++) {
let dist = items[i].options.point.distanceTo(center);
if (dist > maxDist && !items[i].visible) {
index = i;
maxDist = dist;
}
}
if (index !== -1) {
items[index].destroy();
items.splice(index, 1);
}
}
}
setDefaultOptions(TilePool, {
maxSize: 100
});