route360
Version:
The Route360° JavaScript API for Motion Intelligence's time-based access mapping service.
1,551 lines (1,238 loc) • 164 kB
JavaScript
/*
Route360° JavaScript API v0.5.3 (a575c9e), a JS library for leaflet.js and google maps API. http://route360.net
(c) 2017 Henning Hollburg, Daniel Gerber and Jan Silbersiepe, (c) 2017 Motion Intelligence GmbH
*/
(function (window, document, undefined) {
var r360 = {
version : 'v0.5.3',
// Is a given variable undefined?
isUndefined : function(obj) {
return obj === void 0;
},
// Shortcut function for checking if an object has a given property directly
// on itself (in other words, not on a prototype).
has : function(obj, key) {
return obj != null && hasOwnProperty.call(obj, key);
},
// is a given object a function
isFunction : function(obj) {
return typeof obj == 'function' || false;
},
findWhere : function(array, attr) {
var result = undefined;
array.some(function(elem,index,array){
var match = false;
for(var index in attr) {
match = (r360.has(elem,index) && elem[index] === attr[index]) ? true : false;
}
if (match) {
result = elem;
return true;
}
});
return result;
},
filter : function(array,predicate) {
var results = [];
array.forEach(function(elem,index,array){
if (predicate(elem, index, array)) results.push(elem);
});
return results;
},
contains : function(array,item) {
return array.indexOf(item) > -1;
},
each : function(array,cb) {
array.forEach(function(elem,index,array){
cb(elem,index,array);
});
},
max : function(array, iteratee, context) {
var result = -Infinity, lastComputed = -Infinity,
value, computed;
if (iteratee == null || (typeof iteratee == 'number' && typeof array[0] != 'arrayect') && array != null) {
for (var i = 0, length = array.length; i < length; i++) {
value = array[i];
if (value != null && value > result) {
result = value;
}
}
} else {
r360.each(array, function(elem, index, array) {
computed = iteratee(elem, index, array);
if (computed > lastComputed || computed === -Infinity && result === -Infinity) {
result = elem;
lastComputed = computed;
}
});
}
return result;
},
keys : function(obj) {
if (typeof obj !== 'Object') return [];
if (Object.keys(obj)) return Object.keys(obj);
var keys = [];
for (var key in obj) if (r360.has(obj, key)) keys.push(key);
return keys;
}
};
function expose() {
var oldr360 = window.r360;
r360.noConflict = function () {
window.r360 = oldr360;
return this;
};
window.r360 = r360;
}
// define r360 for Node module pattern loaders, including Browserify
if (typeof module === 'object' && typeof module.exports === 'object')
module.exports = r360;
// define r360 as an AMD module
else if (typeof define === 'function' && define.amd) define(r360);
// define r360 as a global r360 variable, saving the original r360 to restore later if needed
else expose();
/*
* IE 8 does not get the bind function. This is a workaround...
*/
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5 internal IsCallable function
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis
? this
: oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
r360.config = {
serviceVersion : 'v1',
pathSerializer : 'compact',
requestTimeout : 10000,
maxRoutingTime : 3600,
maxRoutingLength : 100000,
bikeSpeed : 15,
bikeUphill : 20,
bikeDownhill : -10,
walkSpeed : 5,
walkUphill : 10,
walkDownhill : 0,
travelTimes : [300, 600, 900, 1200, 1500, 1800],
travelType : "walk",
logging : false,
rushHour : false,
// options for the travel time slider; colors and lengths etc.
defaultTravelTimeControlOptions : {
travelTimes : [
{ time : 300 , color : "#006837", opacity : 0.1 },
{ time : 600 , color : "#39B54A", opacity : 0.2 },
{ time : 900 , color : "#8CC63F", opacity : 0.3 },
{ time : 1200 , color : "#F7931E", opacity : 0.4 },
{ time : 1500 , color : "#F15A24", opacity : 0.5 },
{ time : 1800 , color : "#C1272D", opacity : 1.0 }
],
position : 'topright',
label: 'travel time',
initValue: 30
},
routeTypes : [
// non transit
{ routeType : 'WALK' , color : "red", haloColor : "white"},
{ routeType : 'BIKE' , color : "#558D54", haloColor : "white"},
{ routeType : 'CAR' , color : "#558D54", haloColor : "white"},
{ routeType : 'TRANSFER' , color : "#C1272D", haloColor : "white"},
// berlin
{ routeType : 102 , color : "#006837", haloColor : "white" },
{ routeType : 400 , color : "#156ab8", haloColor : "white" },
{ routeType : 900 , color : "red", haloColor : "white" },
{ routeType : 700 , color : "#A3007C", haloColor : "white" },
{ routeType : 1000 , color : "blue", haloColor : "white" },
{ routeType : 109 , color : "#006F35", haloColor : "white" },
{ routeType : 100 , color : "red", haloColor : "white" },
// new york
{ routeType : 1 , color : "red", haloColor : "red"},
{ routeType : 2 , color : "blue", haloColor : "blue"},
{ routeType : 3 , color : "yellow", haloColor : "yellow"},
{ routeType : 0 , color : "green", haloColor : "green"},
{ routeType : 4 , color : "orange", haloColor : "orange"},
{ routeType : 5 , color : "red", haloColor : "red"},
{ routeType : 6 , color : "blue", haloColor : "blue"},
{ routeType : 7 , color : "yellow", haloColor : "yellow" }
],
photonPlaceAutoCompleteOptions : {
serviceUrl : "https://service.route360.net/geocode/",
position : 'topleft',
reset : false,
reverse : false,
placeholder : 'Select source',
maxRows : 5,
width : 300
},
defaultRadioOptions: {
position : 'topright',
},
// configuration for the Route360PolygonLayer
defaultPolygonLayerOptions:{
opacity : 0.4,
strokeWidth: 30,
tolerance: 15,
// background values only matter if inverse = true
backgroundColor : 'black',
backgroundOpacity : 0.5,
inverse : false,
animate : false,
animationDuration : 1
}
}
/*
* r360.Bounds represents a rectangular area on the screen in pixel coordinates.
*/
r360.Bounds = function (a, b) { //(Point, Point) or Point[]
if (!a) { return; }
var points = b ? [a, b] : a;
for (var i = 0, len = points.length; i < len; i++) {
this.extend(points[i]);
}
};
r360.Bounds.prototype = {
// extend the bounds to contain the given point
extend: function (point) { // (Point)
point = r360.point(point);
if (!this.min && !this.max) {
this.min = point.clone();
this.max = point.clone();
} else {
this.min.x = Math.min(point.x, this.min.x);
this.max.x = Math.max(point.x, this.max.x);
this.min.y = Math.min(point.y, this.min.y);
this.max.y = Math.max(point.y, this.max.y);
}
return this;
},
getCenter: function (round) { // (Boolean) -> Point
return new r360.Point(
(this.min.x + this.max.x) / 2,
(this.min.y + this.max.y) / 2, round);
},
getBottomLeft: function () { // -> Point
return new r360.Point(this.min.x, this.max.y);
},
getTopRight: function () { // -> Point
return new r360.Point(this.max.x, this.min.y);
},
getSize: function () {
return this.max.subtract(this.min);
},
contains: function (obj) { // (Bounds) or (Point) -> Boolean
var min, max;
if (typeof obj[0] === 'number' || obj instanceof r360.Point) {
obj = r360.point(obj);
} else {
obj = r360.bounds(obj);
}
if (obj instanceof r360.Bounds) {
min = obj.min;
max = obj.max;
} else {
min = max = obj;
}
return (min.x >= this.min.x) &&
(max.x <= this.max.x) &&
(min.y >= this.min.y) &&
(max.y <= this.max.y);
},
intersects: function (bounds) { // (Bounds) -> Boolean
bounds = r360.bounds(bounds);
var min = this.min,
max = this.max,
min2 = bounds.min,
max2 = bounds.max,
xIntersects = (max2.x >= min.x) && (min2.x <= max.x),
yIntersects = (max2.y >= min.y) && (min2.y <= max.y);
return xIntersects && yIntersects;
},
overlaps: function (bounds) { // (Bounds) -> Boolean
bounds = r360.bounds(bounds);
var min = this.min,
max = this.max,
min2 = bounds.min,
max2 = bounds.max,
xOverlaps = (max2.x > min.x) && (min2.x < max.x),
yOverlaps = (max2.y > min.y) && (min2.y < max.y);
return xOverlaps && yOverlaps;
},
isValid: function () {
return !!(this.min && this.max);
}
};
r360.bounds = function (a, b) { // (Bounds) or (Point, Point) or (Point[])
if (!a || a instanceof r360.Bounds) {
return a;
}
return new r360.Bounds(a, b);
};
/*
* r360.LatLngBounds represents a rectangular area on the map in geographical coordinates.
*/
r360.LatLngBounds = function (southWest, northEast) { // (LatLng, LatLng) or (LatLng[])
if (!southWest) { return; }
var latlngs = northEast ? [southWest, northEast] : southWest;
for (var i = 0, len = latlngs.length; i < len; i++) {
this.extend(latlngs[i]);
}
};
r360.LatLngBounds.prototype = {
// extend the bounds to contain the given point or bounds
extend: function (obj) { // (LatLng) or (LatLngBounds)
var sw = this._southWest,
ne = this._northEast,
sw2, ne2;
if (obj instanceof r360.LatLng) {
sw2 = obj;
ne2 = obj;
} else if (obj instanceof r360.LatLngBounds) {
sw2 = obj._southWest;
ne2 = obj._northEast;
if (!sw2 || !ne2) { return this; }
} else {
return obj ? this.extend(r360.latLng(obj) || r360.latLngBounds(obj)) : this;
}
if (!sw && !ne) {
this._southWest = new r360.LatLng(sw2.lat, sw2.lng);
this._northEast = new r360.LatLng(ne2.lat, ne2.lng);
} else {
sw.lat = Math.min(sw2.lat, sw.lat);
sw.lng = Math.min(sw2.lng, sw.lng);
ne.lat = Math.max(ne2.lat, ne.lat);
ne.lng = Math.max(ne2.lng, ne.lng);
}
return this;
},
// extend the bounds by a percentage
pad: function (bufferRatio) { // (Number) -> LatLngBounds
var sw = this._southWest,
ne = this._northEast,
heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio,
widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio;
return new r360.LatLngBounds(
new r360.LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer),
new r360.LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer));
},
getCenter: function () { // -> LatLng
return new r360.LatLng(
(this._southWest.lat + this._northEast.lat) / 2,
(this._southWest.lng + this._northEast.lng) / 2);
},
getSouthWest: function () {
return this._southWest;
},
getNorthEast: function () {
return this._northEast;
},
getNorthWest: function () {
return new r360.LatLng(this.getNorth(), this.getWest());
},
getSouthEast: function () {
return new r360.LatLng(this.getSouth(), this.getEast());
},
getWest: function () {
return this._southWest.lng;
},
getSouth: function () {
return this._southWest.lat;
},
getEast: function () {
return this._northEast.lng;
},
getNorth: function () {
return this._northEast.lat;
},
contains: function (obj) { // (LatLngBounds) or (LatLng) -> Boolean
if (typeof obj[0] === 'number' || obj instanceof r360.LatLng) {
obj = r360.latLng(obj);
} else {
obj = r360.latLngBounds(obj);
}
var sw = this._southWest,
ne = this._northEast,
sw2, ne2;
if (obj instanceof r360.LatLngBounds) {
sw2 = obj.getSouthWest();
ne2 = obj.getNorthEast();
} else {
sw2 = ne2 = obj;
}
return (sw2.lat >= sw.lat) && (ne2.lat <= ne.lat) &&
(sw2.lng >= sw.lng) && (ne2.lng <= ne.lng);
},
intersects: function (bounds) { // (LatLngBounds) -> Boolean
bounds = r360.latLngBounds(bounds);
var sw = this._southWest,
ne = this._northEast,
sw2 = bounds.getSouthWest(),
ne2 = bounds.getNorthEast(),
latIntersects = (ne2.lat >= sw.lat) && (sw2.lat <= ne.lat),
lngIntersects = (ne2.lng >= sw.lng) && (sw2.lng <= ne.lng);
return latIntersects && lngIntersects;
},
overlaps: function (bounds) { // (LatLngBounds) -> Boolean
bounds = r360.latLngBounds(bounds);
var sw = this._southWest,
ne = this._northEast,
sw2 = bounds.getSouthWest(),
ne2 = bounds.getNorthEast(),
latOverlaps = (ne2.lat > sw.lat) && (sw2.lat < ne.lat),
lngOverlaps = (ne2.lng > sw.lng) && (sw2.lng < ne.lng);
return latOverlaps && lngOverlaps;
},
toBBoxString: function () {
return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()].join(',');
},
equals: function (bounds) { // (LatLngBounds)
if (!bounds) { return false; }
bounds = r360.latLngBounds(bounds);
return this._southWest.equals(bounds.getSouthWest()) &&
this._northEast.equals(bounds.getNorthEast());
},
isValid: function () {
return !!(this._southWest && this._northEast);
}
};
//TODO International date line?
r360.latLngBounds = function (a, b) { // (LatLngBounds) or (LatLng, LatLng)
if (!a || a instanceof r360.LatLngBounds) {
return a;
}
return new r360.LatLngBounds(a, b);
};
/*
* r360.Point represents a point with x and y coordinates.
*/
r360.Point = function (x, y, round) {
this.x = (round ? Math.round(x) : x);
this.y = (round ? Math.round(y) : y);
};
r360.Point.prototype = {
clone: function () {
return new r360.Point(this.x, this.y);
},
// non-destructive, returns a new point
add: function (point) {
return this.clone()._add(r360.point(point));
},
// destructive, used directly for performance in situations where it's safe to modify existing point
_add: function (point) {
this.x += point.x;
this.y += point.y;
return this;
},
subtract: function (point) {
return this.clone()._subtract(r360.point(point));
},
_subtract: function (point) {
this.x -= point.x;
this.y -= point.y;
return this;
},
divideBy: function (num) {
return this.clone()._divideBy(num);
},
_divideBy: function (num) {
this.x /= num;
this.y /= num;
return this;
},
multiplyBy: function (num) {
return this.clone()._multiplyBy(num);
},
_multiplyBy: function (num) {
this.x *= num;
this.y *= num;
return this;
},
round: function () {
return this.clone()._round();
},
_round: function () {
this.x = Math.round(this.x);
this.y = Math.round(this.y);
return this;
},
floor: function () {
return this.clone()._floor();
},
_floor: function () {
this.x = Math.floor(this.x);
this.y = Math.floor(this.y);
return this;
},
ceil: function () {
return this.clone()._ceil();
},
_ceil: function () {
this.x = Math.ceil(this.x);
this.y = Math.ceil(this.y);
return this;
},
distanceTo: function (point) {
point = r360.point(point);
var x = point.x - this.x,
y = point.y - this.y;
return Math.sqrt(x * x + y * y);
},
equals: function (point) {
point = r360.point(point);
return point.x === this.x &&
point.y === this.y;
},
contains: function (point) {
point = r360.point(point);
return Math.abs(point.x) <= Math.abs(this.x) &&
Math.abs(point.y) <= Math.abs(this.y);
},
toString: function () {
return 'Point(' +
r360.Util.formatNum(this.x) + ', ' +
r360.Util.formatNum(this.y) + ')';
}
};
r360.point = function (x, y, round) {
if (x instanceof r360.Point) {
return x;
}
if (r360.Util.isArray(x)) {
return new r360.Point(x[0], x[1]);
}
if (x === undefined || x === null) {
return x;
}
return new r360.Point(x, y, round);
};
/*
* r360.LatLng represents a geographical point with latitude and longitude coordinates.
*/
r360.LatLng = function (lat, lng, alt) {
if (isNaN(lat) || isNaN(lng)) {
throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')');
}
this.lat = +lat;
this.lng = +lng;
if (alt !== undefined) {
this.alt = +alt;
}
};
r360.LatLng.prototype = {
equals: function (obj, maxMargin) {
if (!obj) { return false; }
obj = r360.latLng(obj);
var margin = Math.max(
Math.abs(this.lat - obj.lat),
Math.abs(this.lng - obj.lng));
return margin <= (maxMargin === undefined ? 1.0E-9 : maxMargin);
},
toString: function (precision) {
return 'LatLng(' +
r360.Util.formatNum(this.lat, precision) + ', ' +
r360.Util.formatNum(this.lng, precision) + ')';
},
distanceTo: function (other) {
return r360.CRS.Earth.distance(this, r360.latLng(other));
},
wrap: function () {
return r360.CRS.Earth.wrapLatLng(this);
},
toBounds: function (sizeInMeters) {
var latAccuracy = 180 * sizeInMeters / 40075017,
lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * this.lat);
return r360.latLngBounds(
[this.lat - latAccuracy, this.lng - lngAccuracy],
[this.lat + latAccuracy, this.lng + lngAccuracy]);
},
clone: function () {
return new r360.LatLng(this.lat, this.lng, this.alt);
}
};
// constructs LatLng with different signatures
// (LatLng) or ([Number, Number]) or (Number, Number) or (Object)
r360.latLng = function (a, b, c) {
if (a instanceof r360.LatLng) {
return a;
}
if (r360.Util.isArray(a) && typeof a[0] !== 'object') {
if (a.length === 3) {
return new r360.LatLng(a[0], a[1], a[2]);
}
if (a.length === 2) {
return new r360.LatLng(a[0], a[1]);
}
return null;
}
if (a === undefined || a === null) {
return a;
}
if (typeof a === 'object' && 'lat' in a) {
return new r360.LatLng(a.lat, 'lng' in a ? a.lng : a.lon, a.alt);
}
if (b === undefined) {
return null;
}
return new r360.LatLng(a, b, c);
};
/*
* r360.Browser handles different browser and feature detections for internal Leaflet use.
*/
(function () {
var ua = navigator.userAgent.toLowerCase(),
doc = document.documentElement,
ie = 'ActiveXObject' in window,
webkit = ua.indexOf('webkit') !== -1,
phantomjs = ua.indexOf('phantom') !== -1,
android23 = ua.search('android [23]') !== -1,
chrome = ua.indexOf('chrome') !== -1,
gecko = ua.indexOf('gecko') !== -1 && !webkit && !window.opera && !ie,
mobile = typeof orientation !== 'undefined' || ua.indexOf('mobile') !== -1,
msPointer = navigator.msPointerEnabled && navigator.msMaxTouchPoints && !window.PointerEvent,
pointer = (window.PointerEvent && navigator.pointerEnabled && navigator.maxTouchPoints) || msPointer,
ie3d = ie && ('transition' in doc.style),
webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23,
gecko3d = 'MozPerspective' in doc.style,
opera12 = 'OTransition' in doc.style;
var touch = !window.L_NO_TOUCH && !phantomjs && (pointer || 'ontouchstart' in window ||
(window.DocumentTouch && document instanceof window.DocumentTouch));
r360.Browser = {
ie: ie,
ielt9: ie && !document.addEventListener,
webkit: webkit,
gecko: gecko,
android: ua.indexOf('android') !== -1,
android23: android23,
chrome: chrome,
safari: !chrome && ua.indexOf('safari') !== -1,
ie3d: ie3d,
webkit3d: webkit3d,
gecko3d: gecko3d,
opera12: opera12,
any3d: !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d) && !opera12 && !phantomjs,
mobile: mobile,
mobileWebkit: mobile && webkit,
mobileWebkit3d: mobile && webkit3d,
mobileOpera: mobile && window.opera,
mobileGecko: mobile && gecko,
touch: !!touch,
msPointer: !!msPointer,
pointer: !!pointer,
retina: (window.devicePixelRatio || (window.screen.deviceXDPI / window.screen.logicalXDPI)) > 1
};
}());
/*
* r360.Class powers the OOP facilities of the library.
* Thanks to John Resig and Dean Edwards for inspiration!
*/
r360.Class = function () {};
r360.Class.extend = function (props) {
// extended class with the new prototype
var NewClass = function () {
// call the constructor
if (this.initialize) {
this.initialize.apply(this, arguments);
}
// call all constructor hooks
this.callInitHooks();
};
var parentProto = NewClass.__super__ = this.prototype;
var proto = r360.Util.create(parentProto);
proto.constructor = NewClass;
NewClass.prototype = proto;
// inherit parent's statics
for (var i in this) {
if (this.hasOwnProperty(i) && i !== 'prototype') {
NewClass[i] = this[i];
}
}
// mix static properties into the class
if (props.statics) {
r360.extend(NewClass, props.statics);
delete props.statics;
}
// mix includes into the prototype
if (props.includes) {
r360.Util.extend.apply(null, [proto].concat(props.includes));
delete props.includes;
}
// merge options
if (proto.options) {
props.options = r360.Util.extend(r360.Util.create(proto.options), props.options);
}
// mix given properties into the prototype
r360.extend(proto, props);
proto._initHooks = [];
// add method for calling all hooks
proto.callInitHooks = function () {
if (this._initHooksCalled) { return; }
if (parentProto.callInitHooks) {
parentProto.callInitHooks.call(this);
}
this._initHooksCalled = true;
for (var i = 0, len = proto._initHooks.length; i < len; i++) {
proto._initHooks[i].call(this);
}
};
return NewClass;
};
// method for adding properties to prototype
r360.Class.include = function (props) {
r360.extend(this.prototype, props);
};
// merge new default options to the Class
r360.Class.mergeOptions = function (options) {
r360.extend(this.prototype.options, options);
};
// add a constructor hook
r360.Class.addInitHook = function (fn) { // (Function) || (String, args...)
var args = Array.prototype.slice.call(arguments, 1);
var init = typeof fn === 'function' ? fn : function () {
this[fn].apply(this, args);
};
this.prototype._initHooks = this.prototype._initHooks || [];
this.prototype._initHooks.push(init);
};
r360.PolygonUtil = {
/**
* [clip clipping like sutherland http://rosettacode.org/wiki/Sutherland-Hodgman_polygon_clipping#JavaScript]
* @param {type} subjectPolygon [description]
* @param {type} clipPolygon [description]
* @return {type} [description]
*/
clip: function(subjectPolygon, clipPolygon) {
var cp1, cp2, s, e;
var inside = function (p) {
return (cp2[0]-cp1[0])*(p[1]-cp1[1]) > (cp2[1]-cp1[1])*(p[0]-cp1[0]);
};
var intersection = function () {
var dc = [ cp1[0] - cp2[0], cp1[1] - cp2[1] ],
dp = [ s[0] - e[0], s[1] - e[1] ],
n1 = cp1[0] * cp2[1] - cp1[1] * cp2[0],
n2 = s[0] * e[1] - s[1] * e[0],
n3 = 1.0 / (dc[0] * dp[1] - dc[1] * dp[0]);
return [(n1*dp[0] - n2*dc[0]) * n3, (n1*dp[1] - n2*dc[1]) * n3];
};
var outputList = subjectPolygon;
var cp1 = clipPolygon[clipPolygon.length-1];
for (j in clipPolygon) {
var cp2 = clipPolygon[j];
var inputList = outputList;
outputList = [];
s = inputList[inputList.length - 1]; //last on the input list
for (i in inputList) {
var e = inputList[i];
if (inside(e)) {
if (!inside(s)) {
outputList.push(intersection());
}
outputList.push(e);
}
else if (inside(s)) {
outputList.push(intersection());
}
s = e;
}
cp1 = cp2;
}
return outputList
},
/**
* [isCollinear Checks if the given three points are collinear. Also see
* https://en.wikipedia.org/wiki/Collinearity. This method uses a tolerance
* factor defined in r360.config.defaultPolygonLayerOptions.tolerance.]
*
* @param {type} p1 [description]
* @param {type} p2 [description]
* @param {type} p3 [description]
* @return {Boolean} [description]
*/
isCollinear: function(p1, p2, p3){
if(p1.x == p3.x && p1.y == p3.y)
return false;
if(p1.x == p2.x && p2.x == p3.x)
return true;
if(p1.y == p2.y && p2.y == p3.y)
return true;
var val = (p1.x * (p2.y -p3.y) + p2.x * (p3.y - p1.y) + p3.x * (p1.y - p2.y));
if ( val < r360.config.defaultPolygonLayerOptions.tolerance &&
val > -r360.config.defaultPolygonLayerOptions.tolerance &&
p1.x != p3.x && p1.y != p3.y )
return true;
return false;
},
/**
* [scale Scales a point (x and y coordinate) by the given scale. This method changes
* the values of the given point.]
* @param {type} point [the point to be scaled]
* @param {type} scale [the scale]
* @return {type} [the scaled point]
*/
scale: function(point, scale){
return r360.point(point.x * scale, point.y * scale);
},
/**
* [subtract Subtracts the given x and y coordinate from the cooresponding values of the given point.
* This method changes the values of the given point. ]
* @param {type} point [the point to be changed]
* @param {type} x [the x value to be subtracted]
* @param {type} y [the y value to be subtracted]
* @return {type} [the subtracted point]
*/
subtract: function(point, x, y){
return r360.point(point.x - x, point.y - y);
},
divide: function(point, quotient){
return r360.point(point.x / quotient, point.y / quotient);
},
/**
* [roundPoint Rounds a point's x and y coordinate. The method changes the x and y
* values of the given point. If the fractional portion of number (x and y)
* is 0.5 or greater, the argument is rounded to the next higher integer. If the
* fractional portion of number is less than 0.5, the argument is rounded to the
* next lower integer.]
*
* @param {type} point [the point to rounded]
* @return {type} [the point to be rounded with integer x and y coordinate]
*/
roundPoint: function(point){
point.x = Math.round(point.x);
point.y = Math.round(point.y);
return point;
},
/**
* [buildPath Creates an SVG path. ]
* @param {type} point [The point to add]
* @param {type} suffix [The svg suffix for the point]
* @return {type} [An array containing the suffix, point.x, point.y]
*/
buildPath:function(point, suffix){
return [suffix, Math.round(point.x), Math.round(point.y)];
},
/**
* [getEuclidianDistance This method returns the euclidean distance between two points (x and y coordinates).]
* @param {type} point1 [the first point]
* @param {type} point2 [the second point]
* @return {type} [the distance]
*/
getEuclidianDistance: function(point1, point2){
return Math.sqrt(Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2));
},
/**
* [getSvgFrame description]
* @param {type} width [description]
* @param {type} height [description]
* @return {type} [description]
*/
getSvgFrame: function(width, height){
return [['M',0, 0], ['L',width, 0], ['L',width, height], ['L',0, height], ['z']];
},
/**
* [extendBounds description]
* @param {type} bounds [description]
* @param {type} extendWidthX [description]
* @param {type} extendWidthY [description]
* @return {type} [description]
*/
extendBounds : function(bounds, extendWidthX, extendWidthY) {
var extendX = Math.ceil(extendWidthX);
var extendY = Math.ceil(extendWidthY);
bounds.max.x += extendX;
bounds.min.x -= extendX;
bounds.max.y += extendY;
bounds.min.y -= extendY;
return bounds;
},
/*
*
*/
addPolygonToMultiPolygon: function(multiPolygons, polygon){
var filteredMultiPolygons = r360.filter(multiPolygons, function(multiPolygon){ return multiPolygon.getTravelTime() == polygon.travelTime; });
// multipolygon with polygon's travetime already there
if ( filteredMultiPolygons.length > 0 ) filteredMultiPolygons[0].addPolygon(polygon);
else {
var multiPolygon = new r360.multiPolygon();
multiPolygon.setTravelTime(polygon.travelTime);
multiPolygon.addPolygon(polygon);
multiPolygon.setColor(polygon.getColor());
multiPolygon.setOpacity(polygon.getOpacity());
multiPolygons.push(multiPolygon);
}
},
}
r360.SvgUtil = {
/**
* [getGElement description]
* @param {type} svgData [description]
* @param {type} opacity [description]
* @param {type} color [description]
* @param {type} animate [description]
* @return {type} [description]
*/
getGElement : function(svgData, options){
var randomId = r360.Util.generateId();
var initialOpacity = options.opacity;
return "<g id=" + randomId + " style='opacity:" + initialOpacity + "'>"+
"<path style='stroke: " + options.color + "; fill: " + options.color + " ; stroke-opacity: 1; stroke-width: " + options.strokeWidth + "; fill-opacity:1'd='" + svgData.toString().replace(/\,/g, ' ') + "'/>"+
"</g>";
},
/**
* [getInverseSvgElement description]
* @param {type} gElements [description]
* @return {type} [description]
*/
getInverseSvgElement: function(gElements, options){
var svgFrame = r360.PolygonUtil.getSvgFrame(options.svgWidth, options.svgHeight);
var svgStart = "<div id=svg_"+ options.id + " style='" + r360.Util.getTranslation(options.offset) + ";''><svg" +
" height=" + options.svgHeight +
" width=" + options.svgWidth +
" style='fill:" + options.backgroundColor + " ; opacity: "+ options.backgroundOpacity + "; stroke-width: " + options.strokeWidth + "; stroke-linejoin:round; stroke-linecap:round; fill-rule: evenodd' xmlns='http://www.w3.org/2000/svg'>"
var svgEnd = "</svg></div>";
var newSvg = "<defs>"+
"<mask id='mask_" + options.id + "'>"+
"<path style='fill-opacity:1;stroke: white; fill:white;' d='" + svgFrame.toString().replace(/\,/g, ' ') + "'/>"+
gElements.join('') +
"</mask>"+
"</defs>";
var frame = "<path style='mask: url(#mask_" + options.id + ")' d='" + svgFrame.toString().replace(/\,/g, ' ') + "'/>";
return svgStart + frame + newSvg + svgEnd;
},
/**
* [getNormalSvgElement description]
* @param {type} gElement [description]
* @return {type} [description]
*/
getNormalSvgElement: function(gElements, options){
var svgStart = "<div id=svg_"+ options.id + " style='" + r360.Util.getTranslation(options.offset) + ";''><svg " +
" height=" + options.svgHeight +
" width=" + options.svgWidth +
" style='fill:" + options.backgroundColor + " ; opacity: " + options.opacity + "; stroke-linejoin:round; stroke-linecap:round; fill-rule: evenodd' xmlns='http://www.w3.org/2000/svg'>"
var svgEnd = "</svg></div>";
return svgStart + gElements.join('') + svgEnd;
},
/**
* [createSvgData description]
* @param {type} polygon [description]
* @param {type} options [description]
* @return {type} [description]
*/
createSvgData : function(polygon, options) {
var pathData = [];
var topRight = r360.PolygonUtil.scale(polygon.getTopRightDecimal(), options.scale);
var bottomLeft = r360.PolygonUtil.scale(polygon.getBottomLeftDecimal(), options.scale);
// the outer boundary
if ( !(bottomLeft.x > options.bounds.max.x || topRight.x < options.bounds.min.x ||
topRight.y > options.bounds.max.y || bottomLeft.y < options.bounds.min.y ))
r360.SvgUtil.buildSVGPolygon(pathData, polygon.getOuterBoundary().getCoordinates(), options);
var innerBoundary = polygon.getInnerBoundary();
// the inner boundaries
for ( var i = 0 ; i < innerBoundary.length ; i++ ) {
var topRightInner = r360.PolygonUtil.scale(innerBoundary[i].getTopRightDecimal(), options.scale);
var bottomLeftInner = r360.PolygonUtil.scale(innerBoundary[i].getBottomLeftDecimal(), options.scale);
if ( !(bottomLeftInner.x > options.bounds.max.x || topRightInner.x < options.bounds.min.x ||
topRightInner.y > options.bounds.max.y || bottomLeftInner.y < options.bounds.min.y ))
r360.SvgUtil.buildSVGPolygon(pathData, innerBoundary[i].getCoordinates(), options);
}
return pathData;
},
/**
* [buildSVGPolygon description]
* @param {type} pathData [description]
* @param {type} coordinateArray [description]
* @param {type} bounds [description]
* @param {type} scale [description]
* @return {type} [description]
*/
buildSVGPolygon: function(pathData, coordinateArray, options){
var point, point1, point2, isCollinear, euclidianDistance, pointCount = 0;
var boundArray = [[options.bounds.min.x, options.bounds.min.y],
[options.bounds.max.x, options.bounds.min.y],
[options.bounds.max.x, options.bounds.max.y],
[options.bounds.min.x, options.bounds.max.y]];
var pointsToClip = [];
for ( var i = 0 ; i < coordinateArray.length ; i++ ) {
point = r360.PolygonUtil.scale(r360.point(coordinateArray[i].x, coordinateArray[i].y), options.scale);
euclidianDistance = (i > 0) ? r360.PolygonUtil.getEuclidianDistance(point2, point) : options.tolerance;
if ( euclidianDistance >= options.tolerance ) {
isCollinear = false;
if ( pointCount > 2 )
isCollinear = r360.PolygonUtil.isCollinear(point1, point2, point);
if ( isCollinear ) {
pointsToClip[pointsToClip.length-1][0] = point.x;
pointsToClip[pointsToClip.length-1][1] = point.y;
}
else {
pointsToClip.push([point.x, point.y]);
point1 = point2;
point2 = point;
pointCount++;
}
}
}
var clippedArray = r360.PolygonUtil.clip(pointsToClip, boundArray);
var lastPoint;
for ( var i = 0 ; i < clippedArray.length ; i++ ){
point = r360.PolygonUtil.subtract(r360.point(clippedArray[i][0], clippedArray[i][1]),
options.pixelOrigin.x + options.offset.x,
options.pixelOrigin.y + options.offset.y)
pathData.push( i > 0 ? r360.PolygonUtil.buildPath(point, "L") : r360.PolygonUtil.buildPath(point, "M"));
lastPoint = point;
}
if ( pathData.length > 0 )
pathData.push(["z"]); // svgz
return pathData;
},
}
/*
*
*/
r360.Util = {
/*
* This method returns the current time, at the time this method is executed,
* in seconds. This means that the current hours, minutes and seconds of the current
* time are added up, e.g.: 12:11:15 pm:
*
* -> (12 * 3600) + (11 * 60) + 15 = 43875
*
* @method getTimeInSeconds
*
* @returns {Number} The current time in seconds
*/
getTimeInSeconds : function() {
var now = new Date();
return (now.getHours() * 3600) + (now.getMinutes() * 60) + now.getSeconds();
},
/*
* This method returns the current time in seconds, rounded down to the nearest minute,
* at the time this method is executed. This means that the current hours and minutes of the
* current time are converted to seconds and added up, e.g.: 12:11 pm:
*
* -> (12 * 3600) + (11 * 60) = 43860
*
* @method getHoursAndMinutesInSeconds
*
* @returns {Number} The current time in seconds
*/
getHoursAndMinutesInSeconds : function() {
var now = new Date();
return (now.getHours() * 3600) + (now.getMinutes() * 60);
},
/*
* Returns the current date in the form 20140508 (YYYYMMDD). Note that month is
* not zero but 1 based, which means 6 == June.
*
* @method getCurrentDate
*
* @return {String} the date object in string representation YYYYMMDD
*/
getCurrentDate : function() {
var date = new Date();
var year = date.getFullYear();
var month = (date.getMonth() + 1) < 10 ? "0" + (date.getMonth() + 1) : (date.getMonth() + 1);
var day = date.getDate() < 10 ? "0" + date.getDate() : date.getDate();
return year + "" + month + "" + day;
},
getTimeFormat : function(seconds) {
var i18n = r360.config.i18n;
if ( i18n.language == 'en' ) if ( seconds >= 43200 ) return 'p.m.';
return i18n.get('timeFormat');
},
/*
* Transforms the given seconds to a hour and minuten view. This means
* that for example 10:15 (one hour and 15 minutes) is translates to the string:
* -> 10h 15min
*
* Note that no trailing zeros are returned. Also if hours < 1 only minute values will be returned.
*
* @method secondsToHoursAndMinutes
* @returns {String} the transformed seconds in "xh ymin"
*/
secondsToHoursAndMinutes : function(seconds) {
var minutes = (seconds / 60).toFixed(0);
var hours = Math.floor(minutes / 60);
minutes = minutes - hours * 60;
var timeString = "";
if (hours != 0) timeString += (hours + "h ");
timeString += (minutes + "min");
return timeString;
},
/*
* This methods transforms a given time in seconds to a format like:
* 43200 -> 12:00:00
*
* @method secondsToTimeOfDay
* @returns {String} the formated time string in the format HH:MM:ss
*/
secondsToTimeOfDay : function(seconds){
var hours = Math.floor(seconds/3600);
var minutes = Math.floor(seconds/60)-hours*60;
seconds = seconds - (hours * 3600) - (minutes *60);
return hours+":"+ ("0" + minutes).slice(-2) +":"+ ("0" + seconds).slice(-2);
},
/*
* This methods generates a unique ID with the given length or 10 if no length was given.
* The method uses all characters from [A-z0-9] but does not guarantuee a unique string.
* It's more a pseudo random string.
*
* @method generateId
* @param the length of the returned pseudo random string
* @return a random string with the given length
*/
generateId : function(length) {
var id = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < (length ? length : 10); i++) {
id += possible.charAt(Math.floor(Math.random() * possible.length));
}
return id;
},
/*
*
*/
parseLatLonArray : function(latlngs) {
var coordinates = new Array();
for ( var i = 0 ; i < latlngs.length ; i++ )
coordinates.push(new r360.Point(latlngs[i][0], latlngs[i][1]))
return coordinates;
},
/*
* This methods uses the Rotue360° geocoding service to return
* a street address for a given latitude/longitude coordinate pair.
* This functionality is typically called reverse geocoding.
*
* @method getAddressByCoordinates
* @param {Object} [latlon] The coordinate
* @param {Number} [latlon.lat] The latitude of the coordinate.
* @param {Number} [latlon.lng] The longitude of the coordinate.
* @param {String} [language] The country code, 'nb' for norway, 'de' for germany.
* @param {Function} [callback] The callback methods which processes the returned data.
*/
getAddressByCoordinates : function(latlng, language, callback){
$.getJSON(r360.config.nominatimUrl + 'reverse.php?&format=json&lat=' + latlng.lat + '&accept-language=' + language + '&lon=' + latlng.lng + '&json_callback=?', callback);
},
/*
* This method takes a result from the nominatim reverse geocoder and formats
* it to a readable and displayable string. It builds up an address like this:
* 'STREETNAME STREETNUMBER, POSTALCODE, CITY'
* In case any of these values are undefined, they get removed from returned string.
* In case all values are undefined, the 'display_name' property of the returned
* json (from nominatim) is used to generate the output value.
* @return {String} a string representing the geocoordinates in human readable form
*/
formatReverseGeocoding : function(json) {
var streetAdress = [];
if ( r360.has(json.address, 'road') ) streetAdress.push(json.address.road);
if ( r360.has(json.address, 'house_number') ) streetAdress.push(json.address.house_number);
var city = [];
if ( r360.has(json.address, 'postcode') ) city.push(json.address.postcode);
if ( r360.has(json.address, 'city') ) city.push(json.address.city);
var address = [];
if ( streetAdress.length > 0 ) address.push(streetAdress.join(' '));
if ( city.length > 0) address.push(city.join(', '));
if ( streetAdress.length == 0 && city.length == 0 ) address.push(json.display_name);
return address.join(', ');
},
/**
* [formatPhotonReverseGeocoding description]
* @param {type} place [description]
* @return {type} [description]
*/
formatPhotonReverseGeocoding : function(place) {
var streetAdress = [];
if ( r360.has(place, 'name') ) streetAdress.push(place.name);
if ( r360.has(place, 'street') ) streetAdress.push(place.street);
if ( r360.has(place, 'housenumber') ) streetAdress.push(place.housenumber);
var city = [];
if ( r360.has(place, 'postcode') ) city.push(place.postcode);
if ( r360.has(place, 'city') ) city.push(place.city);
var address = [];
if ( streetAdress.length > 0 ) address.push(streetAdress.join(' '));
if ( city.length > 0) address.push(city.join(', '));
if ( streetAdress.length == 0 && city.length == 0 ) address.push("Reverse geocoding not possible.");
return address.join(', ');
},
/*
*
*/
parsePolygons : function(polygonsJson) {
var multiPolygon = [];
// we get polygons for each source
for ( var i = 0 ; i < polygonsJson.length ; i++ ) {
var source = polygonsJson[i];
for ( var j = 0; j < source.polygons.length; j++ ) {
// get the polygon infos
var polygonJson = source.polygons[j];
// create a polygon with the outer boundary as the initial linestring
var polygon = r360.polygon(polygonJson.travelTime, polygonJson.area, r360.lineString(r360.Util.parseLatLonArray(polygonJson.outerBoundary)));
// set color and default to black of not found
var color = r360.findWhere(r360.config.defaultTravelTimeControlOptions.travelTimes, { time : polygon.getTravelTime() });
polygon.setColor(!r360.isUndefined(color) ? color.color : '#000000');
// set opacity and default to 1 if not found
var opacity = r360.findWhere(r360.config.defaultTravelTimeControlOptions.travelTimes, { time : polygon.getTravelTime() })
polygon.setOpacity(!r360.isUndefined(opacity) ? opacity.opacity : 1);
if ( typeof polygonJson.innerBoundary !== 'undefined' ) {
// add all inner linestrings to polygon
for ( var k = 0 ; k < polygonJson.innerBoundary.length ; k++ )
polygon.addInnerBoundary(r360.lineString(r360.Util.parseLatLonArray(polygonJson.innerBoundary[k])));
}
r360.PolygonUtil.addPolygonToMultiPolygon(multiPolygon, polygon);
}
}
// make sure the multipolygons are sorted by the travel time ascendingly
multiPolygon.sort(function(a,b) { return b.getTravelTime() - a.getTravelTime(); });
return multiPolygon;
},
/*
* This method parses the JSON returned from the Route360 Webservice and generates
* java script objects representing the values.
*/
parseRoutes : function(json){
var routes = new Array();
for(var i = 0; i < json.routes.length; i++){
var meta = json.routes[i];
routes.push(r360.route(json.routes[i].travelTime, json.routes[i].segments, meta));
}
return routes;
},
// 3857 => pixel
webMercatorToLeaflet : function(point){
return r360.CRS.EPSG3857.transformation._transform(r360.point(point.x / 6378137, point.y / 6378137));
},
webMercatorToLatLng : function(point, elevation){
var latlng = r360.CRS.EPSG3857.projection.unproject(new r360.Point(point.x, point.y));
// x,y,z given so we have elevation data
if ( typeof elevation !== 'undefined' )
return r360.latLng([latlng.lat, latlng.lng, elevation]);
// no elevation given, just unproject coordinates to lat/lng
else
return latlng;
},
latLngToWebMercator : function(latlng){
var point = r360.Projection.SphericalMercator.project(latlng);
point.x *= 6378137;
point.y *= 6378137;
return point;
},
getUserAgent : function(){
var ua= navigator.userAgent, tem,
M= ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
if(/trident/i.test(M[1])){
tem= /\brv[ :]+(\d+)/g.exec(ua) || [];
return 'IE '+(tem[1] || '');
}
if(M[1]=== 'Chrome'){
tem= ua.match(/\bOPR\/(\d+)/)
if(tem!= null) return 'Opera '+tem[1];
}