devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
479 lines (405 loc) • 15.8 kB
JavaScript
"use strict";
var extend = require("../../core/utils/extend").extend,
eventEmitterModule = require("./event_emitter");
var _Number = Number,
_min = Math.min,
_max = Math.max,
_abs = Math.abs,
_round = Math.round,
_ln = Math.log,
_pow = Math.pow,
TWO_TO_LN2 = 2 / Math.LN2,
// T224204
// The value is selected so that bounds range of 1 angular second can be defined
// 1 angular second is (1 / 3600) degrees or (1 / 3600 / 180) after projection
// The value 10 times less than projected 1 angular second is chosen
MIN_BOUNDS_RANGE = 1 / 3600 / 180 / 10,
DEFAULT_MIN_ZOOM = 1,
DEFAULT_MAX_ZOOM = 1 << 8,
DEFAULT_CENTER = [NaN, NaN],
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.get(DEFAULT_ENGINE_NAME);
}
function Projection(parameters) {
var that = this;
that._initEvents();
that._params = parameters;
that._engine = getEngine();
that._center = that._engine.center();
that._adjustCenter();
}
Projection.prototype = {
constructor: Projection,
_minZoom: DEFAULT_MIN_ZOOM,
_maxZoom: DEFAULT_MAX_ZOOM,
_zoom: DEFAULT_MIN_ZOOM,
_center: DEFAULT_CENTER,
_canvas: {},
_scale: [],
dispose: function dispose() {
this._disposeEvents();
},
setEngine: function setEngine(value) {
var that = this,
engine = getEngine(value);
if (that._engine !== engine) {
that._engine = engine;
that._fire("engine");
if (that._changeCenter(engine.center())) {
that._triggerCenterChanged();
}
if (that._changeZoom(that._minZoom)) {
that._triggerZoomChanged();
}
that._adjustCenter();
that._setupScreen();
}
},
setBounds: function setBounds(bounds) {
if (bounds !== undefined) {
this.setEngine(this._engine.original().bounds(bounds));
}
},
_setupScreen: function _setupScreen() {
var that = this,
canvas = that._canvas,
width = canvas.width,
height = canvas.height,
aspectRatio = that._engine.ar();
that._x0 = canvas.left + width / 2;
that._y0 = canvas.top + height / 2;
if (width / height <= aspectRatio) {
that._xRadius = width / 2;
that._yRadius = width / 2 / aspectRatio;
} else {
that._xRadius = height / 2 * aspectRatio;
that._yRadius = height / 2;
}
that._fire("screen");
},
setSize: function setSize(canvas) {
var that = this;
that._canvas = canvas;
that._setupScreen();
},
_toScreen: function _toScreen(coordinates) {
return [this._x0 + this._xRadius * coordinates[0], this._y0 + this._yRadius * coordinates[1]];
},
_fromScreen: function _fromScreen(coordinates) {
return [(coordinates[0] - this._x0) / this._xRadius, (coordinates[1] - this._y0) / this._yRadius];
},
_toTransformed: function _toTransformed(coordinates) {
return [coordinates[0] * this._zoom + this._xCenter, coordinates[1] * this._zoom + this._yCenter];
},
_toTransformedFast: function _toTransformedFast(coordinates) {
return [coordinates[0] * this._zoom, coordinates[1] * this._zoom];
},
_fromTransformed: function _fromTransformed(coordinates) {
return [(coordinates[0] - this._xCenter) / this._zoom, (coordinates[1] - this._yCenter) / this._zoom];
},
_adjustCenter: function _adjustCenter() {
var that = this,
center = that._engine.project(that._center);
that._xCenter = -center[0] * that._zoom || 0;
that._yCenter = -center[1] * that._zoom || 0;
},
project: function project(coordinates) {
return this._engine.project(coordinates);
},
transform: function transform(coordinates) {
return this._toScreen(this._toTransformedFast(coordinates));
},
isInvertible: function isInvertible() {
return this._engine.isInvertible();
},
getSquareSize: function getSquareSize(size) {
return [size[0] * this._zoom * this._xRadius, size[1] * this._zoom * this._yRadius];
},
getZoom: function getZoom() {
return this._zoom;
},
_changeZoom: function _changeZoom(value) {
var that = this,
oldZoom = that._zoom,
newZoom = that._zoom = parseAndClamp(value, that._minZoom, that._maxZoom, that._minZoom),
isChanged = !floatsEqual(oldZoom, newZoom);
if (isChanged) {
that._adjustCenter();
that._fire("zoom");
}
return isChanged;
},
setZoom: function setZoom(value) {
if (this._engine.isInvertible() && this._changeZoom(value)) {
this._triggerZoomChanged();
}
},
getScaledZoom: function getScaledZoom() {
return _round((this._scale.length - 1) * _ln(this._zoom) / _ln(this._maxZoom));
},
setScaledZoom: function setScaledZoom(scaledZoom) {
this.setZoom(this._scale[_round(scaledZoom)]);
},
changeScaledZoom: function changeScaledZoom(deltaZoom) {
this.setZoom(this._scale[_max(_min(_round(this.getScaledZoom() + deltaZoom), this._scale.length - 1), 0)]);
},
getZoomScalePartition: function getZoomScalePartition() {
return this._scale.length - 1;
},
_setupScaling: function _setupScaling() {
var that = this,
k = _round(TWO_TO_LN2 * _ln(that._maxZoom)),
step,
zoom,
i = 1;
k = k > 4 ? k : 4;
step = _pow(that._maxZoom, 1 / k);
zoom = that._minZoom;
that._scale = [zoom];
for (; i <= k; ++i) {
that._scale.push(zoom *= step);
}
},
setMaxZoom: function setMaxZoom(maxZoom) {
var that = this;
that._minZoom = DEFAULT_MIN_ZOOM;
that._maxZoom = parseAndClamp(maxZoom, that._minZoom, _Number.MAX_VALUE, DEFAULT_MAX_ZOOM);
that._setupScaling();
if (that._zoom > that._maxZoom) {
that.setZoom(that._maxZoom);
}
that._fire("max-zoom");
},
getCenter: function getCenter() {
return this._center.slice();
},
setCenter: function setCenter(value) {
if (this._engine.isInvertible() && this._changeCenter(value || [])) {
this._triggerCenterChanged();
}
},
_changeCenter: function _changeCenter(value) {
var that = this,
engine = that._engine,
oldCenter = that._center,
newCenter = that._center = parseAndClampArray(value, engine.min(), engine.max(), engine.center()),
isChanged = !arraysEqual(oldCenter, newCenter);
if (isChanged) {
that._adjustCenter();
that._fire("center");
}
return isChanged;
},
_triggerCenterChanged: function _triggerCenterChanged() {
this._params.centerChanged(this.getCenter());
},
_triggerZoomChanged: function _triggerZoomChanged() {
this._params.zoomChanged(this.getZoom());
},
setCenterByPoint: function setCenterByPoint(coordinates, screenPosition) {
var that = this,
p = that._engine.project(coordinates),
q = that._fromScreen(screenPosition);
that.setCenter(that._engine.unproject([-q[0] / that._zoom + p[0], -q[1] / that._zoom + p[1]]));
},
beginMoveCenter: function beginMoveCenter() {
if (this._engine.isInvertible()) {
this._moveCenter = this._center;
}
},
endMoveCenter: function endMoveCenter() {
var that = this;
if (that._moveCenter) {
if (!arraysEqual(that._moveCenter, that._center)) {
that._triggerCenterChanged();
}
that._moveCenter = null;
}
},
moveCenter: function moveCenter(shift) {
var that = this,
current,
center;
if (that._moveCenter) {
current = that._toScreen(that._toTransformed(that._engine.project(that._center)));
center = that._engine.unproject(that._fromTransformed(that._fromScreen([current[0] + shift[0], current[1] + shift[1]])));
that._changeCenter(center);
}
},
getViewport: function getViewport() {
var that = this,
unproject = that._engine.unproject,
lt = unproject(that._fromTransformed([-1, -1])),
lb = unproject(that._fromTransformed([-1, +1])),
rt = unproject(that._fromTransformed([+1, -1])),
rb = unproject(that._fromTransformed([+1, +1])),
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);
},
// T254127
// There should be no expectation that if viewport is got with `getViewport` and set with `setViewport`
// then center and zoom will be retained - in general case they will be not.
// Such retaining requires invertibility of projection which is generally not available
// Invertibility means that `project(unproject([x, y])) === [x, y]` and `unproject(project([x, y])) === [x, y]` for any reasonable `(x, y)`
// For example:
// the "mercator" is non invertible - longitude is invertible, latitude is not (because of tan and log)
// the "equirectangular" is invertible (it uses simple linear transformations)
setViewport: function setViewport(viewport) {
var engine = this._engine,
data = viewport ? getZoomAndCenterFromViewport(engine.project, engine.unproject, viewport) : [this._minZoom, engine.center()];
this.setZoom(data[0]);
this.setCenter(data[1]);
},
getTransform: function getTransform() {
return { translateX: this._xCenter * this._xRadius, translateY: this._yCenter * this._yRadius };
},
fromScreenPoint: function fromScreenPoint(coordinates) {
return this._engine.unproject(this._fromTransformed(this._fromScreen(coordinates)));
},
_eventNames: ["engine", "screen", "center", "zoom", "max-zoom"]
};
eventEmitterModule.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]]),
lb = project([viewport[0], viewport[1]]),
rt = project([viewport[2], viewport[3]]),
rb = project([viewport[2], viewport[1]]),
l = selectClosestPoint(lt[0], lb[0], rt[0], rb[0]),
r = selectClosestPoint(rt[0], rb[0], lt[0], lb[0]),
t = selectClosestPoint(lt[1], rt[1], lb[1], rb[1]),
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 minMax = findMinMax(p1, p2);
engine.min = returnArray(minMax.min);
engine.max = returnArray(minMax.max);
}
function Engine(parameters) {
var that = this,
project = createProjectMethod(parameters.to),
unproject = parameters.from ? createUnprojectMethod(parameters.from) : returnValue(DEFAULT_CENTER);
that.project = project;
that.unproject = unproject;
that.original = returnValue(that);
that.source = function () {
return extend({}, parameters);
};
that.isInvertible = returnValue(!!parameters.from);
that.ar = returnValue(parameters.aspectRatio > 0 ? _Number(parameters.aspectRatio) : 1);
that.center = returnArray(unproject([0, 0]));
setMinMax(that, [unproject([-1, 0])[0], unproject([0, +1])[1]], [unproject([+1, 0])[0], unproject([0, -1])[1]]);
}
Engine.prototype.aspectRatio = function (aspectRatio) {
var engine = new Engine(extend(this.source(), { aspectRatio: aspectRatio }));
engine.original = this.original;
engine.min = this.min;
engine.max = this.max;
return engine;
};
Engine.prototype.bounds = function (bounds) {
bounds = bounds || [];
var parameters = this.source(),
min = this.min(),
max = this.max(),
b1 = parseAndClampArray([bounds[0], bounds[1]], min, max, min),
b2 = parseAndClampArray([bounds[2], bounds[3]], min, max, max),
p1 = parameters.to(b1),
p2 = parameters.to(b2),
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),
engine;
if (delta < 2) {
extend(parameters, createProjectUnprojectMethods(parameters.to, parameters.from, p1, p2, delta));
}
engine = new Engine(parameters);
engine.original = this.original;
setMinMax(engine, b1, b2);
return engine;
};
function isEngine(engine) {
return engine instanceof Engine;
}
function invertVerticalAxis(pair) {
return [pair[0], -pair[1]];
}
function createProjectMethod(method) {
return function (arg) {
return invertVerticalAxis(method(arg));
};
}
function createUnprojectMethod(method) {
return function (arg) {
return method(invertVerticalAxis(arg));
};
}
function returnValue(value) {
return function () {
return value;
};
}
function returnArray(value) {
return function () {
return value.slice();
};
}
function projection(parameters) {
return parameters && parameters.to ? new Engine(parameters) : null;
}
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])]
};
}
var projectionsCache = {};
projection.get = function (name) {
return projectionsCache[name] || null;
};
projection.add = function (name, engine) {
if (!projectionsCache[name] && isEngine(engine)) {
projectionsCache[name] = engine;
}
return projection; // For chaining
};
function createProjectUnprojectMethods(project, unproject, p1, p2, delta) {
var x0 = (p1[0] + p2[0]) / 2 - delta / 2,
y0 = (p1[1] + p2[1]) / 2 - delta / 2,
k = 2 / delta;
return {
to: function to(coordinates) {
var p = project(coordinates);
return [-1 + (p[0] - x0) * k, -1 + (p[1] - y0) * k];
},
from: function from(coordinates) {
var p = [x0 + (coordinates[0] + 1) / k, y0 + (coordinates[1] + 1) / k];
return unproject(p);
}
};
}
exports.Projection = Projection;
exports.projection = projection;
///#DEBUG
exports._TESTS_Engine = Engine;
///#ENDDEBUG