Rough unidirectional timeline casting
Change-Id: I317a6afc4a2bd0d0482977e82f51711bd7779b10
diff --git a/.gitignore b/.gitignore
index 36dd53c..68dbe6f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,4 @@
/ifc
/node_modules
/server-root
-/bin
/tmp
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 49f09c4..719e631 100644
--- a/Makefile
+++ b/Makefile
@@ -9,12 +9,12 @@
server_static := $(patsubst src/static/%,server-root/%,$(wildcard src/static/*))
tests := $(patsubst %.js,%,$(shell find test -name "*.js"))
-out_dirs := ifc server-root node_modules bin
+out_dirs := ifc server-root node_modules
.DELETE_ON_ERROR:
.PHONY: all
-all: static js bin
+all: static js
@true
.PHONY: static
@@ -23,11 +23,10 @@
.PHONY: js
js: server-root/bundle.js
-bin:
- @v23 go build -a -o $@/syncbased v.io/syncbase/x/ref/services/syncbase/syncbased
- @touch $@
+.PHONY: ifc
+ifc: ifc/index.js
-ifc: src/ifc/*
+ifc/index.js: src/ifc/*
@VDLPATH=src vdl generate -lang=javascript -js-out-dir=. ifc
node_modules: package.json
@@ -41,7 +40,7 @@
server-root:
@mkdir server-root
-server-root/bundle.js: ifc node_modules $(js_files) | server-root
+server-root/bundle.js: ifc/index.js node_modules $(js_files) | server-root
browserify --debug src/index.js 1> $@
$(server_static): server-root/%: src/static/% | server-root
@@ -56,7 +55,7 @@
test: lint $(tests)
.PHONY: $(tests)
-$(tests): test/%: test/%.js test/* mocks/* ifc node_modules $(js_files)
+$(tests): test/%: test/%.js test/* mocks/* ifc/index.js node_modules $(js_files)
@tape $<
.PHONY: start
@@ -71,7 +70,7 @@
@principal seekblessings --v23.credentials tmp/creds/$(creds)
.PHONY: syncbase
-syncbase: bin
+syncbase:
@bash ./tools/start_services.sh
.PHONY: clean-all
diff --git a/mocks/google-maps.js b/mocks/google-maps.js
index 65639be..0a70759 100644
--- a/mocks/google-maps.js
+++ b/mocks/google-maps.js
@@ -200,7 +200,8 @@
events: {
'bounds_changed': 'public',
- click: 'public'
+ click: 'public',
+ resize: 'public'
},
init: function(canvas) {
@@ -318,6 +319,13 @@
}
});
+function validateEvent(instance, eventName) {
+ //approximate check; just a sanity check during testing
+ if (typeof instance[eventName] !== 'function') {
+ throw instance + ' does not mock event ' + eventName;
+ }
+}
+
maps = {
ControlPosition: ControlPosition,
DirectionsRenderer: DirectionsRenderer,
@@ -334,13 +342,11 @@
event: {
addListener: function(instance, eventName, handler){
- if (eventName in instance) {
- instance[eventName].add(handler);
- } else {
- throw instance + ' does not mock event ' + eventName;
- }
+ validateEvent(instance, eventName);
+ instance[eventName].add(handler);
},
trigger: function(instance, eventName) {
+ validateEvent(instance, eventName);
instance[eventName].apply(instance,
Array.prototype.slice.call(arguments, 2));
}
diff --git a/src/casting-manager.js b/src/casting-manager.js
index 53b1b1d..9bf1c73 100644
--- a/src/casting-manager.js
+++ b/src/casting-manager.js
@@ -132,32 +132,36 @@
var self = this;
var direction = getGestureDirection(v);
- var related = this.getRelatedDevices(direction);
- if (related.size === 1) {
- related.forEach(function(deviceName, owner) {
- self.cast(owner, deviceName, spec).catch(self.onError);
- });
- } else {
- var unknown = this.travelSync.getUnconnectedCastTargets();
-
- if (related.size === 0 && unknown.size === 1) {
- unknown.forEach(function(deviceName, owner) {
- Promise.all([
- self.cast(owner, deviceName, spec),
- self.travelSync.relateDevice(owner, deviceName, {
- direction: direction,
- magnitude: DeviceSync.NEAR
- })
- ]).catch(self.onError);
+ if (direction) {
+ var related = this.getRelatedDevices(direction);
+ if (related.size === 1) {
+ // Use forEach for singleton multimap entry extraction.
+ related.forEach(function(deviceName, owner) {
+ self.cast(owner, deviceName, spec).catch(self.onError);
});
} else {
- var all = this.travelSync.getPossibleCastTargets();
- var other = difference(all, related);
+ var unknown = this.travelSync.getUnconnectedCastTargets();
- if (related.size > 0 || unknown.size > 0 || other.size > 0) {
- this.onAmbiguousCast(related, unknown, other);
+ if (related.size === 0 && unknown.size === 1) {
+ // Use forEach for singleton multimap entry extraction.
+ unknown.forEach(function(deviceName, owner) {
+ Promise.all([
+ self.cast(owner, deviceName, spec),
+ self.travelSync.relateDevice(owner, deviceName, {
+ direction: direction,
+ magnitude: DeviceSync.NEAR
+ })
+ ]).catch(self.onError);
+ });
} else {
- this.onNoNearbyDevices();
+ var all = this.travelSync.getPossibleCastTargets();
+ var other = difference(all, related);
+
+ if (related.size > 0 || unknown.size > 0 || other.size > 0) {
+ this.onAmbiguousCast(related, unknown, other);
+ } else {
+ this.onNoNearbyDevices();
+ }
}
}
}
@@ -168,13 +172,18 @@
return this.travelSync.cast(targetOwner, targetDeviceName, spec)
.then(function() {
- self.onCast(spec);
+ self.onSendCast(targetOwner, targetDeviceName, spec);
}, this.onError);
}
},
events: {
- onCast: '',
+ /**
+ * @param targetOwner target device owner
+ * @param targetDeviceName target device name
+ * @param spec the cast spec, as given to makeCastable's opts.
+ */
+ onSendCast: '',
/**
* @param related owner => device multimap of related cast candidates
* @param unknown owner => device multimap of unconnected cast candidates
diff --git a/src/components/destination-search.js b/src/components/destination-search.js
index 2dffcc9..e5e64c4 100644
--- a/src/components/destination-search.js
+++ b/src/components/destination-search.js
@@ -2,81 +2,122 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+require('es6-shim');
+
var $ = require('../util/jquery');
var defineClass = require('../util/define-class');
+var debug = require('../debug');
+var Place = require('../place');
+
var DestinationSearch = defineClass({
publics: {
clear: function() {
- this.setPlace(null);
+ var async = this.setPlace(null);
this.$searchBox.prop('value', '');
+ return async;
},
enable: function() {
this.$searchBox.removeAttr('disabled');
+ return Promise.resolve();
},
disable: function() {
this.$searchBox.attr('disabled', 'disabled');
+ return Promise.resolve();
},
focus: function() {
this.$.find('input:visible').focus();
+ return Promise.resolve();
},
hasFocus: function() {
- return this.$.find(':focus').length > 0;
+ return Promise.resolve(this.$.find(':focus').length > 0);
},
setSearchBounds: function(bounds) {
this.searchBox.setBounds(bounds);
+ return Promise.resolve();
},
select: function() {
this.$.addClass('selected');
+ return Promise.resolve();
},
deselect: function() {
- if (this.isSelected()) {
- this.$.removeClass('selected');
- this.onDeselect();
- }
+ var self = this;
+ return this.isSelected().then(function(isSelected) {
+ if (isSelected) {
+ self.$.removeClass('selected');
+ self.onDeselect();
+ }
+ });
},
isSelected: function() {
- return this.$.hasClass('selected');
+ return Promise.resolve(this.$.hasClass('selected'));
},
getPlace: function() {
- return this.place;
+ return Promise.resolve(this.place);
},
setPlace: function(place) {
+ var self = this;
var prev = this.place;
- if (prev !== place) {
+ if (!Place.equal(prev, place)) {
this.place = place;
this.setAutocomplete(!place);
var newValue;
if (place) {
- newValue = place.getSingleLine();
- } else if (!this.hasFocus()) {
- newValue = '';
- }
- if (newValue !== undefined) {
- this.$searchBox.prop('value', newValue);
+ newValue = Promise.resolve(place.getSingleLine());
+ } else {
+ newValue = this.hasFocus().then(function(hasFocus) {
+ /* We only want to clear when we don't have focus because if we have
+ * focus, we're actively editing the text even if it may be
+ * presently invalid. */
+ if (!hasFocus) {
+ return '';
+ }
+ });
}
- this.onPlaceChange(place, prev);
+ /* Since making all timeline UI asynchronous, we introduce a race
+ * condition where a destination deselect starts a chain of events to
+ * clear a place, then a reselect starts a chain of events to set it,
+ * but since the clear includes an asynchronous focus check, it takes
+ * longer to complete and can overwrite the effect of the set. So, we
+ * need to queue the aftereffects. */
+
+ this.setValueInProgress = this.setValueInProgress
+ .catch($.noop)
+ .then(function() {
+ return newValue;
+ })
+ .then(function(newValue) {
+ if (newValue !== undefined) {
+ self.$searchBox.prop('value', newValue);
+ }
+
+ self.onPlaceChange(place, prev);
+ });
+ return this.setValueInProgress;
+ } else {
+ return Promise.resolve();
}
},
setPlaceholder: function(placeholder) {
this.$searchBox.attr('placeholder', placeholder);
+ return Promise.resolve();
},
getValue: function() {
- return this.$searchBox.prop('value');
+ return Promise.resolve(this.$searchBox.prop('value'));
}
},
@@ -123,13 +164,15 @@
inputKey: function(e) {
if (e.which === 13) {
- this.onSubmit(this.getValue());
+ this.getValue().then(this.onSubmit).catch(debug.log);
}
e.stopPropagation();
}
},
events: [
+ 'onDeselect',
+
/**
* @param event jQuery Event object for text box focus event.
*/
@@ -146,8 +189,6 @@
*/
'onSearch',
- 'onDeselect',
-
/**
* Event fired when the enter key is pressed. This is distinct from the
* onSearch event, which is fired when valid location properties are chosen,
@@ -163,6 +204,8 @@
init: function(maps) {
var self = this;
+ this.setValueInProgress = Promise.resolve();
+
var $searchBox = $.merge($('<input>'), $('<input>'))
.attr('type', 'text')
//to make dummy box consistent with search
@@ -185,7 +228,9 @@
this.autocomplete = true;
maps.event.addListener(this.searchBox, 'places_changed', function() {
- self.onSearch(self.searchBox.getPlaces());
+ self.onSearch(self.searchBox.getPlaces().map(function(result) {
+ return new Place(result);
+ }));
});
}
});
diff --git a/src/components/map-widget.js b/src/components/map-widget.js
index 6659ad3..219b74f 100644
--- a/src/components/map-widget.js
+++ b/src/components/map-widget.js
@@ -126,7 +126,7 @@
this.closeActiveInfoWindow();
this.fitGeoms(results.map(function(result) {
- return result.geometry;
+ return result.getGeometry();
}));
var dest = this.selectedDestination;
@@ -134,11 +134,9 @@
/* 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.*/
- dest.setPlace(new Place(results[0]));
+ dest.setPlace(results[0]);
} else if (results.length > 0) {
- $.each(results, function(i, result) {
- var place = new Place(result);
-
+ $.each(results, function(i, place) {
var marker = self.getOrCreateMarker(place, SEARCH_CLIENT,
DestinationMarker.color.RED, null, false);
self.searchMarkers.push(marker);
diff --git a/src/components/timeline-client.js b/src/components/timeline-client.js
new file mode 100644
index 0000000..fe5ced4
--- /dev/null
+++ b/src/components/timeline-client.js
@@ -0,0 +1,194 @@
+// 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 defineClass = require('../util/define-class');
+
+var ifcx = require('../ifc/conversions');
+
+var destDefs = {
+ getPlace: function() {
+ var self = this;
+ return this.outer.service.getDestinationPlace(this.outer.context, this.id)
+ .then(function(place) {
+ return ifcx.toPlace(self.outer.dependencies, place);
+ });
+ },
+
+ setPlace: function(place) {
+ return this.outer.service.setDestinationPlace(this.outer.context, this.id,
+ ifcx.fromPlace(place) || null);
+ },
+
+ setPlaceholder: function(placeholder) {
+ return this.outer.service.setDestinationPlaceholder(this.outer.context,
+ this.id, placeholder);
+ },
+
+ setSearchBounds: function(bounds) {
+ return this.outer.service.setDestinationSearchBounds(this.outer.context,
+ this.id, ifcx.fromLatLngBounds(bounds));
+ }
+};
+
+function bindDestinationMethod(localMethod, remoteMethod) {
+ destDefs[localMethod] = function() {
+ return this.outer.service[remoteMethod](this.outer.context, this.id);
+ };
+}
+
+['clear', 'enable', 'disable', 'focus', 'select', 'deselect'].forEach(
+function(method) {
+ bindDestinationMethod(method, method + 'Destination');
+});
+
+bindDestinationMethod('hasFocus', 'destinationHasFocus');
+bindDestinationMethod('isSelected', 'isDestinationSelected');
+bindDestinationMethod('getValue', 'getDestinationValue');
+
+var TimelineClient = defineClass({
+ publics: {
+ disableAdd: function() {
+ return this.service.disableAdd(this.context);
+ },
+
+ enableAdd: function() {
+ return this.service.enableAdd(this.context);
+ },
+
+ add: function(i) {
+ return this.service.add(this.context, ifcx.box(i))
+ .then(this.getDestination);
+ },
+
+ get: function(i) {
+ return this.service.get(this.context, ifcx.box(i))
+ .then(this.getDestinationOrDestinations);
+ },
+
+ remove: function(i) {
+ return this.service.get(this.context, ifcx.box(i))
+ .then(this.getDestination);
+ },
+
+ setSearchBounds: function(bounds) {
+ return this.service.setSearchBounds(this.context,
+ ifcx.fromLatLngBounds(bounds));
+ }
+ },
+
+ privates: {
+ destinationClient: defineClass.innerClass({
+ publics: destDefs,
+
+ privates: {
+ /**
+ * @param localEventName the name of the event on the client object
+ * @param remoteEventName the name of the streaming API serving the
+ * event on the remote server
+ * @param translateArgs a function taking the remote event data and
+ * returning an array of arguments or returning a promise resolving to
+ * an array of arguments to be passed to local event handlers.
+ */
+ bindEvent: function(localEventName, remoteEventName, translateArgs) {
+ var self = this;
+
+ var event = this.outer.service[remoteEventName]
+ (this.outer.context, this.id);
+ event.catch(this.outer.onError);
+ event.stream.on('error', this.outer.onError);
+ event.stream.on('data', function(e) {
+ Promise.resolve(translateArgs && translateArgs(e))
+ .then(function(args) {
+ self[localEventName].apply(self, args);
+ }).catch(self.outer.onError);
+ });
+ }
+ },
+
+ events: [
+ 'onDeselect',
+ 'onFocus',
+ 'onPlaceChange',
+ 'onSearch',
+ 'onSubmit'
+ ],
+
+ init: function(id) {
+ var self = this;
+
+ this.id = id;
+
+ this.bindEvent('onDeselect', 'onDestinationDeselect');
+ this.bindEvent('onFocus', 'onDestinationFocus');
+ this.bindEvent('onPlaceChange', 'onDestinationPlaceChange',
+ function(e) {
+ return Promise.all([
+ ifcx.toPlace(self.outer.dependencies, e.place),
+ ifcx.toPlace(self.outer.dependencies, e.previous)
+ ]);
+ });
+ this.bindEvent('onSearch', 'onDestinationSearch', function(e) {
+ return Promise.all(e.places.map(function(place) {
+ return ifcx.toPlace(self.outer.dependencies, place);
+ })).then(function(places) {
+ return [places];
+ });
+ });
+ this.bindEvent('onSubmit', 'onDestinationSubmit', function(e) {
+ return [e.value];
+ });
+ }
+ }),
+
+ getDestination: function(id) {
+ if (!id) {
+ return null;
+ }
+
+ var destClient = this.destinations[id];
+ if (!destClient) {
+ destClient = this.destinations[id] = this.destinationClient(id);
+ }
+ return destClient;
+ },
+
+ getDestinationOrDestinations: function(idOrIds) {
+ return idOrIds.ids?
+ idOrIds.ids.map(this.getDestination) : this.getDestination(idOrIds.id);
+ },
+
+ bindEvent: function(eventName, translateArgs) {
+ var self = this;
+
+ var event = this.service[eventName](this.context);
+ event.catch(this.onError);
+ event.stream.on('error', this.onError);
+ event.stream.on('data', function(e) {
+ self[eventName].apply(self, translateArgs && translateArgs(e));
+ });
+ }
+ },
+
+ events: {
+ onAddClick: '',
+ onDestinationAdd: '',
+ onError: 'memory'
+ },
+
+ init: function(context, service, dependencies) {
+ var self = this;
+
+ this.context = context;
+ this.service = service;
+ this.dependencies = dependencies;
+ this.destinations = {};
+
+ this.bindEvent('onAddClick');
+ this.bindEvent('onDestinationAdd', function(e) {
+ return [ self.getDestination(e.id) ];
+ });
+ }
+});
+
+module.exports = TimelineClient;
diff --git a/src/components/timeline-server.js b/src/components/timeline-server.js
new file mode 100644
index 0000000..638baf1
--- /dev/null
+++ b/src/components/timeline-server.js
@@ -0,0 +1,219 @@
+// 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.
+
+require('es6-shim');
+
+var $ = require('../util/jquery');
+
+var ifcx = require('../ifc/conversions');
+var uuid = require('uuid');
+
+var vdlTravel = require('../../ifc');
+
+function addEventStreamListener(context, event, eventFactory, $stream) {
+ return new Promise(function(resolve, reject) {
+ function listener() {
+ try {
+ var buffOk = $stream.write(eventFactory.apply(context, arguments), null,
+ function(err) {
+ if (err) {
+ reject(err);
+ }
+ });
+ if (!buffOk) {
+ reject('Event buffer full.');
+ }
+ } catch (err) {
+ reject(err);
+ }
+ }
+
+ event.add(listener);
+ $stream.on('end', function() {
+ event.remove(listener);
+ resolve();
+ });
+ $stream.on('error', reject);
+ });
+}
+
+function multiplexedEvent(eventName, eventFactory) {
+ return function(ctx, serverCall, id, $stream) {
+ var event = this.destinations[id][eventName];
+ function multiplexedFactory() {
+ // Prepend id to the arg list.
+ var args = [id];
+ Array.prototype.push.apply(args, arguments);
+ return eventFactory.apply(this, args);
+ }
+ return addEventStreamListener(this, event, multiplexedFactory, $stream);
+ };
+}
+
+function event(eventName, eventFactory) {
+ return function(ctx, serverCall, $stream) {
+ var event = this.timeline[eventName];
+ return addEventStreamListener(this, event, eventFactory, $stream);
+ };
+}
+
+/**
+ * We can't defineClass this because v23 checks length property of each member
+ * function, and we'd have to new Function each one to preserve that.
+ *
+ * @param timeline the timeline control to serve.
+ * @param dependencies {placesService, maps}
+ */
+function TimelineService(timeline, dependencies) {
+ this.timeline = timeline;
+ this.dependencies = dependencies;
+ this.destinations = {};
+ this.destinationIds = new Map();
+
+ this._identifyDestination = identifyDestination.bind(this);
+ this._identifyDestinationOrDestinations =
+ identifyDestinationOrDestinations.bind(this);
+
+ timeline.onDestinationAdd.add(this._identifyDestination);
+ timeline.get().then(function(destinations) {
+ destinations.forEach(this._identifyDestination);
+ });
+}
+
+TimelineService.prototype = new vdlTravel.Timeline();
+
+function identifyDestination(destination) {
+ if (!destination) {
+ return '';
+ }
+
+ var id = this.destinationIds.get(destination);
+ if (!id) {
+ id = uuid.v4();
+ this.destinations[id] = destination;
+ this.destinationIds.set(destination, id);
+ }
+ return id;
+}
+
+function identifyDestinationOrDestinations(polyd) {
+ return new vdlTravel.IdOrIds($.isArray(polyd)?
+ { ids: polyd.map(this._identifyDestination) } :
+ { id: this._identifyDestination(polyd) });
+}
+
+$.extend(TimelineService.prototype, {
+ destinationHasFocus: function(ctx, serverCall, id) {
+ return this.destinations[id].hasFocus();
+ },
+
+ isDestinationSelected: function(ctx, serverCall, id) {
+ return this.destinations[id].isSelected();
+ },
+
+ getDestinationPlace: function(ctx, serverCall, id) {
+ return this.destinations[id].getPlace().then(ifcx.fromPlace);
+ },
+
+ setDestinationPlace: function(ctx, serverCall, id, place) {
+ var destination = this.destinations[id];
+ return ifcx.toPlace(this.dependencies, place).then(destination.setPlace);
+ },
+
+ setDestinationPlaceholder: function(ctx, serverCall, id, placeholder) {
+ this.destinations[id].setPlaceholder(placeholder);
+ },
+
+ setDestinationSearchBounds: function(ctx, serverCall, id, bounds) {
+ this.destinations[id].setSearchBounds(
+ ifcx.toLatLngBounds(this.dependencies.maps, bounds));
+ },
+
+ getDestinationValue: function(ctx, serverCall, id) {
+ return this.destinations[id].getValue();
+ },
+
+ onDestinationDeselect: multiplexedEvent('onDeselect',
+ function(id) {
+ return new vdlTravel.MultiplexedEvent({
+ source: id
+ });
+ }),
+
+ onDestinationFocus: multiplexedEvent('onFocus',
+ function(id) {
+ return new vdlTravel.MultiplexedEvent({
+ source: id
+ });
+ }),
+
+ onDestinationPlaceChange: multiplexedEvent('onPlaceChange',
+ function(id, place, previous) {
+ return new vdlTravel.DestinationPlaceChangeEvent({
+ source: id,
+ place: ifcx.fromPlace(place),
+ previous: ifcx.fromPlace(previous)
+ });
+ }),
+
+ onDestinationSearch: multiplexedEvent('onSearch',
+ function(id, places) {
+ return new vdlTravel.DestinationSearchEvent({
+ source: id,
+ places: places.map(ifcx.fromPlace)
+ });
+ }),
+
+ onDestinationSubmit: multiplexedEvent('onSubmit',
+ function(id, value) {
+ return new vdlTravel.DestinationSubmitEvent({
+ source: id,
+ value: value
+ });
+ }),
+
+ setSearchBounds: function(ctx, serverCall, bounds) {
+ this.timeline.setSearchBounds(
+ ifcx.toLatLngBounds(this.dependencies.maps, bounds));
+ },
+
+ onAddClick: event('onAddClick',
+ function() {
+ return new vdlTravel.Event();
+ }),
+
+ onDestinationAdd: event('onDestinationAdd',
+ function(destinationSearch) {
+ return new vdlTravel.DestinationAddEvent({
+ id: this._identifyDestination(destinationSearch)
+ });
+ })
+});
+
+['clear', 'enable', 'disable', 'focus', 'select', 'deselect']
+.forEach(function(method) {
+ TimelineService.prototype[method + 'Destination'] =
+ function(ctx, serverCall, id) {
+ this.destinations[id][method]();
+ };
+});
+
+['disableAdd', 'enableAdd'].forEach(function(method) {
+ TimelineService.prototype[method] = function(ctx, serverCall) {
+ this.timeline[method]();
+ };
+});
+
+['add', 'remove'].forEach(function(method) {
+ TimelineService.prototype[method] = function(ctx, serverCall, i) {
+ return this.timeline[method](ifcx.unbox(i)).then(this._identifyDestination);
+ };
+});
+
+TimelineService.prototype.get = function(ctx, serverCall, i) {
+ return this.timeline.get(ifcx.unbox(i))
+ .then(this._identifyDestinationOrDestinations);
+};
+
+module.exports = TimelineService;
diff --git a/src/components/timeline.js b/src/components/timeline.js
index 10da699..31d2209 100644
--- a/src/components/timeline.js
+++ b/src/components/timeline.js
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+require ('es6-shim');
+
var $ = require('../util/jquery');
var defineClass = require('../util/define-class');
@@ -12,10 +14,12 @@
publics: {
disableAdd: function() {
this.addButton.disable();
+ return Promise.resolve();
},
enableAdd: function() {
this.addButton.enable();
+ return Promise.resolve();
},
add: function(i) {
@@ -30,16 +34,18 @@
controls.splice(i, 0, destinationSearch);
}
- return destinationSearch;
+ this.onDestinationAdd(destinationSearch);
+
+ return Promise.resolve(destinationSearch);
},
get: function(i) {
if (i === undefined) {
- return this.controls.slice(0);
+ return Promise.resolve(this.controls.slice(0));
} else if (i >= 0) {
- return this.controls[i];
+ return Promise.resolve(this.controls[i]);
} else if (i < 0) {
- return this.controls[this.controls.length + i];
+ return Promise.resolve(this.controls[this.controls.length + i]);
}
},
@@ -54,12 +60,25 @@
if (removed) {
removed.$.remove();
}
- return removed;
+ return Promise.resolve(removed);
+ },
+
+ setSearchBounds: function(bounds) {
+ return Promise.all(this.controls.map(function(control) {
+ return control.setSearchBounds(bounds);
+ }));
}
},
constants: [ '$' ],
- events: [ 'onAddClick' ],
+ events: [
+ 'onAddClick',
+
+ /**
+ * @param destinationSearch
+ */
+ 'onDestinationAdd'
+ ],
init: function(maps) {
this.maps = maps;
diff --git a/src/destination.js b/src/destination.js
index 624149f..c794848 100644
--- a/src/destination.js
+++ b/src/destination.js
@@ -4,6 +4,8 @@
var defineClass = require('./util/define-class');
+var Place = require('./place');
+
var Destination = defineClass({
publics: {
getIndex: function() {
@@ -20,7 +22,7 @@
setPlace: function(place) {
var prev = this.place;
- if (prev !== place) {
+ if (!Place.equal(prev, place)) {
this.place = place;
this.onPlaceChange(place, prev);
}
diff --git a/src/ifc/conversions.js b/src/ifc/conversions.js
new file mode 100644
index 0000000..6cc210d
--- /dev/null
+++ b/src/ifc/conversions.js
@@ -0,0 +1,51 @@
+// 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.
+
+require('es6-shim');
+
+var vdlTravel = require('../../ifc');
+
+var Place = require('../place');
+
+module.exports = {
+ box: function(i) {
+ return i === undefined || i === null? i : new vdlTravel.Int16({ value: i });
+ },
+
+ unbox: function(ifc) {
+ return ifc && ifc.value;
+ },
+
+ toPlace: function(dependencies, ifc) {
+ return ifc? Place.fromObject(dependencies, ifc) : Promise.resolve();
+ },
+
+ fromPlace: function(place) {
+ return place && new vdlTravel.Place(place.toObject());
+ },
+
+ toLatLng: function(maps, ifc) {
+ return new maps.LatLng(ifc.lat, ifc.lng);
+ },
+
+ fromLatLng: function(latlng) {
+ return new vdlTravel.LatLng({
+ lat: latlng.lat(),
+ lng: latlng.lng()
+ });
+ },
+
+ toLatLngBounds: function(maps, ifc) {
+ return new maps.LatLngBounds(
+ module.exports.toLatLng(maps, ifc.sw),
+ module.exports.toLatLng(maps, ifc.ne));
+ },
+
+ fromLatLngBounds: function(bounds) {
+ return new vdlTravel.LatLngBounds({
+ sw: module.exports.fromLatLng(bounds.getSouthWest()),
+ ne: module.exports.fromLatLng(bounds.getNorthEast())
+ });
+ }
+};
\ No newline at end of file
diff --git a/src/ifc/timeline.vdl b/src/ifc/timeline.vdl
new file mode 100644
index 0000000..e45ec4b
--- /dev/null
+++ b/src/ifc/timeline.vdl
@@ -0,0 +1,37 @@
+// 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.
+
+package ifc
+
+type Timeline interface {
+ ClearDestination(id Id) error
+ EnableDestination(id Id) error
+ DisableDestination(id Id) error
+ FocusDestination(id Id) error
+ DestinationHasFocus(id Id) (bool | error)
+ SelectDestination(id Id) error
+ DeselectDestination(id Id) error
+ IsDestinationSelected(id Id) (bool | error)
+ GetDestinationPlace(id Id) (?Place | error)
+ SetDestinationPlace(id Id, place ?Place) error
+ SetDestinationPlaceholder(id Id, placeholder string) error
+ SetDestinationSearchBounds(id Id, bounds LatLngBounds) error
+ GetDestinationValue(id Id) (string | error)
+
+ OnDestinationDeselect(id Id) stream<_, MultiplexedEvent> error
+ OnDestinationFocus(id Id) stream<_, MultiplexedEvent> error
+ OnDestinationPlaceChange(id Id) stream<_, DestinationPlaceChangeEvent> error
+ OnDestinationSearch(id Id) stream<_, DestinationSearchEvent> error
+ OnDestinationSubmit(id Id) stream<_, DestinationSubmitEvent> error
+
+ DisableAdd() error
+ EnableAdd() error
+ Add(i ?Int16) (Id | error)
+ Get(i ?Int16) (IdOrIds | error)
+ Remove(i ?Int16) (Id | error)
+ SetSearchBounds(bounds LatLngBounds) error
+
+ OnAddClick() stream<_, Event> error
+ OnDestinationAdd() stream<_, DestinationAddEvent> error
+}
diff --git a/src/ifc/types.vdl b/src/ifc/types.vdl
index 9dc1401..b171441 100644
--- a/src/ifc/types.vdl
+++ b/src/ifc/types.vdl
@@ -4,7 +4,61 @@
package ifc
+type Id string
+type Latitude float32
+type Longitude float32
+
+type IdOrIds union {
+ Id Id
+ Ids []Id
+}
+
+// required for optional ints
+type Int16 struct {
+ Value int16
+}
+
type CastSpec struct {
PanelName string
}
+type LatLng struct {
+ Lat Latitude
+ Lng Longitude
+}
+
+type LatLngBounds struct {
+ Sw LatLng
+ Ne LatLng
+}
+
+type Place struct {
+ PlaceId Id
+}
+
+type Event struct {
+}
+
+type MultiplexedEvent struct {
+ Source Id
+}
+
+type DestinationPlaceChangeEvent struct {
+ Source Id
+ Place ?Place
+ Previous ?Place
+}
+
+type DestinationSearchEvent struct {
+ Source Id
+ Places []Place
+}
+
+type DestinationSubmitEvent struct {
+ Source Id
+ Value string
+}
+
+type DestinationAddEvent struct {
+ Id Id
+}
diff --git a/src/naming.js b/src/naming.js
index 02a23ef..8d1be3b 100644
--- a/src/naming.js
+++ b/src/naming.js
@@ -18,8 +18,9 @@
return vanadium.naming.join(appMount(username), deviceName);
}
-function rpcMount(username, deviceName) {
- return vanadium.naming.join(deviceMount(username, deviceName), 'rpc');
+function rpcMount(username, deviceName, serviceName) {
+ return vanadium.naming.join(deviceMount(username, deviceName),
+ serviceName || 'rpc');
}
function mountNames(id) {
@@ -27,7 +28,10 @@
user: userMount(id.username),
app: appMount(id.username),
device: deviceMount(id.username, id.deviceName),
- rpc: rpcMount(id.username, id.deviceName)
+ rpc: rpcMount(id.username, id.deviceName),
+ rpcMount: function(serviceName) {
+ return rpcMount(id.username, id.deviceName, serviceName);
+ }
};
}
diff --git a/src/place.js b/src/place.js
index 5096a9d..cc93922 100644
--- a/src/place.js
+++ b/src/place.js
@@ -2,28 +2,34 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-var Deferred = require('vanadium/src/lib/deferred');
+require('es6-shim');
var defineClass = require('./util/define-class');
var Place = defineClass({
statics: {
+ /**
+ * @param dependencies {placesService, maps}
+ * @param obj the plain object representation of the place
+ */
fromObject: function(dependencies, obj) {
- var async = new Deferred();
+ return new Promise(function(resolve, reject) {
+ if (obj.placeId) {
+ dependencies.placesService.getDetails(obj, function(place, status) {
+ if (status === dependencies.maps.places.PlacesServiceStatus.OK) {
+ resolve(new Place(place));
+ } else {
+ reject(status);
+ }
+ });
+ } else {
+ reject('Deserialization not supported.'); //TODO(rosswang)
+ }
+ });
+ },
- if (obj.placeId) {
- dependencies.placesService.getDetails(obj, function(place, status) {
- if (status === dependencies.maps.places.PlacesServiceStatus.OK) {
- async.resolve(new Place(place));
- } else {
- async.reject(status);
- }
- });
- } else {
- async.reject('Deserialization not supported.'); //TODO(rosswang)
- }
-
- return async.promise;
+ equal: function(a, b) {
+ return a === b || a && b && a.toKey() === b.toKey();
}
},
diff --git a/src/strings.js b/src/strings.js
index 6f4ac8d..8327e1f 100644
--- a/src/strings.js
+++ b/src/strings.js
@@ -26,6 +26,9 @@
add: function(object) {
return 'Add ' + object.toLowerCase();
},
+ notCastable: function(feature) {
+ return 'The ' + feature + ' feature is not castable.';
+ },
castingTooltip: 'To cast a panel to a nearby device, middle-click and ' +
'drag (or left-right-click and drag) the panel towards the target ' +
'device.',
diff --git a/src/sync-util/device-sync.js b/src/sync-util/device-sync.js
index 4245782..a126179 100644
--- a/src/sync-util/device-sync.js
+++ b/src/sync-util/device-sync.js
@@ -72,6 +72,20 @@
}
}
+var RE = 6.371e6;
+
+function cartesian(geo) {
+ var lat = geo.latitude * Math.PI / 180;
+ var lng = geo.longitude * Math.PI / 180;
+ var planeFactor = Math.cos(lng);
+
+ return {
+ x: RE * Math.cos(lat) * planeFactor,
+ y: RE * Math.sin(lat) * planeFactor,
+ z: RE * Math.sin(lng)
+ };
+}
+
var DeviceSync = defineClass({
statics: {
LEFT: LEFT,
diff --git a/src/travel.js b/src/travel.js
index 55c16ca..820bab1 100644
--- a/src/travel.js
+++ b/src/travel.js
@@ -16,6 +16,8 @@
var Messages = require('./components/messages');
var Message = require('./components/message');
var Timeline = require('./components/timeline');
+var TimelineClient = require('./components/timeline-client');
+var TimelineService = require('./components/timeline-server');
var CastingManager = require('./casting-manager');
var Destinations = require('./destinations');
@@ -29,41 +31,6 @@
var naming = require('./naming');
var strings = require('./strings').currentLocale;
-function bindControlToDestination(control, destination) {
- function updateOrdinal() {
- handleDestinationOrdinalUpdate(control, destination);
- }
-
- if (destination) {
- destination.onPlaceChange.add(control.setPlace);
- destination.onSelect.add(control.select);
- destination.onDeselect.add(control.deselect);
- destination.onOrdinalChange.add(updateOrdinal);
- control.setPlace(destination.getPlace());
- /* Since these controls are 1:1 with destinations, we don't want to stay in
- * a state where the control has invalid text but the destination is still
- * valid; that would be confusing to the user (e.g. abandoned query string
- * "restaurants" for destination 4 Privet Drive.) */
- control.onPlaceChange.add(destination.setPlace);
- }
-
- updateOrdinal();
-
- if (destination && destination.isSelected()) {
- control.select();
- } else {
- control.deselect();
- }
-
- return destination? function unbind() {
- destination.onPlaceChange.remove(control.setPlace);
- destination.onSelect.remove(control.select);
- destination.onDeselect.remove(control.deselect);
- destination.onOrdinalChange.remove(updateOrdinal);
- control.onPlaceChange.remove(destination.setPlace);
- } : $.noop;
-}
-
function buildStatusErrorStringMap(statusClass, stringGroup) {
var dict = {};
$.each(statusClass, function(name, value) {
@@ -73,7 +40,8 @@
}
function handleDestinationOrdinalUpdate(control, destination) {
- control.setPlaceholder(describeDestination.descriptionOpenEnded(destination));
+ return control.setPlaceholder(
+ describeDestination.descriptionOpenEnded(destination));
}
var CMD_REGEX = /\/(\S*)(?:\s+(.*))?/;
@@ -134,70 +102,151 @@
} else {
this.error(strings['Trip is still initializing.']);
}
+ },
+
+ castTimeline: function() {
+
}
},
privates: {
- handleDestinationAdd: function(destination) {
- var map = this.map;
-
- var control = this.timeline.add(destination.getIndex());
- bindControlToDestination(control, destination);
-
- control.setSearchBounds(map.getBounds());
- map.onBoundsChange.add(control.setSearchBounds);
-
- control.onFocus.add(function() {
- if (!destination.isSelected()) {
- map.closeActiveInfoWindow();
- destination.select();
- }
- });
-
- control.onSearch.add(function(results) {
- /* There seems to be a bug where if you click a search suggestion (for
- * a query, not a resolved location) in autocomplete, the input box
- * under it gets clicked and focused... I haven't been able to figure
- * out why. */
- control.focus();
-
- map.showSearchResults(results);
- });
-
- if (!destination.hasNext()) {
- this.timeline.disableAdd();
- var oldLast = this.timeline.get(-2);
- if (oldLast) {
- this.unbindLastDestinationSearchEvents(oldLast);
- }
- this.bindLastDestinationSearchEvents(control);
- }
-
- this.bindMiniFeedback(destination);
-
- return {
- destination: destination,
- control: control
+ trap: function(asyncMethod) {
+ var self = this;
+ return function() {
+ return asyncMethod.apply(this, arguments).catch(self.error);
};
},
- handleDestinationRemove: function(destination) {
- var index = destination.getIndex();
- this.unbindLastDestinationSearchEvents(this.timeline.remove(index));
+ bindControlToDestination: function(control, destination) {
+ var asyncs = [];
- if (index >= this.destinations.count()) {
- var lastControl = this.timeline.get(-1);
- if (lastControl) {
- this.bindLastDestinationSearchEvents(lastControl);
- this.handleLastPlaceChange(lastControl.getPlace());
- }
+ function updateOrdinalAsync() {
+ return handleDestinationOrdinalUpdate(control, destination);
}
- //TODO(rosswang): reselect?
+
+ var setPlace, select, deselect, updateOrdinal;
+
+ if (destination) {
+ setPlace = this.trap(control.setPlace);
+ select = this.trap(control.select);
+ deselect = this.trap(control.deselect);
+ updateOrdinal = this.trap(updateOrdinalAsync);
+
+ destination.onPlaceChange.add(setPlace);
+ destination.onSelect.add(select);
+ destination.onDeselect.add(deselect);
+ destination.onOrdinalChange.add(updateOrdinal);
+ asyncs.push(control.setPlace(destination.getPlace()));
+ /* Since these controls are 1:1 with destinations, we don't want to stay
+ * in a state where the control has invalid text but the destination is
+ * still valid; that would be confusing to the user (e.g. abandoned
+ * query string "restaurants" for destination 4 Privet Drive.) */
+ control.onPlaceChange.add(destination.setPlace);
+ }
+
+ asyncs.push(updateOrdinalAsync());
+
+ if (destination && destination.isSelected()) {
+ asyncs.push(control.select());
+ } else {
+ asyncs.push(control.deselect());
+ }
+
+ var unbind = destination? function() {
+ destination.onPlaceChange.remove(setPlace);
+ destination.onSelect.remove(select);
+ destination.onDeselect.remove(deselect);
+ destination.onOrdinalChange.remove(updateOrdinal);
+ control.onPlaceChange.remove(destination.setPlace);
+ } : $.noop;
+
+ return Promise.all(asyncs).then(function() {
+ return unbind;
+ }, function(err) {
+ unbind();
+ throw err;
+ });
+ },
+
+ handleDestinationAdd: function(destination) {
+ var self = this;
+
+ this.addDestinationToTimeline(this.timeline, destination)
+ .then(function() {
+ self.bindMiniFeedback(destination);
+ }).catch(this.error);
+ },
+
+ addDestinationToTimeline: function(timeline, destination) {
+ var self = this;
+ return timeline.add(destination.getIndex()).then(function(control) {
+ self.bindControlToDestination(control, destination);
+
+ var asyncs = [control.setSearchBounds(self.map.getBounds())];
+
+ control.onFocus.add(function() {
+ if (!destination.isSelected()) {
+ self.map.closeActiveInfoWindow();
+ destination.select();
+ }
+ });
+
+ control.onSearch.add(function(results) {
+ /* There seems to be a bug where if you click a search suggestion (for
+ * a query, not a resolved location) in autocomplete, the input box
+ * under it gets clicked and focused... I haven't been able to figure
+ * out why. */
+ self.trap(control.focus)();
+
+ self.map.showSearchResults(results);
+ });
+
+ if (!destination.hasNext()) {
+ asyncs.push(timeline.disableAdd());
+ var oldLastIndex = destination.getIndex() - 1;
+ if (oldLastIndex >= 0) {
+ asyncs.push(timeline.get(oldLastIndex)
+ .then(function(oldLast) {
+ if (oldLast) {
+ self.unbindLastDestinationSearchEvents(oldLast);
+ }
+ }));
+ }
+ self.bindLastDestinationSearchEvents(control);
+ }
+
+ return Promise.all([asyncs]);
+ });
+ },
+
+ handleDestinationRemove: function(destination) {
+ var self = this;
+ var index = destination.getIndex();
+ this.timeline.remove(index).then(function(control) {
+ self.unbindLastDestinationSearchEvents(control);
+
+ if (index >= self.destinations.count()) {
+ return self.timeline.get(-1).then(function(lastControl) {
+ if (lastControl) {
+ self.bindLastDestinationSearchEvents(lastControl);
+ self.handleLastPlaceChange(lastControl.getPlace());
+ }
+ });
+ }
+ //TODO(rosswang): reselect?
+ }).catch(this.error);
},
handleTimelineDestinationAdd: function() {
- this.destinations.add();
- this.timeline.get(-1).focus();
+ var self = this;
+ var timeline = this.timeline;
+ function selectNewControl(control) {
+ control.focus().catch(self.error);
+ timeline.onDestinationAdd.remove(selectNewControl);
+ }
+ timeline.onDestinationAdd.add(selectNewControl);
+
+ this.destinations.add().select();
},
handleMiniDestinationAdd: function() {
@@ -305,16 +354,14 @@
handleLastPlaceChange: function(place) {
if (place) {
- this.timeline.enableAdd();
+ this.timeline.enableAdd().catch(this.error);
} else {
- this.timeline.disableAdd();
+ this.timeline.disableAdd().catch(this.error);
}
},
handleLastPlaceDeselected: function() {
- /* Wait until next frame to allow selection/focus to update; we don't want
- * to remove a box that has just received focus. */
- raf(this.trimUnusedDestinations);
+ this.trimUnusedDestinations().catch(this.error);
},
runCommand: function(command, rest) {
@@ -360,6 +407,76 @@
this.messages.push(message);
},
+ handleSendCast: function(targetOwner, targetDeviceName, spec) {
+ switch (spec.panelName) {
+ case 'timeline':
+ this.sendTimelineCast(targetOwner, targetDeviceName);
+ break;
+ default:
+ this.error(strings.notCastable(spec.panelName));
+ }
+ },
+
+ handleReceiveCast: function(spec) {
+ switch (spec.panelName) {
+ case 'timeline':
+ this.receiveTimelineCast();
+ break;
+ default:
+ this.error(strings.notCastable(spec.panelName));
+ }
+ },
+
+ sendTimelineCast: function(targetOwner, targetDeviceName) {
+ var self = this;
+ this.vanadiumStartup.then(function(args) {
+ var endpoint = naming.rpcMount(
+ targetOwner, targetDeviceName, 'timeline');
+ return args.vanadiumWrapper.client(endpoint).then(function(ts) {
+ var tc = new TimelineClient(args.vanadiumWrapper.context(),
+ ts, self.dependencies);
+ tc.onError.add(self.error);
+ return self.adoptTimeline(tc);
+ });
+ }).catch(this.error);
+ },
+
+ receiveTimelineCast: function() {
+ var self = this;
+ var timeline = new Timeline(this.map.maps);
+ var ts = new TimelineService(timeline, this.dependencies);
+
+ this.vanadiumStartup.then(function(args) {
+ return args.vanadiumWrapper.server(
+ args.mountNames.rpcMount('timeline'), ts);
+ }).then(function() {
+ //TODO(rosswang): delay swap until after initialized
+ self.$appRoot.replaceWith(timeline.$);
+ }).catch(this.error);
+ },
+
+ adoptTimeline: function(timeline) {
+ var self = this;
+ timeline.onAddClick.add(this.handleTimelineDestinationAdd);
+ this.map.onBoundsChange.add(this.trap(timeline.setSearchBounds));
+ var async = Promise.resolve();
+ this.destinations.each(function(i, destination) {
+ async = async.then(function() {
+ return self.addDestinationToTimeline(timeline, destination);
+ });
+ });
+ this.timeline = timeline;
+ if (timeline.$) {
+ this.$timelineContainer.empty().append(timeline.$).show();
+ this.$toggleTimeline.show();
+ } else {
+ this.$timelineContainer.hide();
+ this.$toggleTimeline.hide();
+ }
+ this.map.invalidateSize();
+ return async;
+ },
+
handleUserMessage: function(message, raw) {
var match = CMD_REGEX.exec(raw);
if (match) {
@@ -370,11 +487,27 @@
},
trimUnusedDestinations: function() {
- for (var lastControl = this.timeline.get(-1);
- !lastControl.getPlace() && !lastControl.isSelected() &&
- this.destinations.count() > 1;
- lastControl = this.timeline.get(-1)) {
- this.destinations.remove(-1);
+ var self = this;
+
+ var lastIndex = this.destinations.count() - 1;
+ if (lastIndex > 0) {
+ return this.timeline.get(lastIndex).then(function(lastControl) {
+ return Promise.all([
+ lastControl.getPlace(),
+ lastControl.isSelected()
+ ]);
+ }).then(function(conditions) {
+ if (!(conditions[0] || conditions[1])) {
+ //check for race condition; if we're no longer up-to-date
+ //just execute the next "iteration" without actually removing
+ if (lastIndex === self.destinations.count() - 1) {
+ self.destinations.remove(-1);
+ }
+ return self.trimUnusedDestinations();
+ }
+ });
+ } else {
+ return Promise.resolve();
}
},
@@ -418,7 +551,8 @@
var timeline = this.timeline = new Timeline(maps);
var error = this.error;
- var vanadiumStartup = vanadiumWrapper.init(opts.vanadium)
+ var vanadiumStartup = this.vanadiumStartup =
+ vanadiumWrapper.init(opts.vanadium)
.then(function(wrapper) {
wrapper.onError.add(error);
wrapper.onCrash.add(error);
@@ -440,10 +574,13 @@
sbName = '/localhost:' + sbName;
}
- var sync = this.sync = new TravelSync(vanadiumStartup, {
+ this.dependencies = {
maps: maps,
placesService: map.createPlacesService()
- }, sbName);
+ };
+
+ var sync = this.sync = new TravelSync(
+ vanadiumStartup, this.dependencies, sbName);
sync.bindDestinations(destinations);
this.info(strings['Connecting...'], sync.startup
@@ -477,17 +614,15 @@
messages.onMessage.add(this.handleUserMessage);
- timeline.onAddClick.add(this.handleTimelineDestinationAdd);
-
var miniAddButton = this.miniAddButton = new AddButton();
var miniDestinationSearch = this.miniDestinationSearch =
new DestinationSearch(maps);
miniAddButton.onClick.add(this.handleMiniDestinationAdd);
- miniDestinationSearch.setPlaceholder(strings['Search']);
- miniDestinationSearch.setSearchBounds(map.getBounds());
- map.onBoundsChange.add(miniDestinationSearch.setSearchBounds);
+ miniDestinationSearch.setPlaceholder(strings['Search']).catch(error);
+ miniDestinationSearch.setSearchBounds(map.getBounds()).catch(error);
+ map.onBoundsChange.add(this.trap(miniDestinationSearch.setSearchBounds));
miniDestinationSearch.onSearch.add(function(results) {
if (results.length > 0) {
@@ -504,6 +639,8 @@
miniDestinationSearch.onPlaceChange.add(function(place) {
if (!place) {
self.map.enableLocationSelection();
+ } else {
+ self.map.disableLocationSelection();
}
});
@@ -528,8 +665,7 @@
* unnecessarily distort the text (which is an effect that is nice for the
* add button, so that gets it explicitly). */
var $timelineContainer = this.$timelineContainer = $('<div>')
- .addClass('timeline-container collapsed')
- .append(timeline.$);
+ .addClass('timeline-container collapsed');
var $toggleTimeline = this.$toggleTimeline = $('<div>')
.addClass('toggle-timeline no-select collapsed')
@@ -541,9 +677,10 @@
map.addControls(maps.ControlPosition.LEFT_TOP, $miniPanel);
map.addControls(maps.ControlPosition.LEFT_CENTER, $toggleTimeline);
- var $domRoot = opts.domRoot? $(opts.domRoot) : $('body');
+ var $domRoot = this.$domRoot = opts.domRoot? $(opts.domRoot) : $('body');
+ var $appRoot = this.$appRoot = $('<div>');
- $domRoot.append($timelineContainer, map.$);
+ $domRoot.append($appRoot.append($timelineContainer, map.$));
this.initMiniFeedback();
@@ -553,18 +690,24 @@
panelName: 'timeline'
}
});
- castingManager.onAmbiguousCast.add(function(related, unknown) {
+ castingManager.onAmbiguousCast.add(function(related, unknown, other) {
console.debug('ambiguous cast');
console.debug(related);
console.debug(unknown);
+ console.debug(other);
});
castingManager.onNoNearbyDevices.add(function() {
self.error(strings.noNearbyDevices);
});
castingManager.onError.add(error);
+ castingManager.onSendCast.add(this.handleSendCast);
+ sync.onReceiveCast.add(this.handleReceiveCast);
+
+ this.adoptTimeline(timeline);
+
destinations.add();
- miniDestinationSearch.focus();
+ miniDestinationSearch.focus().catch(error);
$domRoot.keypress(function() {
messages.open();
diff --git a/src/travelsync.js b/src/travelsync.js
index dead53c..386dfec 100644
--- a/src/travelsync.js
+++ b/src/travelsync.js
@@ -8,6 +8,7 @@
var defineClass = require('./util/define-class');
+var debug = require('./debug');
var naming = require('./naming');
var SyncgroupManager = require('./syncgroup-manager');
@@ -163,7 +164,8 @@
},
handleCast: function(ctx, serverCall, spec) {
- console.debug('Cast target for ' + spec.panelName);
+ debug.log('Cast target for ' + spec.panelName);
+ this.onReceiveCast(spec);
},
clientPromise: function(endpoint) {
@@ -182,6 +184,11 @@
constants: [ 'invitationManager', 'startup', 'status' ],
events: {
/**
+ * @param spec
+ */
+ onReceiveCast: '',
+
+ /**
* @param newSize
*/
onTruncateDestinations: '',
diff --git a/src/vanadium-wrapper/syncbase-wrapper.js b/src/vanadium-wrapper/syncbase-wrapper.js
index 4f1adc0..f704e99 100644
--- a/src/vanadium-wrapper/syncbase-wrapper.js
+++ b/src/vanadium-wrapper/syncbase-wrapper.js
@@ -132,7 +132,7 @@
function(db, cb) {
var t = db.table('t');
var putToSyncbase = promisify(t.put.bind(t));
- var deleteFromSyncbase = promisify(t.delete.bind(t));
+ var deleteFromSyncbase = promisify(t.deleteRange.bind(t));
var ops = {
put: function(k, v) {
@@ -217,7 +217,6 @@
}
if (this.writes.size) {
- debug.log('Syncbase: deferring refresh due to writes in progress');
return Promise.all(this.writes)
.then(repull, repull);
@@ -256,7 +255,6 @@
//no-op
} else if (self.dirty) {
abort = true;
- debug.log('Syncbase: aborting refresh due to writes');
resolve(repull()); //try/wait for idle again
/* It would be nice to abort this stream for real, but we can't.
* Leave this handler attached but no-oping to drain the stream.
@@ -402,7 +400,6 @@
standardPut: function(fn, k, v) {
k = joinKey(k);
- debug.log('Syncbase: put ' + k + ' = ' + v);
return fn(this.context, k, v);
},
@@ -433,7 +430,7 @@
this.runInBatch = promisify(syncbase.nosql.runInBatch);
this.putToSyncbase = promisify(this.t.put.bind(this.t));
- this.deleteFromSyncbase = promisify(this.t.delete.bind(this.t));
+ this.deleteFromSyncbase = promisify(this.t.deleteRange.bind(this.t));
// Start the watch loop to periodically poll for changes from sync.
// TODO(rosswang): Remove this once we have client watch.
diff --git a/test/travel.js b/test/travel.js
index 71da800..79e247e 100644
--- a/test/travel.js
+++ b/test/travel.js
@@ -19,7 +19,7 @@
var PLACES = mockMaps.places.corpus;
//All SLAs are expressed in milliseconds.
-var UI_SLA = 50;
+var UI_SLA = 100;
/**
* Syncbase doesn't yet provide us any notification that the first sync after
* joining the initial sync groups has happened. This SLA is currently based on
diff --git a/tools/start_services.sh b/tools/start_services.sh
index 9175c45..1ecb678 100644
--- a/tools/start_services.sh
+++ b/tools/start_services.sh
@@ -9,25 +9,11 @@
#
# Optionally, the creds variable can specify a subdirectory.
+PATH=${PATH}:${V23_ROOT}/release/go/bin
+
set -euo pipefail
-trap kill_child_processes INT TERM EXIT
-silence() {
- "$@" &> /dev/null || true
-}
-# Copied from chat example app.
-kill_child_processes() {
- # Attempt to stop child processes using the TERM signal.
- if [[ -n "$(jobs -p -r)" ]]; then
- silence pkill -P $$
- sleep 1
- # Kill any remaining child processes using the KILL signal.
- if [[ -n "$(jobs -p -r)" ]]; then
- silence sudo -u "${SUDO_USER}" pkill -9 -P $$
- fi
- fi
-}
+
main() {
- PATH=${PATH}:${V23_ROOT}/release/go/bin
local -r TMP=tmp
local -r CREDS=./tmp/creds/${creds-}
local -r PORT=${port-4000}
@@ -51,7 +37,7 @@
fi
mkdir -p $TMP
- ./bin/syncbased \
+ syncbased \
--v=5 \
--alsologtostderr=false \
--root-dir=${TMP}/syncbase_${PORT} \
@@ -61,6 +47,5 @@
--v23.tcp.address=${SYNCBASED_ADDR} \
--v23.credentials=${CREDS} \
--v23.permissions.literal="{\"Admin\":{\"In\":[\"${BLESSINGS}\"]},\"Write\":{\"In\":[\"${BLESSINGS}\"]},\"Read\":{\"In\":[\"${BLESSINGS}\"]},\"Resolve\":{\"In\":[\"${BLESSINGS}\"]},\"Debug\":{\"In\":[\"...\"]}}"
- tail -f /dev/null # wait forever
}
main "$@"