blob: f28f0e6ae03f144b799201df44ec3f83e3247592 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
var $ = require('../util/jquery');
var defineClass = require('../util/define-class');
var Destination = require('../destination');
var Place = require('../place');
var DestinationInfo = require('./destination-info');
var DestinationMarker = require('./destination-marker');
var strings = require('../strings').currentLocale;
//named destination marker clients
var SEARCH_CLIENT = 'search';
var Map = defineClass({
publics: {
getBounds: function() {
return this.map.getBounds();
},
addControls: function(controlPosition, $controls) {
var controls = this.map.controls[controlPosition];
$controls.each(function() {
controls.push(this);
});
},
addDestination: function() {
var self = this;
var destination = new Destination();
if (!this.origin) {
this.finalDestination = this.origin = destination;
} else {
this.finalDestination.bindNext(destination);
this.finalDestination = destination;
}
destination.onPlaceChange.add(function(place) {
self.handleDestinationPlaceChange(destination, place);
});
destination.onDeselect.add(function() {
self.handleDestinationDeselect(destination);
});
destination.onSelect.add(function() {
self.handleDestinationSelect(destination);
});
return destination;
},
clearSearchMarkers: function() {
$.each(this.searchMarkers, function() {
this.removeClient(SEARCH_CLIENT);
});
this.searchMarkers = [];
},
closeActiveInfoWindow: function() {
if (this.info) {
this.info.close();
}
},
deselectDestination: function() {
if (this.selectedDestination) {
this.selectedDestination.deselect();
}
},
fitAll: function() {
var geoms = [];
var dest = this.origin;
function addToGeoms() {
geoms.push({ location: this });
}
while (dest) {
if (dest.hasPlace()) {
if (dest.leg && dest.leg.sync) {
$.each(dest.leg.sync.routes[0]['overview_path'], addToGeoms);
}
geoms.push(dest.getPlace().getGeometry());
}
dest = dest.getNext();
}
this.ensureGeomsVisible(geoms);
},
ensureVisible: function(place) {
this.ensureGeomsVisible([place.getGeometry()]);
},
showSearchResults: function(results) {
var self = this;
this.clearSearchMarkers();
this.closeActiveInfoWindow();
this.fitGeoms(results.map(function(result) {
return result.geometry;
}));
if (results.length === 1) {
var place = new Place(results[0]);
/* It would be nice if we could distinguish between an autocomplete
* click and a normal search so that we don't overwrite the search box
* text for the autocomplete click.*/
var dest = this.selectedDestination;
if (dest) {
dest.setPlace(place);
self.createDestinationMarker(dest);
}
} else if (results.length > 1) {
$.each(results, function(i, result) {
var place = new Place(result);
var marker = self.createMarker(place, SEARCH_CLIENT,
DestinationMarker.color.RED);
self.searchMarkers.push(marker);
marker.onClick.add(function() {
var dest = self.selectedDestination;
if (dest) {
dest.setPlace(place);
self.associateDestinationMarker(dest, marker);
}
});
});
}
}
},
privates: {
createMarker: function(place, client, color) {
var self = this;
var marker = new DestinationMarker(this.maps, this.map, place,
client, color);
if (place.hasDetails()) {
marker.onClick.add(function() {
self.showDestinationInfo(marker);
}, true);
}
return marker;
},
createDestinationMarker: function(destination) {
var marker = this.createMarker(destination.getPlace(), destination,
this.getAppropriateDestinationMarkerColor(destination));
this.bindDestinationMarker(destination, marker);
return marker;
},
associateDestinationMarker: function(destination, marker) {
if (!marker.onClick.has(destination.select)) {
marker.pushClient(destination,
this.getAppropriateDestinationMarkerColor(destination));
this.bindDestinationMarker(destination, marker);
}
},
bindDestinationMarker: function(destination, marker) {
var self = this;
marker.onClick.add(destination.select);
function handleSelection() {
marker.setColor(self.getAppropriateDestinationMarkerColor(destination));
}
destination.onSelect.add(handleSelection);
destination.onDeselect.add(handleSelection);
function handleOrdinalChange() {
var destLabel;
if (!destination.hasPrevious()) {
destLabel = strings['Origin'];
marker.setIcon(DestinationMarker.icon.ORIGIN);
} else if (!destination.hasNext()) {
destLabel = strings[destination.getIndex() === 1?
'Destination' : 'Final destination'];
marker.setIcon(DestinationMarker.icon.DESTINATION);
} else {
destLabel = strings.destination(destination.getIndex());
marker.setLabel(destination.getIndex());
}
marker.setDestinationLabel(destLabel);
}
destination.onOrdinalChange.add(handleOrdinalChange);
handleOrdinalChange();
function handlePlaceChange() {
marker.removeClient(destination);
marker.onClick.remove(destination.select);
destination.onSelect.remove(handleSelection);
destination.onDeselect.remove(handleSelection);
destination.onOrdinalChange.remove(handleOrdinalChange);
destination.onPlaceChange.remove(handlePlaceChange);
}
destination.onPlaceChange.add(handlePlaceChange);
},
getAppropriateDestinationMarkerColor: function(destination) {
return destination.isSelected()?
DestinationMarker.color.GREEN : DestinationMarker.color.BLUE;
},
showDestinationInfo: function(destinationMarker) {
if (!this.info) {
this.info = new DestinationInfo(
this.maps, this.map, destinationMarker.place);
} else {
this.info.setPlace(destinationMarker.place);
}
this.info.show(destinationMarker.marker);
},
handleDestinationPlaceChange: function(destination, place) {
if (destination.getPrevious()) {
this.updateLeg(destination);
}
if (destination.getNext()) {
this.updateLeg(destination.getNext());
}
},
handleDestinationDeselect: function(destination) {
this.selectedDestination = null;
destination.onPlaceChange.remove(
this.handleSelectedDestinationPlaceChange);
this.disableLocationSelection();
this.clearSearchMarkers();
},
handleDestinationSelect: function(destination) {
this.deselectDestination();
this.selectedDestination = destination;
destination.onPlaceChange.add(this.handleSelectedDestinationPlaceChange);
this.handleSelectedDestinationPlaceChange(destination.getPlace());
},
handleSelectedDestinationPlaceChange: function(place) {
if (place) {
this.disableLocationSelection();
this.ensureVisible(place);
} else {
this.enableLocationSelection();
}
},
updateLeg: function(destination) {
var self = this;
var maps = this.maps;
var map = this.map;
var a = destination.getPrevious().getPlace();
var b = destination.getPlace();
var leg = destination.leg;
if (leg) {
if (leg.async) {
leg.async.reject();
}
// setMap(null) seems to be the best way to clear the nav route
leg.renderer.setMap(null);
} else {
var renderer = new maps.DirectionsRenderer({
preserveViewport: true,
suppressMarkers: true
});
destination.leg = leg = { renderer: renderer };
}
if (a && b) {
var request = {
origin: a.getLocation(),
destination: b.getLocation(),
travelMode: maps.TravelMode.DRIVING // TODO(rosswang): user choice
};
leg.async = $.Deferred();
this.directionsService.route(request, function(result, status) {
if (status === maps.DirectionsStatus.OK) {
leg.async.resolve(result);
leg.sync = result;
} else {
self.onError({ directionsStatus: status });
leg.async.reject(status);
}
});
leg.async.done(function(result) {
leg.renderer.setDirections(result);
leg.renderer.setMap(map);
self.ensureGeomsVisible(result.routes[0]['overview_path'].map(
function(point) {
return { location: point };
}));
});
}
},
centerOnCurrentLocation: function() {
var self = this;
var maps = this.maps;
var map = this.map;
// https://developers.google.com/maps/documentation/javascript/examples/map-geolocation
if (global.navigator && global.navigator.geolocation) {
global.navigator.geolocation.getCurrentPosition(function(position) {
var latLng = new maps.LatLng(
position.coords.latitude, position.coords.longitude);
map.setCenter(latLng);
self.geocoder.geocode({ location: latLng },
function(results, status) {
if (status === maps.GeocoderStatus.OK &&
self.origin && !self.origin.hasPlace()) {
self.origin.setPlace(new Place(results[0]));
self.createDestinationMarker(self.origin);
}
});
});
}
},
ensureGeomsVisible: function(geoms) {
var curBounds = this.map.getBounds();
if (!geoms.every(function(geom) {
return curBounds.contains(geom.location);
})) {
this.fitGeoms(geoms);
}
},
fitGeoms: function(geoms) {
var curBounds = this.map.getBounds();
var curSize = curBounds.toSpan();
function wontShrink(proposed) {
var size = proposed.toSpan();
return size.lat() >= curSize.lat() || size.lng() >= curSize.lng();
}
if (geoms.length === 1) {
var geom = geoms[0];
if (geom.viewport && wontShrink(geom.viewport)) {
this.map.fitBounds(geom.viewport);
} else {
this.map.panTo(geom.location);
}
} else if (geoms.length > 1) {
this.map.fitBounds(geoms.reduce(function(acc, geom) {
if (geom.viewport) {
acc.union(geom.viewport);
} else {
acc.extend(geom.location);
}
return acc;
}, new this.maps.LatLngBounds()));
}
},
enableLocationSelection: function() {
this.map.setOptions({ draggableCursor: 'auto' });
this.locationSelectionEnabled = true;
},
disableLocationSelection: function() {
this.map.setOptions({ draggableCursor: null });
this.locationSelectionEnabled = false;
},
selectLocation: function(latLng) {
var self = this;
var maps = this.maps;
var dest = this.selectedDestination;
if (dest && this.locationSelectionEnabled) {
self.geocoder.geocode({ location: latLng },
function(results, status) {
if (status === maps.GeocoderStatus.OK) {
dest.setPlace(new Place(results[0]));
self.createDestinationMarker(dest);
}
});
}
}
},
constants: ['$', 'maps'],
events: {
/**
* @param error A union with one of the following keys:
* directionsStatus
*/
onError: 'memory',
/**
* @param bounds
*/
onBoundsChange: ''
},
// https://developers.google.com/maps/documentation/javascript/tutorial
init: function(opts) {
opts = opts || {};
var self = this;
var maps = opts.maps || global.google.maps;
this.maps = maps;
this.navigator = opts.navigator || global.navigator;
this.geocoder = new maps.Geocoder();
this.directionsService = new maps.DirectionsService();
this.$ = $('<div>').addClass('map-canvas');
this.searchMarkers = [];
this.initialConfig = {
center: new maps.LatLng(37.4184, -122.0880), //Googleplex
zoom: 11
};
var map = new maps.Map(this.$[0], this.initialConfig);
this.map = map;
maps.event.addListener(map, 'click', function(e) {
self.selectLocation(e.latLng);
});
maps.event.addListener(map, 'bounds_changed', function() {
self.onBoundsChange(map.getBounds());
});
this.centerOnCurrentLocation();
}
});
module.exports = Map;