famous-map
Version:
Map integration for Famo.us (google-maps, leaflet, open-layers & mapboxgl)
1,445 lines (1,299 loc) • 55.6 kB
JavaScript
/**
* This Source Code is licensed under the MIT license. If a copy of the
* MIT-license was not distributed with this file, You can obtain one at:
* http://opensource.org/licenses/mit-license.html.
*
* @author: Hein Rutjes (IjzerenHein)
* @license MIT
* @copyright Gloey Apps, 2014/2015
*
* @library famous-map
* @version 0.3.2
* @generated 08-05-2015
*/
/**
* This Source Code is licensed under the MIT license. If a copy of the
* MIT-license was not distributed with this file, You can obtain one at:
* http://opensource.org/licenses/mit-license.html.
*
* @author: Hein Rutjes (IjzerenHein)
* @license MIT
* @copyright Gloey Apps, 2014
*/
/**
* This namespace holds standalone functionality.
*
* @module
*/
define('famous-map/MapUtility',['require','exports','module'],function(require, exports, module) {
/**
* @class
* @alias module:MapUtility
*/
var MapUtility = {};
/**
* Get the latitude from the position (LatLng) object.
*
* @param {LatLng} position Position
* @return {Number} Latitude in degrees
*/
MapUtility.lat = function lat(position) {
if (position instanceof Array) {
return position[0];
}
else if (position.lat instanceof Function) {
return position.lat();
}
else {
return position.lat;
}
};
/**
* Get the longitude from the position (LatLng) object.
*
* @param {LatLng} position Position
* @return {Number} Longitude in degrees
*/
MapUtility.lng = function lng(position) {
if (position instanceof Array) {
return position[1];
}
else if (position.lng instanceof Function) {
return position.lng();
}
else {
return position.lng;
}
};
/**
* Compares two positions for equality.
*
* @param {LatLng} position1 Position 1
* @param {LatLng} position2 Position 2
* @return {Boolean} Result of comparison
*/
MapUtility.equals = function(position1, position2) {
return (MapUtility.lat(position1) === MapUtility.lat(position2)) &&
(MapUtility.lng(position1) === MapUtility.lng(position2));
};
/**
* Converts degrees into radians (radians = degrees * (Math.PI / 180)).
*
* @param {Number} deg Degrees
* @return {Number} radians.
*/
MapUtility.radiansFromDegrees = function(deg) {
return deg * (Math.PI / 180);
};
/**
* Calculates the rotation-angle between two given positions.
*
* @param {LatLng} start Start position.
* @param {LatLng} end End position.
* @return {Number} Rotation in radians.
*/
MapUtility.rotationFromPositions = function(start, end) {
return Math.atan2(MapUtility.lng(start) - MapUtility.lng(end), MapUtility.lat(start) - MapUtility.lat(end)) + (Math.PI / 2.0);
};
/**
* Calculates the distance between two positions in kilometers.
*
* @param {LatLng} start Starting position
* @param {LatLng} end End position
* @return {Number} Distance in km
*/
MapUtility.distanceBetweenPositions = function(start, end) {
// Taken from: http://www.movable-type.co.uk/scripts/latlong.html
var R = 6371; // earths radius in km
var lat1 = MapUtility.radiansFromDegrees(MapUtility.lat(start));
var lat2 = MapUtility.radiansFromDegrees(MapUtility.lat(end));
var deltaLat = MapUtility.radiansFromDegrees(MapUtility.lat(end) - MapUtility.lat(start));
var deltaLng = MapUtility.radiansFromDegrees(MapUtility.lng(end) - MapUtility.lng(start));
var a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
Math.cos(lat1) * Math.cos(lat2) *
Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
var d = R * c;
return d;
};
module.exports = MapUtility;
});
/**
* This Source Code is licensed under the MIT license. If a copy of the
* MIT-license was not distributed with this file, You can obtain one at:
* http://opensource.org/licenses/mit-license.html.
*
* @author: Hein Rutjes (IjzerenHein)
* @license MIT
* @copyright Gloey Apps, 2014
*/
/**
* The MapPositionTransitionable makes it possible to transition between two geographical
* positions. Currently, only standard transition definitions are supported (see `Transitionable`), but in the future more interesting
* transitions may be added.
*
* *This class is used internally by `MapView` and `MapStateModifier`.*
*
* @module
*/
define('famous-map/MapPositionTransitionable',['require','exports','module','famous/transitions/Transitionable','./MapUtility'],function(require, exports, module) {
// import dependencies
var Transitionable = require('famous/transitions/Transitionable');
var MapUtility = require('./MapUtility');
/**
* @class
* @param {LatLng} [position] Default geopgraphical position
* @alias module:MapPositionTransitionable
*/
function MapPositionTransitionable(position) {
this.position = new Transitionable([0, 0]);
if (position) {
this.set(position);
}
}
/**
* Sets the default transition to use for transitioning between position states.
*
* @param {Object} transition Transition definition
*/
MapPositionTransitionable.prototype.setDefaultTransition = function setDefaultTransition(transition) {
this.position.setDefault(transition);
};
/**
* Cancel all transitions and reset to a geographical position.
*
* @param {LatLng} position
*/
MapPositionTransitionable.prototype.reset = function reset(position) {
var latlng = [MapUtility.lat(position), MapUtility.lng(position)];
this.position.reset(latlng);
this._final = position;
};
/**
* Set the geographical position by adding it to the queue of transition.
*
* @param {LatLng} position
* @param {Object} [transition] Transition definition
* @param {Function} [callback] Callback
*/
MapPositionTransitionable.prototype.set = function set(position, transition, callback) {
var latlng = [MapUtility.lat(position), MapUtility.lng(position)];
this.position.set(latlng, transition, callback);
this._final = position;
return this;
};
/**
* Get the current geographical position.
*
* @return {LatLng}
*/
MapPositionTransitionable.prototype.get = function get() {
if (this.isActive()) {
var latlng = this.position.get();
return {
lat: latlng[0],
lng: latlng[1]
};
}
else {
return this._final;
}
};
/**
* Get the destination geographical position.
*
* @return {LatLng}
*/
MapPositionTransitionable.prototype.getFinal = function getFinal() {
return this._final;
};
/**
* Determine if the transitionable is currently transitioning
*
* @return {Boolean}
*/
MapPositionTransitionable.prototype.isActive = function isActive() {
return this.position.isActive();
};
/**
* Halts the transition
*/
MapPositionTransitionable.prototype.halt = function halt() {
this._final = this.get();
this.position.halt();
};
module.exports = MapPositionTransitionable;
});
/**
* This Source Code is licensed under the MIT license. If a copy of the
* MIT-license was not distributed with this file, You can obtain one at:
* http://opensource.org/licenses/mit-license.html.
*
* @author: Hein Rutjes (IjzerenHein)
* @license MIT
* @copyright Gloey Apps, 2014
*/
/**
* MapTransitions can transition between geographical positions using specific speed (e.g. 50 km/h).
*
* @module
*/
define('famous-map/MapTransition',['require','exports','module','./MapUtility'],function(require, exports, module) {
// import dependencies
var MapUtility = require('./MapUtility');
/**
* @class
* @alias module:MapTransition
*/
function MapTransition() {
this.state = undefined;
this._startTime = 0;
this._startState = 0;
this._updateTime = 0;
this._endState = 0;
this._active = false;
this._duration = 0;
this._distance = 0;
this._callback = undefined;
}
MapTransition.SUPPORTS_MULTIPLE = 2;
/**
* @property DEFAULT_OPTIONS
* @protected
*/
MapTransition.DEFAULT_OPTIONS = {
/**
* The speed of the transition in mph.
*/
speed: 1000 // mph
};
// Interpolate: If a linear function f(0) = a, f(1) = b, then return f(t)
function _interpolate(a, b, t) {
return ((1 - t) * a) + (t * b);
}
function _clone(obj) {
return obj.slice(0);
}
/**
* Resets the position
*
* @param {Array.Number} state Array: [lat, lng]
*/
MapTransition.prototype.reset = function reset(state) {
if (this._callback) {
var callback = this._callback;
this._callback = undefined;
callback();
}
this.state = _clone(state);
this._startTime = 0;
this._updateTime = 0;
this._startState = this.state;
this._endState = this.state;
this._duration = 0;
this._distance = 0;
this._active = false;
};
/**
* Set the end position and transition, with optional callback on completion.
*
* @param {Array.Number} state Array: [lat,lng]
* @param {Object} [transition] Transition definition
* @param {Function} [callback] Callback
*/
MapTransition.prototype.set = function set(state, transition, callback) {
if (!transition) {
this.reset(state);
if (callback) {
callback();
}
return;
}
this._speed = MapTransition.DEFAULT_OPTIONS.speed;
if (transition && transition.speed) {
this._speed = transition.speed;
}
this._startState = this.get();
this._startTime = Date.now();
this._endState = _clone(state);
this._active = true;
this._callback = callback;
this._distance = MapUtility.distanceBetweenPositions(this._startState, this._endState);
this._duration = (this._distance / this._speed) * (60 * 60 * 1000);
//console.log('distance: ' + this._distance + ' km, speed: ' + transition.speed + 'km/h, duration:' + this._duration + ' ms');
};
/**
* Get the current position of the transition.
*
* @param {Date} [timestamp] Timestamp at which to get the position
* @return {Array.Number} Array: [lat, lng]
*/
MapTransition.prototype.get = function get(timestamp) {
if (!this._active) {
if (this._callback) {
var callback = this._callback;
this._callback = undefined;
callback();
}
return this.state;
}
if (!timestamp) {
timestamp = Date.now();
}
if (this._updateTime >= timestamp) {
return this.state;
}
this._updateTime = timestamp;
var timeSinceStart = timestamp - this._startTime;
if (timeSinceStart >= this._duration) {
this.state = this._endState;
this._active = false;
}
else if (timeSinceStart < 0) {
this.state = this._startState;
}
else {
var t = timeSinceStart / this._duration;
var lat = _interpolate(this._startState[0], this._endState[0], t);
var lng = _interpolate(this._startState[1], this._endState[1], t);
this.state = [lat, lng];
}
return this.state;
};
/**
* Detects whether a transition is in progress
*
* @return {Boolean}
*/
MapTransition.prototype.isActive = function isActive() {
return this._active;
};
/**
* Halt the transition
*/
MapTransition.prototype.halt = function halt() {
this.set(this.get());
};
module.exports = MapTransition;
});
/**
* This Source Code is licensed under the MIT license. If a copy of the
* MIT-license was not distributed with this file, You can obtain one at:
* http://opensource.org/licenses/mit-license.html.
*
* @author: Hein Rutjes (IjzerenHein)
* @license MIT
* @copyright Gloey Apps, 2014/2015
*/
/*global google, L, ol, mapboxgl*/
/**
* MapView encapsulates a Google maps view so it can be used with famo.us.
*
* Additionally it adds methods to set the position and zoom-factor of the map using transitions.
* Use `MapModifier` and `MapStateModifier` to place famo.us renderables on the map, much like google-maps markers.
*
* **Map-types**
*
* |Value|Description|
* |---|---|
* |MapType.GOOGLEMAPS (default)|Google-maps|
* |MapType.LEAFLET|Leaflet.js|
* |MapType.OPENLAYERS3|Open layers 3|
* |MapType.MAPBOXGL|Mapbox GL|
* @module
*/
define('famous-map/MapView',['require','exports','module','famous/core/Surface','famous/views/SizeAwareView','famous/transitions/Transitionable','./MapUtility','./MapPositionTransitionable','./MapTransition'],function(require, exports, module) {
// import dependencies
var Surface = require('famous/core/Surface');
var SizeAwareView = require('famous/views/SizeAwareView');
var Transitionable = require('famous/transitions/Transitionable');
var MapUtility = require('./MapUtility');
var MapPositionTransitionable = require('./MapPositionTransitionable');
var MapTransition = require('./MapTransition');
Transitionable.registerMethod('map-speed', MapTransition);
var globalMapViewId = 1;
/*
* Map-type
* @enum {Number}
* @alias module:MapView.MapType
*/
var MapType = {
GOOGLEMAPS: 1,
LEAFLET: 2,
OPENLAYERS3: 3,
MAPBOXGL: 4
};
/**
* @class
* @param {Object} options Options.
* @param {MapType} options.type Map-type (e.g. MapView.MapType.GOOGLEMAPS, MapView.MapType.LEAFLET, MapView.MapType.OPENLAYERS3).
* @param {Object} options.mapOptions Options that are passed directly to the Map object. The options should include the 'center' and 'zoom'.
* @param {String} [options.id] Id of the DOM-element to use. When ommitted, a DOM-element is created using a surface.
* @param {Transition} [options.zoomTransition] Transition to use for smoothly zooming renderables (by default a transition of 120 ms is used).
* @alias module:MapView
*/
function MapView() {
SizeAwareView.apply(this, arguments);
// Initialize
this.map = null;
this.mapType = this.options.type;
this._position = new MapPositionTransitionable(this.options.mapOptions.center);
this._zoom = {
center: new MapPositionTransitionable(this.options.mapOptions.center),
northEast: new MapPositionTransitionable(this.options.mapOptions.center),
southWest: new MapPositionTransitionable(this.options.mapOptions.center)
};
this._cache = {};
// Disable zoom-transitions for leaflet
if (this.mapType === MapType.LEAFLET) {
this.options.zoomTransition = {duration: 0};
}
// When a specific dom-id is specified, use that
if (this.options.mapOptions && this.options.id) {
this.mapId = this.options.id;
}
else {
// Otherwise generate unique id, and create the div ourselves
this.mapId = 'MapView' + globalMapViewId;
globalMapViewId++;
// Insert div into the DOM
var surface = new Surface({
classes: ['mapview', 'fm-mapview'], // mapview is included for backwards compat
content: '<div id="' + this.mapId + '" style="width: 100%; height: 100%;"></div>',
size: [undefined, undefined]
});
this.add(surface);
this._surface = surface;
}
}
MapView.prototype = Object.create(SizeAwareView.prototype);
MapView.prototype.constructor = MapView;
MapView.MapType = MapType;
MapView.DEFAULT_OPTIONS = {
type: MapType.GOOGLEMAPS,
mapOptions: {
zoom: 10,
center: {lat: 51.4400867, lng: 5.4782571}
},
id: null,
zoomTransition: {duration: 100}
};
/**
* Initializes the map (happens after the DOM element has been created).
*
* @private
* @ignore
*/
MapView.prototype._initMap = function() {
// Try to find DOM element
var elm = document.getElementById(this.mapId);
if (!elm) {
return;
}
// Supported map-types
var options;
switch (this.mapType) {
// Create google.maps.Map
case MapType.GOOGLEMAPS:
this.map = new google.maps.Map(elm, this.options.mapOptions);
// Listen for the first occurance of 'projection_changed', to ensure the map is full
// initialized.
var func = this.map.addListener('projection_changed', function() {
google.maps.event.removeListener(func);
this._initComplete = true;
}.bind(this));
break;
// Create leaflet Map
case MapType.LEAFLET:
this.map = L.map(elm, this.options.mapOptions);
this._initComplete = true;
break;
// Create open layers 3 Map
case MapType.OPENLAYERS3:
options = this.options.mapOptions;
var center = options.center;
this.map = new ol.Map({
target: elm,
controls: ol.control.defaults({attributionOptions: {collapsible: false}}),
view: new ol.View({
center: ol.proj.transform([MapUtility.lng(center), MapUtility.lat(center)], 'EPSG:4326', 'EPSG:3857'),
zoom: options.zoom
})
});
this._surface.on('resize', function() {
this.map.updateSize();
}.bind(this));
// When famo.us removes the OpenLayers div from the DOM, the canvas gets
// hidden and is not restored to its visible state when shown again.
// The following code, calls 'updateSize' whenever famo.us re-deploys
// the surface to the DOM, fixing this issue.
this._surface.on('deploy', function() {
if (this._initComplete) {
this.map.updateSize();
}
}.bind(this));
this.map.once('postrender', function() {
this._initComplete = true;
}.bind(this));
break;
// Create mapbox GL Map
case MapType.MAPBOXGL:
options = {};
for (var key in this.options.mapOptions) {
options[key] = this.options.mapOptions[key];
}
options.container = elm;
this.map = new mapboxgl.Map(options);
this._initComplete = true;
break;
}
};
/**
* Get the internal map-object. This object may not yet have been initialized, the map is only
* guarenteed to be valid after the 'load' event has been emited.
*
* @return {Map} Map object.
*/
MapView.prototype.getMap = function() {
return this.map;
};
/**
* Returns `true` when the map-view has been fully initialized.
*
* @return {Bool} true/false.
*/
MapView.prototype.isInitialized = function() {
return this._initComplete && this._loadEventEmitted;
};
/**
* Set the center of the map to the given geographical coordinates.
*
* @param {LatLng} position Position in geographical coordinates.
* @param {Transitionable} [transition] Transitionable.
* @param {Function} [callback] callback to call after transition completes.
*/
MapView.prototype.setPosition = function(position, transition, callback) {
this._position.set(position, transition, callback);
this._positionInvalidated = true;
return this;
};
/**
* Get the current center position of the map, in geographical coordinates.
*
* @return {LatLng} Position in geographical coordinates.
*/
MapView.prototype.getPosition = function() {
return this._zoom.center.get();
};
/**
* Get the destination center position of the map, in geographical coordinates.
*
* @return {LatLng} Position in geographical coordinates.
*/
MapView.prototype.getFinalPosition = function() {
return this._position.getFinal();
};
/**
* Get the current zoom-level of the map, taking into account smooth transition between zoom-levels.
* E.g., when zooming from zoom-level 4 to 5, this function returns an increasing value starting at 4 and ending
* at 5, over time. The used zoomTransition can be set as an option.
*
* @return {Number} Zoom-level.
*/
MapView.prototype.getZoom = function() {
return this._cache.zoom;
};
/**
* Get the rotation of the map. 0 means north-up.
* @return {Number} Rotation in radians.
*/
MapView.prototype.getRotation = function() {
switch (this.mapType) {
case MapType.GOOGLEMAPS:
case MapType.LEAFLET:
return 0;
case MapType.OPENLAYERS3:
return this.map.getView().getRotation();
case MapType.MAPBOXGL:
return (this.map.getBearing() * Math.PI) / -180;
}
};
/**
* Get the position in pixels (relative to the left-top of the container) for the given geographical position.
*
* @param {LatLng} position in geographical coordinates.
* @return {Point} Position in pixels, relative to the left-top of the mapView.
*/
MapView.prototype.pointFromPosition = function(position) {
var pnt;
switch (this.mapType) {
case MapType.GOOGLEMAPS:
if (!(position instanceof google.maps.LatLng)) {
position = new google.maps.LatLng(MapUtility.lat(position), MapUtility.lng(position), true);
}
var worldPoint = this.map.getProjection().fromLatLngToPoint(position);
return {
x: (worldPoint.x - this._cache.bottomLeft.x) * this._cache.scale,
y: (worldPoint.y - this._cache.topRight.y) * this._cache.scale
};
case MapType.LEAFLET:
// Note: smooth zooming is not yet supported for leaflet
pnt = this.map.latLngToContainerPoint(position);
return pnt;
case MapType.OPENLAYERS3:
// Note: updates during map interaction are not yet supported
pnt = this.map.getPixelFromCoordinate(ol.proj.transform([MapUtility.lng(position), MapUtility.lat(position)], 'EPSG:4326', 'EPSG:3857'));
return {x: pnt[0], y: pnt[1]};
case MapType.MAPBOXGL:
return this.map.project([MapUtility.lat(position), MapUtility.lng(position)]);
}
};
/**
* Get the geographical coordinates for a given position in pixels (relative to the left-top of the container).
*
* @param {Point} point Position in pixels, relative to the left-top of the mapView.
* @return {LatLng} Position in geographical coordinates.
*/
MapView.prototype.positionFromPoint = function(point) {
switch (this.mapType) {
case MapType.GOOGLEMAPS:
var worldPoint = new google.maps.Point(
(point.x / this._cache.scale) + this._cache.bottomLeft.x,
(point.y / this._cache.scale) + this._cache.topRight.y
);
return this.map.getProjection().fromPointToLatLng(worldPoint);
case MapType.LEAFLET:
// Note: smooth zooming is not yet supported for leaflet
return this.map.containerPointToLatLng(point);
case MapType.OPENLAYERS3:
// Note: updates during map interaction are not yet supported
var lonLat = ol.proj.transform(this.map.getCoordinateFromPixel([point.x, point.y]), 'EPSG:3857', 'EPSG:4326');
return {lat: lonLat[1], lng: lonLat[0]};
case MapType.MAPBOXGL:
return this.map.unproject(point);
}
};
/**
* Get the size of the map-view in pixels.
*
* @return {Array.Number} Size of the mapView.
*/
MapView.prototype.getSize = function() {
return this._cache.size;
};
/**
* Halts any pending transitions.
*/
MapView.prototype.halt = function() {
this._position.halt();
this._positionInvalidated = true;
};
/**
* Is there at least one action pending completion?
*
* @return {Bool} True when there are active transitions running.
*/
MapView.prototype.isActive = function() {
return this._position.isActive();
};
/**
* @private
* @ignore
*/
MapView.prototype._updateCache = function(zoom, northEast, southWest) {
// Store final data
this._cache.finalZoom = zoom;
this._cache.finalScale = Math.pow(2, this._cache.finalZoom);
this._cache.finalNorthEast = northEast;
this._cache.finalSouthWest = southWest;
// Calculate size of the MapView
switch (this.mapType) {
case MapType.GOOGLEMAPS:
if (!(northEast instanceof google.maps.LatLng)) {
northEast = new google.maps.LatLng(MapUtility.lat(northEast), MapUtility.lng(northEast), true);
}
if (!(southWest instanceof google.maps.LatLng)) {
southWest = new google.maps.LatLng(MapUtility.lat(southWest), MapUtility.lng(southWest), true);
}
var topRight = this.map.getProjection().fromLatLngToPoint(northEast);
var bottomLeft = this.map.getProjection().fromLatLngToPoint(southWest);
this._cache.size = [
(topRight.x - bottomLeft.x) * this._cache.finalScale,
(bottomLeft.y - topRight.y) * this._cache.finalScale
];
break;
case MapType.LEAFLET:
var point = this.map.getSize();
this._cache.size = [point.x, point.y];
break;
case MapType.OPENLAYERS3:
this._cache.size = this.map.getSize();
break;
case MapType.MAPBOXGL:
this._cache.size = this.getParentSize();
break;
}
// Calculate current world point edges and scale
switch (this.mapType) {
case MapType.GOOGLEMAPS:
northEast = this._zoom.northEast.get();
southWest = this._zoom.southWest.get();
if (!(northEast instanceof google.maps.LatLng)) {
northEast = new google.maps.LatLng(MapUtility.lat(northEast), MapUtility.lng(northEast), true);
}
if (!(southWest instanceof google.maps.LatLng)) {
southWest = new google.maps.LatLng(MapUtility.lat(southWest), MapUtility.lng(southWest), true);
}
this._cache.topRight = this.map.getProjection().fromLatLngToPoint(northEast);
this._cache.bottomLeft = this.map.getProjection().fromLatLngToPoint(southWest);
this._cache.scale = this._cache.size[0] / (this._cache.topRight.x - this._cache.bottomLeft.x);
this._cache.zoom = Math.log(this._cache.scale) / Math.log(2);
break;
case MapType.LEAFLET:
case MapType.OPENLAYERS3:
case MapType.MAPBOXGL:
// Note: smooth zooming is not yet supported for leaflet, and
// updates during map interaction are not yet supported for ol3
this._cache.zoom = zoom;
break;
}
};
/**
* Get map-information from the underlying map-provider, such as position, bounds, zoom-level...
*
* @private
* @ignore
*/
MapView.prototype._getMapInfo = function() {
var bounds;
var northEast;
var southWest;
var center;
var zoom;
switch (this.mapType) {
case MapType.GOOGLEMAPS:
// map.getBounds() returns the northEast and southWest in wrapped coordinates (between -180..180).
// This makes it difficult to create a linear coordinate space for converting world-coordinates
// into pixels. This function therefore 'unwraps' the northEast and southWest coordinates using
// * map.getCenter() (which does return unwrapped coordinates).
bounds = this.map.getBounds();
center = this.map.getCenter();
zoom = this.map.getZoom();
var centerLng = MapUtility.lng(center);
northEast = bounds.getNorthEast();
var northEastLng = northEast.lng();
while (northEastLng < centerLng) {
northEastLng += 360;
}
while (northEastLng > (centerLng + 360)) {
northEastLng -= 360;
}
southWest = bounds.getSouthWest();
var southWestLng = southWest.lng();
while (southWestLng < (centerLng - 360)) {
southWestLng += 360;
}
while (southWestLng > centerLng) {
southWestLng -= 360;
}
return {
zoom: zoom,
center: {lat: center.lat(), lng: center.lng()},
southWest: {lat: southWest.lat(), lng: southWestLng},
northEast: {lat: northEast.lat(), lng: northEastLng}
};
case MapType.LEAFLET:
bounds = this.map.getBounds();
southWest = bounds.getSouthWest();
northEast = bounds.getNorthEast();
center = this.map.getCenter();
zoom = this.map.getZoom();
return {
zoom: zoom,
center: {lat: center.lat, lng: center.lng},
southWest: {lat: southWest.lat, lng: southWest.lng},
northEast: {lat: northEast.lat, lng: northEast.lng}
};
case MapType.OPENLAYERS3:
var view = this.map.getView();
bounds = ol.proj.transformExtent(view.calculateExtent(this.map.getSize()), 'EPSG:3857', 'EPSG:4326');
center = ol.proj.transform(view.getCenter(), 'EPSG:3857', 'EPSG:4326');
zoom = view.getZoom();
return {
zoom: zoom,
center: {lat: center[1], lng: center[0]},
southWest: {lat: bounds[1], lng: bounds[0]},
northEast: {lat: bounds[3], lng: bounds[2]},
rotation: view.getRotation()
};
case MapType.MAPBOXGL:
bounds = this.map.getBounds();
return {
zoom: this.map.getZoom(),
center: this.map.getCenter(),
southWest: bounds.getSouthWest(),
northEast: bounds.getNorthEast(),
rotation: (this.map.getBearing() * Math.PI) / -180
};
}
};
/**
* Renders the view.
*
* @private
* @ignore
*/
MapView.prototype.render = function render() {
// Init the map (once)
if (!this.map) {
this._initMap();
}
if (this._initComplete) {
// When the zoom-level is changed by the map, start a transition
// that runs alongside.
var options;
var info = this._getMapInfo();
var invalidateCache = false;
if (info.zoom !== this._cache.finalZoom) {
this._zoom.northEast.halt();
this._zoom.southWest.halt();
this._zoom.center.halt();
this._zoom.northEast.set(info.northEast, this.options.zoomTransition);
this._zoom.southWest.set(info.southWest, this.options.zoomTransition);
this._zoom.center.set(info.center, this.options.zoomTransition);
invalidateCache = true;
}
else if (!this._zoom.northEast.isActive()) {
this._zoom.northEast.reset(info.northEast);
this._zoom.southWest.reset(info.southWest);
this._zoom.center.reset(info.center);
}
else {
this._zoom.northEast.get(); // ensure that .get() always gets called to ensure that isActive() works
invalidateCache = true;
}
// Update the cache
if (invalidateCache || (info.zoom !== this._cache.finalZoom) ||
!MapUtility.equals(info.northEast, this._cache.finalNorthEast) ||
!MapUtility.equals(info.southWest, this._cache.finalSouthWest)) {
//console.log('updating cache..');
this._updateCache(info.zoom, info.northEast, info.southWest);
}
// Get/set map center
if (this._position.isActive() || this._positionInvalidated) {
options = {
center: this._position.get()
};
this._positionInvalidated = false;
}
else {
this._position.reset(info.center);
}
if (options) {
switch (this.mapType) {
case MapType.GOOGLEMAPS:
this.map.setOptions(options);
break;
case MapType.LEAFLET:
this.map.panTo(options.center, {animate: false});
break;
case MapType.OPENLAYERS3:
this.map.getView().setCenter(ol.proj.transform([MapUtility.lng(options.center), MapUtility.lat(options.center)], 'EPSG:4326', 'EPSG:3857'));
break;
case MapType.MAPBOXGL:
this.map.setCenter([MapUtility.lat(options.center), MapUtility.lng(options.center)]);
break;
}
}
if (!this._loadEventEmitted) {
this._loadEventEmitted = true;
this._eventOutput.emit('load', this);
}
}
// Call super
return this._node.render();
};
/**
* Helper function that checks whether the given DOM element
* is a child of a MapView.
*/
function _elementIsChildOfMapView(element) {
if (element.className.indexOf('fm-mapview') >= 0) {
return true;
}
return element.parentElement ? _elementIsChildOfMapView(element.parentElement) : false;
}
/**
* Installs the selective touch-move handler so that Google Maps
* can be correctly used with touch events (mobile).
*
* Install this handler before creating the main Context:
* ```javascript
* var Engine = require('famous/core/Engine');
* var MapView = require('famous-map/MapView');
* var isMobile = require('ismobilejs');
*
* if (isMobile.any) {
* Engine.setOptions({appMode: false});
* MapView.installSelectiveTouchMoveHandler();
* }
*
* var mainContext = Engine.createContext();
* ...
* ```
*/
MapView.installSelectiveTouchMoveHandler = function() {
window.addEventListener('touchmove', function(event) {
if (!_elementIsChildOfMapView(event.target)) {
event.preventDefault();
}
});
};
module.exports = MapView;
});
/**
* This Source Code is licensed under the MIT license. If a copy of the
* MIT-license was not distributed with this file, You can obtain one at:
* http://opensource.org/licenses/mit-license.html.
*
* @author: Hein Rutjes (IjzerenHein)
* @license MIT
* @copyright Gloey Apps, 2014
*/
/**
* The MapModifier makes it possible to link renderables to a geopgraphical position on a `MapView`.
* Additionally it adds functionality for rotating and zooming renderables, and possibly all kinds of future map-related transformations.
* Use `MapStateModifier` if you want to use transitions, e.g. to animate a move from one geographical position to another.
*
* @module
*/
define('famous-map/MapModifier',['require','exports','module','famous/core/Transform','./MapUtility'],function(require, exports, module) {
// import dependencies
var Transform = require('famous/core/Transform');
var MapUtility = require('./MapUtility');
/**
* @class
* @param {Object} options Options.
* @param {MapView} options.mapView The MapView.
* @param {LatLng} [options.position] Initial geographical coordinates.
* @param {LatLng} [options.offset] Displacement offset in geographical coordinates from the position.
* @param {LatLng | object | function} [options.rotateTowards] Position to rotate the renderables towards.
* @param {number} [options.zoomBase] Base zoom-level at which the renderables are displayed in their true size.
* @param {number | function} [options.zoomScale] Customer zoom-scaling factor or function.
* @alias module:MapModifier
*/
function MapModifier(options) {
this.mapView = options.mapView;
this._output = {
transform: Transform.identity,
opacity: 1,
origin: null,
align: null,
size: null,
target: null
};
this._cache = {};
this._positionGetter = null;
this._rotateTowardsGetter = null;
this._offset = options.offset;
this._zoomScale = options.zoomScale;
this._zoomBase = options.zoomBase;
if (options.position) {
this.positionFrom(options.position);
}
if (options.rotateTowards) {
this.rotateTowardsFrom(options.rotateTowards);
}
}
/**
* Set the geographical position of the renderables.
*
* @param {LatLng|Function|Object} position Position in geographical coordinates.
*/
MapModifier.prototype.positionFrom = function(position) {
if (!position) {
this._positionGetter = null;
this._position = null;
}
else if (position instanceof Function) {
this._positionGetter = position;
}
else if (position instanceof Object && position.getPosition) {
this._positionGetter = position.getPosition.bind(position);
}
else {
this._positionGetter = null;
this._position = position;
}
return this;
};
/**
* Set the geographical position to rotate the renderables towards.
* The child renderables are assumed to be rotated to the right by default.
* To change the base rotation, add a rotation-transform to the renderable, like this:
* `new Modifier({transform: Transform.rotateZ(Math.PI/2)})`
*
* @param {LatLng} position Geographical position to rotate towards.
*/
MapModifier.prototype.rotateTowardsFrom = function(position) {
if (!position) {
this._rotateTowardsGetter = null;
this._rotateTowards = null;
}
else if (position instanceof Function) {
this._rotateTowardsGetter = position;
}
else if (position instanceof Object && position.getPosition) {
this._rotateTowardsGetter = position.getPosition.bind(position);
}
else {
this._rotateTowardsGetter = null;
this._rotateTowards = position;
}
return this;
};
/**
* Set the base zoom-level. When set, auto-zooming is effectively enabled.
* The renderables are then displayed in their true size when the map zoom-level equals zoomBase.
*
* @param {Number} zoomBase Map zoom-level
*/
MapModifier.prototype.zoomBaseFrom = function(zoomBase) {
this._zoomBase = zoomBase;
return this;
};
/**
* Set the zoom-scale (ignored when zoomBase is not set). When set, the scale is increased when zooming in and
* decreased when zooming-out. The zoomScale can be either a Number or a Function which returns
* a scale-factor, with the following signature: function (zoomBase, zoomCurrent).
*
* @param {Number|Function} zoomScale Zoom-scale factor or function.
*/
MapModifier.prototype.zoomScaleFrom = function(zoomScale) {
this._zoomScale = zoomScale;
return this;
};
/**
* Set the displacement offset in geographical coordinates.
*
* @param {LatLng} offset Displacement offset in geographical coordinates.
*/
MapModifier.prototype.offsetFrom = function(offset) {
this._offset = offset;
return this;
};
/**
* Get the current geographical position.
*
* @return {LatLng} Position in geographical coordinates.
*/
MapModifier.prototype.getPosition = function() {
return this._positionGetter || this._position;
};
/**
* Get the geographical position towards which the renderables are rotated.
*
* @return {LatLng} Geographical position towards which renderables are rotated.
*/
MapModifier.prototype.getRotateTowards = function() {
return this._rotateTowardsGetter || this._rotateTowards;
};
/**
* Get the base zoom-level. The zoomBase indicates the zoom-level at which renderables are
* displayed in their true size.
*
* @return {Number} Base zoom level
*/
MapModifier.prototype.getZoomBase = function() {
return this._zoomBase;
};
/**
* Get the base zoom-scale. The zoomScale can be either a Number or a Function which returns
* a scale-factor.
*
* @return {Number|Function} Zoom-scale
*/
MapModifier.prototype.getZoomScale = function() {
return this._zoomScale;
};
/**
* Get the geographical displacement offset.
*
* @return {LatLng} Offset in geographical coordinates.
*/
MapModifier.prototype.getOffset = function() {
return this._offset;
};
/**
* Return render spec for this MapModifier, applying to the provided
* target component. This is similar to render() for Surfaces.
*
* @private
* @ignore
*
* @param {Object} target (already rendered) render spec to
* which to apply the transform.
* @return {Object} render spec for this MapModifier, including the
* provided target
*/
MapModifier.prototype.modify = function modify(target) {
if (!this.mapView.isInitialized()) {
this._output.target = target;
return this._output;
}
var cacheInvalidated = false;
// Calculate scale transform
if (this._zoomBase !== undefined) {
var scaling;
if (this._zoomScale) {
if (this._zoomScale instanceof Function) {
scaling = this._zoomScale(this._zoomBase, this.mapView.getZoom());
}
else {
var zoom = (this.mapView.getZoom() - this._zoomBase) + 1;
if (zoom < 0) {
scaling = (1 / (Math.abs(zoom) + 1)) * this._zoomScale;
}
else {
scaling = (1 + zoom) * this._zoomScale;
}
}
}
else {
scaling = Math.pow(2, this.mapView.getZoom() - this._zoomBase);
}
if (this._cache.scaling !== scaling) {
this._cache.scaling = scaling;
this._cache.scale = Transform.scale(scaling, scaling, 1.0);
cacheInvalidated = true;
}
}
else if (this._cache.scale) {
this._cache.scale = null;
this._cache.scaling = null;
cacheInvalidated = true;
}
// Move, rotate, etc... based on position
var position = this._positionGetter ? this._positionGetter() : this._position;
if (position) {
// Offset position
if (this._offset) {
position = {
lat: MapUtility.lat(position) + MapUtility.lat(this._offset),
lng: MapUtility.lng(position) + MapUtility.lng(this._offset)
};
}
// Calculate rotation transform
var rotateTowards = this._rotateTowardsGetter ? this._rotateTowardsGetter() : this._rotateTowards;
if (rotateTowards) {
var rotation = MapUtility.rotationFromPositions(position, rotateTowards);
rotation += this.mapView.getRotation();
if (this._cache.rotation !== rotation) {
this._cache.rotation = rotation;
this._cache.rotate = Transform.rotateZ(rotation);
cacheInvalidated = true;
}
}
else if (this._cache.rotate) {
this._cache.rotate = null;
this._cache.rotation = null;
cacheInvalidated = true;
}
// Calculate translation transform
var point = this.mapView.pointFromPosition(position);
if (!this._cache.point || (point.x !== this._cache.point.x) || (point.y !== this._cache.point.y)) {
this._cache.point = point;
this._cache.translate = Transform.translate(point.x, point.y, 0);
cacheInvalidated = true;
}
}
else if (this._cache.translate) {
this._cache.point = null;
this._cache.translate = null;
cacheInvalidated = true;
}
// Update transformation matrix
if (cacheInvalidated) {
var transform = this._cache.scale;
if (this._cache.rotate) {
transform = transform ? Transform.multiply(this._cache.rotate, transform) : this._cache.rotate;
}
if (this._cache.translate) {
transform = transform ? Transform.multiply(this._cache.translate, transform) : this._cache.translate;
}
this._output.transform = transform;
}
this._output.target = target;
return this._output;
};
module.exports = MapModifier;
});
/**
* This Source Code is licensed under the MIT license. If a copy of the
* MIT-license was not distributed with this file, You can obtain one at:
* http://opensource.org/licenses/mit-license.html.
*
* @author: Hein Rutjes (IjzerenHein)
* @license MIT
* @copyright Gloey Apps, 2014
*/
/**
* The MapStateModifier makes it possible to use transitions to e.g. move a renderable from one geographical
* position to another. If the renderable doesn't require transitions, the use of the lightweight
* and stateless `MapModifier` is strongly preferred.
*
* @module
*/
define('famous-map/MapStateModifier',['require','exports','module','./MapModifier','./MapPositionTransitionable'],function(require, exports, module) {
// import dependencies
var MapModifier = require('./MapModifier');
var MapPositionTransitionable = require('./MapPositionTransitionable');
/**
* @class
* @param {Object} options Options.
* @param {MapView} options.mapView The MapView.
* @param {LatLng} [options.position] Initial geographical coordinates.
* @param {LatLng} [options.offset] Displacement offset in geographical coordinates from the position.
* @param {LatLng} [options.rotateTowards] Position to rotate the renderables towards.
* @param {number} [options.zoomBase] Base zoom-level at which the renderables are displayed in their true size.
* @param {number|function} [options.zoomScale] Custom zoom-scaling factor or function.
* @alias module:MapStateModifier
*/
function MapStateModifier(options) {
this.mapView = options.mapView;
this._positionState = new MapPositionTransitionable(options.position);
this._rotateTowardsState = new MapPositionTransitionable(options.rotateTowards);
this._modifier = new MapModifier({
mapView: this.mapView
});
if (options.position) {
this.setPosition(options.position);
}
if (options.rotateTowards) {
this.rotateTowards(options.rotateTowards);
}
if (options.offset) {
this.setOffset(options.offset);
}
if (options.zoomBase !== undefined) {
this.setZoomBase(options.zoomBase);
}
if (options.zoomScale) {
this.setZoomScale(options.zoomScale);
}
}
/**
* Set the geographical position of the renderables, by adding the new position to the chain of transitions.
*
* @param {LatLng} position New position in geographical coordinates (Latitude, Longitude).
* @param {Transition} [transition] Famo.us transitionable object.
* @param {Function} [callback] callback to call after transition completes.
*/
MapStateModifier.prototype.setPosition = function(position, transition, callback) {
this._positionState.set(position, transition, callback);
return this;
};
/**
* Set the destination geographical position to rotate the renderables towards, by adding them.
* to the chain of transitions.
* The c