devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
410 lines (393 loc) • 15.2 kB
JavaScript
/**
* DevExtreme (esm/viz/vector_map/projection.main.js)
* Version: 21.1.4
* Build date: Mon Jun 21 2021
*
* Copyright (c) 2012 - 2021 Developer Express Inc. ALL RIGHTS RESERVED
* Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
*/
import {
extend
} from "../../core/utils/extend";
import {
makeEventEmitter
} from "./event_emitter";
var _Number = Number;
var _min = Math.min;
var _max = Math.max;
var _abs = Math.abs;
var _round = Math.round;
var _ln = Math.log;
var _pow = Math.pow;
var TWO_TO_LN2 = 2 / Math.LN2;
var MIN_BOUNDS_RANGE = 1 / 3600 / 180 / 10;
var DEFAULT_MIN_ZOOM = 1;
var DEFAULT_MAX_ZOOM = 256;
var DEFAULT_CENTER = [NaN, NaN];
var DEFAULT_ENGINE_NAME = "mercator";
function floatsEqual(f1, f2) {
return _abs(f1 - f2) < 1e-8
}
function arraysEqual(a1, a2) {
return floatsEqual(a1[0], a2[0]) && floatsEqual(a1[1], a2[1])
}
function parseAndClamp(value, minValue, maxValue, defaultValue) {
var val = _Number(value);
return isFinite(val) ? _min(_max(val, minValue), maxValue) : defaultValue
}
function parseAndClampArray(value, minValue, maxValue, defaultValue) {
return [parseAndClamp(value[0], minValue[0], maxValue[0], defaultValue[0]), parseAndClamp(value[1], minValue[1], maxValue[1], defaultValue[1])]
}
function getEngine(engine) {
return engine instanceof Engine && engine || projection.get(engine) || projection(engine) || projection.get(DEFAULT_ENGINE_NAME)
}
export var Projection = function(parameters) {
this._initEvents();
this._params = parameters;
this._engine = getEngine();
this._center = this._engine.center();
this._adjustCenter()
};
Projection.prototype = {
constructor: Projection,
_minZoom: DEFAULT_MIN_ZOOM,
_maxZoom: DEFAULT_MAX_ZOOM,
_zoom: DEFAULT_MIN_ZOOM,
_center: DEFAULT_CENTER,
_canvas: {},
_scale: [],
dispose: function() {
this._disposeEvents()
},
setEngine: function(value) {
var engine = getEngine(value);
if (this._engine !== engine) {
this._engine = engine;
this._fire("engine");
if (this._changeCenter(engine.center())) {
this._triggerCenterChanged()
}
if (this._changeZoom(this._minZoom)) {
this._triggerZoomChanged()
}
this._adjustCenter();
this._setupScreen()
}
},
setBounds: function(bounds) {
if (void 0 !== bounds) {
this.setEngine(this._engine.original().bounds(bounds))
}
},
_setupScreen: function() {
var canvas = this._canvas;
var width = canvas.width;
var height = canvas.height;
var engine = this._engine;
var aspectRatio = engine.ar();
this._x0 = canvas.left + width / 2;
this._y0 = canvas.top + height / 2;
var min = [this.project([engine.min()[0], 0])[0], this.project([0, engine.min()[1]])[1]];
var max = [this.project([engine.max()[0], 0])[0], this.project([0, engine.max()[1]])[1]];
var screenAR = width / height;
var boundsAR = _abs(max[0] - min[0]) / _abs(max[1] - min[1]);
var correction;
if (isNaN(boundsAR) || 0 === boundsAR || _min(screenAR, aspectRatio) <= aspectRatio * boundsAR && aspectRatio * boundsAR <= _max(screenAR, aspectRatio)) {
correction = 1
} else {
correction = boundsAR > 1 ? boundsAR : 1 / boundsAR
}
if (aspectRatio * boundsAR >= screenAR) {
this._xRadius = width / 2 / correction;
this._yRadius = width / 2 / (aspectRatio * correction)
} else {
this._xRadius = height / 2 * (aspectRatio / correction);
this._yRadius = height / 2 / correction
}
this._fire("screen")
},
setSize: function(canvas) {
this._canvas = canvas;
this._setupScreen()
},
getCanvas: function() {
return this._canvas
},
_toScreen: function(coordinates) {
return [this._x0 + this._xRadius * coordinates[0], this._y0 + this._yRadius * coordinates[1]]
},
_fromScreen: function(coordinates) {
return [(coordinates[0] - this._x0) / this._xRadius, (coordinates[1] - this._y0) / this._yRadius]
},
_toTransformed: function(coordinates) {
return [coordinates[0] * this._zoom + this._xCenter, coordinates[1] * this._zoom + this._yCenter]
},
_toTransformedFast: function(coordinates) {
return [coordinates[0] * this._zoom, coordinates[1] * this._zoom]
},
_fromTransformed: function(coordinates) {
return [(coordinates[0] - this._xCenter) / this._zoom, (coordinates[1] - this._yCenter) / this._zoom]
},
_adjustCenter: function() {
var center = this._engine.project(this._center);
this._xCenter = -center[0] * this._zoom || 0;
this._yCenter = -center[1] * this._zoom || 0
},
project: function(coordinates) {
return this._engine.project(coordinates)
},
transform: function(coordinates) {
return this._toScreen(this._toTransformedFast(coordinates))
},
isInvertible: function() {
return this._engine.isInvertible()
},
getSquareSize: function(size) {
return [size[0] * this._zoom * this._xRadius, size[1] * this._zoom * this._yRadius]
},
getZoom: function() {
return this._zoom
},
_changeZoom: function(value) {
var oldZoom = this._zoom;
var newZoom = this._zoom = parseAndClamp(value, this._minZoom, this._maxZoom, this._minZoom);
var isChanged = !floatsEqual(oldZoom, newZoom);
if (isChanged) {
this._adjustCenter();
this._fire("zoom")
}
return isChanged
},
setZoom: function(value) {
if (this._engine.isInvertible() && this._changeZoom(value)) {
this._triggerZoomChanged()
}
},
getScaledZoom: function() {
return _round((this._scale.length - 1) * _ln(this._zoom) / _ln(this._maxZoom))
},
setScaledZoom: function(scaledZoom) {
this.setZoom(this._scale[_round(scaledZoom)])
},
changeScaledZoom: function(deltaZoom) {
this.setZoom(this._scale[_max(_min(_round(this.getScaledZoom() + deltaZoom), this._scale.length - 1), 0)])
},
getZoomScalePartition: function() {
return this._scale.length - 1
},
_setupScaling: function() {
var k = _max(_round(TWO_TO_LN2 * _ln(this._maxZoom)), 4);
var step = _pow(this._maxZoom, 1 / k);
var zoom = this._minZoom;
this._scale = [zoom];
for (var i = 1; i <= k; ++i) {
this._scale.push(zoom *= step)
}
},
setMaxZoom: function(maxZoom) {
this._minZoom = DEFAULT_MIN_ZOOM;
this._maxZoom = parseAndClamp(maxZoom, this._minZoom, _Number.MAX_VALUE, DEFAULT_MAX_ZOOM);
this._setupScaling();
if (this._zoom > this._maxZoom) {
this.setZoom(this._maxZoom)
}
this._fire("max-zoom")
},
getCenter: function() {
return this._center.slice()
},
setCenter: function(value) {
if (this._engine.isInvertible() && this._changeCenter(value || [])) {
this._triggerCenterChanged()
}
},
_changeCenter: function(value) {
var engine = this._engine;
var oldCenter = this._center;
var newCenter = this._center = parseAndClampArray(value, engine.min(), engine.max(), engine.center());
var isChanged = !arraysEqual(oldCenter, newCenter);
if (isChanged) {
this._adjustCenter();
this._fire("center")
}
return isChanged
},
_triggerCenterChanged: function() {
this._params.centerChanged(this.getCenter())
},
_triggerZoomChanged: function() {
this._params.zoomChanged(this.getZoom())
},
setCenterByPoint: function(coordinates, screenPosition) {
var p = this._engine.project(coordinates);
var q = this._fromScreen(screenPosition);
this.setCenter(this._engine.unproject([-q[0] / this._zoom + p[0], -q[1] / this._zoom + p[1]]))
},
beginMoveCenter: function() {
if (this._engine.isInvertible()) {
this._moveCenter = this._center
}
},
endMoveCenter: function() {
if (this._moveCenter) {
if (!arraysEqual(this._moveCenter, this._center)) {
this._triggerCenterChanged()
}
this._moveCenter = null
}
},
moveCenter: function(shift) {
if (this._moveCenter) {
var current = this.toScreenPoint(this._center);
this._changeCenter(this.fromScreenPoint([current[0] + shift[0], current[1] + shift[1]]))
}
},
getViewport: function() {
var unproject = this._engine.unproject;
var lt = unproject(this._fromTransformed([-1, -1]));
var lb = unproject(this._fromTransformed([-1, 1]));
var rt = unproject(this._fromTransformed([1, -1]));
var rb = unproject(this._fromTransformed([1, 1]));
var minMax = findMinMax([selectFarthestPoint(lt[0], lb[0], rt[0], rb[0]), selectFarthestPoint(lt[1], rt[1], lb[1], rb[1])], [selectFarthestPoint(rt[0], rb[0], lt[0], lb[0]), selectFarthestPoint(lb[1], rb[1], lt[1], rt[1])]);
return [].concat(minMax.min, minMax.max)
},
setViewport: function(viewport) {
var engine = this._engine;
var data = viewport ? getZoomAndCenterFromViewport(engine.project, engine.unproject, viewport) : [this._minZoom, engine.center()];
this.setZoom(data[0]);
this.setCenter(data[1])
},
getTransform: function() {
return {
translateX: this._xCenter * this._xRadius,
translateY: this._yCenter * this._yRadius
}
},
fromScreenPoint: function(coordinates) {
return this._engine.unproject(this._fromTransformed(this._fromScreen(coordinates)))
},
toScreenPoint: function(coordinates) {
return this._toScreen(this._toTransformed(this._engine.project(coordinates)))
},
_eventNames: ["engine", "screen", "center", "zoom", "max-zoom"]
};
makeEventEmitter(Projection);
function selectFarthestPoint(point1, point2, basePoint1, basePoint2) {
var basePoint = (basePoint1 + basePoint2) / 2;
return _abs(point1 - basePoint) > _abs(point2 - basePoint) ? point1 : point2
}
function selectClosestPoint(point1, point2, basePoint1, basePoint2) {
var basePoint = (basePoint1 + basePoint2) / 2;
return _abs(point1 - basePoint) < _abs(point2 - basePoint) ? point1 : point2
}
function getZoomAndCenterFromViewport(project, unproject, viewport) {
var lt = project([viewport[0], viewport[3]]);
var lb = project([viewport[0], viewport[1]]);
var rt = project([viewport[2], viewport[3]]);
var rb = project([viewport[2], viewport[1]]);
var l = selectClosestPoint(lt[0], lb[0], rt[0], rb[0]);
var r = selectClosestPoint(rt[0], rb[0], lt[0], lb[0]);
var t = selectClosestPoint(lt[1], rt[1], lb[1], rb[1]);
var b = selectClosestPoint(lb[1], rb[1], lt[1], rt[1]);
return [2 / _max(_abs(l - r), _abs(t - b)), unproject([(l + r) / 2, (t + b) / 2])]
}
function setMinMax(engine, p1, p2) {
var {
min: min,
max: max
} = findMinMax(p1, p2);
engine.min = returnArray(min);
engine.max = returnArray(max)
}
var Engine = class {
constructor(parameters) {
var project = createProjectMethod(parameters.to);
var unproject = parameters.from ? createUnprojectMethod(parameters.from) : returnValue(DEFAULT_CENTER);
this.project = project;
this.unproject = unproject;
this.original = returnValue(this);
this.source = function() {
return extend({}, parameters)
};
this.isInvertible = returnValue(!!parameters.from);
this.ar = returnValue(parameters.aspectRatio > 0 ? _Number(parameters.aspectRatio) : 1);
this.center = returnArray(unproject([0, 0]));
setMinMax(this, [unproject([-1, 0])[0], unproject([0, 1])[1]], [unproject([1, 0])[0], unproject([0, -1])[1]])
}
aspectRatio(aspectRatio) {
var engine = new Engine(extend(this.source(), {
aspectRatio: aspectRatio
}));
engine.original = this.original;
engine.min = this.min;
engine.max = this.max;
return engine
}
bounds(bounds) {
bounds = bounds || [];
var parameters = this.source();
var min = this.min();
var max = this.max();
var b1 = parseAndClampArray([bounds[0], bounds[1]], min, max, min);
var b2 = parseAndClampArray([bounds[2], bounds[3]], min, max, max);
var p1 = parameters.to(b1);
var p2 = parameters.to(b2);
var delta = _min(_abs(p2[0] - p1[0]) > MIN_BOUNDS_RANGE ? _abs(p2[0] - p1[0]) : 2, _abs(p2[1] - p1[1]) > MIN_BOUNDS_RANGE ? _abs(p2[1] - p1[1]) : 2);
if (delta < 2) {
extend(parameters, createProjectUnprojectMethods(parameters.to, parameters.from, p1, p2, delta))
}
var engine = new Engine(parameters);
engine.original = this.original;
setMinMax(engine, b1, b2);
return engine
}
};
function invertVerticalAxis(pair) {
return [pair[0], -pair[1]]
}
function createProjectMethod(method) {
return arg => invertVerticalAxis(method(arg))
}
function createUnprojectMethod(method) {
return arg => method(invertVerticalAxis(arg))
}
function returnValue(value) {
return () => value
}
function returnArray(value) {
return () => value.slice()
}
function findMinMax(p1, p2) {
return {
min: [_min(p1[0], p2[0]), _min(p1[1], p2[1])],
max: [_max(p1[0], p2[0]), _max(p1[1], p2[1])]
}
}
export var projection = function(parameters) {
return parameters && parameters.to ? new Engine(parameters) : null
};
var projectionsCache = {};
projection.get = function(name) {
return projectionsCache[name] || null
};
projection.add = function(name, engine) {
engine = engine instanceof Engine && engine || projection(engine);
if (!projectionsCache[name] && engine) {
projectionsCache[name] = engine
}
return projection
};
function createProjectUnprojectMethods(project, unproject, p1, p2, delta) {
var x0 = (p1[0] + p2[0]) / 2 - delta / 2;
var y0 = (p1[1] + p2[1]) / 2 - delta / 2;
var k = 2 / delta;
return {
to: function(coordinates) {
var [p0, p1] = project(coordinates);
return [(p0 - x0) * k - 1, (p1 - y0) * k - 1]
},
from: function(coordinates) {
return unproject([x0 + (coordinates[0] + 1) / k, y0 + (coordinates[1] + 1) / k])
}
}
}