Adding place details panel
The panel is called "suggestions" as it will primarily be used to
launch suggestions.
Change-Id: If801d49e72bd0470c8d9e2a401b0a1954afd922b
diff --git a/package.json b/package.json
index ced45b4..4ab107a 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"htmlencode": "^0.0.4",
"jquery": "^2.1.4",
"lodash": "^3.10.1",
+ "mercury": "^14.0.0",
"multimap": "^0.1.1",
"query-string": "^2.4.0",
"raf": "^3.1.0",
diff --git a/src/components/suggestion.js b/src/components/suggestion.js
new file mode 100644
index 0000000..f28f3b2
--- /dev/null
+++ b/src/components/suggestion.js
@@ -0,0 +1,56 @@
+// 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 hg = require('mercury');
+var h = hg.h;
+
+var $ = require('../util/jquery');
+var strings = require('../strings').currentLocale;
+
+module.exports = Suggestion;
+
+function Suggestion(suggestion) {
+ return hg.state({
+ placeId: hg.value(suggestion.placeId),
+ placeName: hg.value(suggestion.placeName),
+ photoUrl: hg.value(suggestion.photoUrl),
+ iconUrl: hg.value(suggestion.iconUrl),
+ rating: hg.value(suggestion.rating),
+ priceLevel: hg.value(suggestion.priceLevel),
+
+ hovered: hg.value(suggestion.hovered),
+ selected: hg.value(suggestion.selected),
+
+ channels: {
+ toggleSelect: toggleSelect
+ }
+ });
+}
+
+function toggleSelect(state) {
+ state.selected.set(!state.selected());
+}
+
+Suggestion.render = function(state) {
+ var elems = [
+ h('.img-container', state.photoUrl? h('img.photo', { src: state.photoUrl })
+ : state.iconUrl ? h('img.icon', { src: state.iconUrl })
+ : ['?']),
+ h('.name', state.placeName)
+ ];
+ if (state.rating !== undefined && state.rating !== null) {
+ elems.push(h('.rating', [
+ state.rating .toString(),
+ 'TODO: stars'
+ ]));
+ }
+ if ($.isNumeric(state.priceLevel)) {
+ elems.push(h('.price-level', state.priceLevel?
+ strings.priceLevelUnit.repeat(state.priceLevel) : strings['Free']));
+ }
+ elems.push(h('.clear-float'));
+ return h('.suggestion', elems);
+};
\ No newline at end of file
diff --git a/src/components/suggestions.js b/src/components/suggestions.js
new file mode 100644
index 0000000..8ad1f45
--- /dev/null
+++ b/src/components/suggestions.js
@@ -0,0 +1,24 @@
+// 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('lodash');
+var hg = require('mercury');
+var h = hg.h;
+
+var Suggestion = require('./suggestion');
+
+module.exports = Suggestions;
+
+function Suggestions(suggestions) {
+ return hg.state({
+ suggestions: hg.varhash(suggestions || {}, Suggestion)
+ });
+}
+
+Suggestions.render = function(state) {
+ return h('.suggestions', _.toArray(state.suggestions).map(
+ function(suggestion) {
+ return hg.partial(Suggestion.render, suggestion);
+ }));
+};
\ No newline at end of file
diff --git a/src/ifc/conversions.js b/src/ifc/conversions.js
index f83c60c..6a2b8ba 100644
--- a/src/ifc/conversions.js
+++ b/src/ifc/conversions.js
@@ -8,6 +8,13 @@
var Place = require('../place');
+/* TODO(rosswang): We can remote getUrl out as an RPC, at least for RPC-based
+ * casting. We'd need some fancy footwork with the Syncbase approach, with no
+ * guarantee of resolution. */
+var PLACE_PHOTO_OPTS = {
+ maxHeight: 96
+};
+
var x = {
box: function(i) {
return i === undefined || i === null? i : new vdlTravel.Int16({ value: i });
@@ -25,7 +32,13 @@
viewport: x.toLatLngBounds(maps, ifc.viewport)
},
'formatted_address': ifc.formattedAddress,
- name: ifc.name
+ name: ifc.name,
+ photos: ifc.photoUrl? [{
+ getUrl: function() { return ifc.photoUrl; }
+ }] : [],
+ icon: ifc.iconUrl,
+ rating: ifc.rating,
+ priceLevel: ifc.priceLevel
});
},
@@ -42,7 +55,12 @@
viewport: place.getGeometry().viewport,
formattedAddress: details && details['formatted_address'] ||
placeObj.query,
- name: details && details.name
+ name: details && details.name,
+ photoUrl: details.photos[0]?
+ details.photos[0].getUrl(PLACE_PHOTO_OPTS) : '',
+ iconUrl: details.icon || '',
+ rating: details.rating,
+ priceLevel: details.priceLevel
});
},
diff --git a/src/ifc/types.vdl b/src/ifc/types.vdl
index 506d826..dc5f4ab 100644
--- a/src/ifc/types.vdl
+++ b/src/ifc/types.vdl
@@ -40,6 +40,10 @@
FormattedAddress string
Name string
+ PhotoUrl string
+ IconUrl string
+ Rating float32
+ PriceLevel byte
}
type Event struct {
diff --git a/src/static/index.css b/src/static/index.css
index b6d591e..66fa046 100644
--- a/src/static/index.css
+++ b/src/static/index.css
@@ -327,6 +327,56 @@
content: '▲';
}
+.suggestions-container {
+ float: right;
+ overflow-y: auto;
+ height: 100%;
+ width: 25em;
+}
+
+.suggestion {
+ border: 1px solid #ccc;
+ font-family: Roboto, Arial, sans-serif;
+ padding: .5em;
+ margin: 0 0 -1px 0;
+}
+
+.suggestion .img-container {
+ float: left;
+ margin-right: 8px;
+ height: 92px;
+ width: 80px;
+ text-align: center;
+ font-size: 80px;
+ color: gray;
+}
+
+.suggestion img {
+ height: 100%;
+ width: 100%;
+}
+
+.suggestion img.photo {
+ object-fit: cover;
+}
+
+.suggestion img.icon {
+ object-fit: scale-down;
+ background-color: #eee;
+}
+
+.suggestion .rating {
+ color: #e7711b;
+ font-size: 13px;
+ line-height: 16px;
+}
+
+.suggestion .price-level {
+ color: #8c8c8c;
+ font-size: 13px;
+ line-height: 16px;
+}
+
.vertical-middle {
position: relative;
top: 50%;
diff --git a/src/strings.js b/src/strings.js
index 8327e1f..2580ece 100644
--- a/src/strings.js
+++ b/src/strings.js
@@ -50,6 +50,7 @@
UNKNOWN_ERROR: 'Server error'
},
'Final destination': 'Final destination',
+ 'Free': 'Free',
label: function(label, details) {
return label + ': ' + details;
},
@@ -81,6 +82,7 @@
return username + ' is not reachable or is not a Travel Planner user.';
},
'Origin': 'Origin',
+ priceLevelUnit: '$',
'Search': 'Search',
sendingInvite: function(username) {
return 'Inviting ' + username + ' to join the trip...';
diff --git a/src/travel.js b/src/travel.js
index 9ddbbdb..8c92b59 100644
--- a/src/travel.js
+++ b/src/travel.js
@@ -4,6 +4,7 @@
require('es6-shim');
+var hg = require('mercury');
var queryString = require('query-string');
var raf = require('raf');
@@ -15,6 +16,7 @@
var MapWidget = require('./components/map-widget');
var Messages = require('./components/messages');
var Message = require('./components/message');
+var Suggestions = require('./components/suggestions');
var Timeline = require('./components/timeline');
var TimelineClient = require('./components/timeline-client');
var TimelineService = require('./components/timeline-server');
@@ -45,6 +47,9 @@
}
var CMD_REGEX = /\/(\S*)(?:\s+(.*))?/;
+var SUGGESTION_PHOTO_OPTS = {
+ maxHeight: 96
+};
var Travel = defineClass({
publics: {
@@ -207,7 +212,7 @@
* out why. */
self.trap(control.focus)();
- self.map.showSearchResults(results);
+ self.handleSearchResults(results);
});
if (!destination.hasNext()) {
@@ -520,6 +525,60 @@
}
},
+ handleSearchResults: function(results) {
+ this.map.showSearchResults(results);
+ if (results.length > 1) {
+ this.hgState.suggestions.set({
+ suggestions: results.map(function(place) {
+ var details = place.getDetails();
+ var photoUrl;
+
+ if (details.photos && details.photos[0]) {
+ photoUrl = details.photos[0].getUrl(SUGGESTION_PHOTO_OPTS);
+ }
+
+ return {
+ placeId: place.toObject().placeId,
+ placeName: place.getName(),
+ photoUrl: photoUrl,
+ iconUrl: details.icon,
+ rating: details.rating,
+ priceLevel: details['price_level']
+ };
+ })
+ });
+ this.showSuggestions();
+ } else {
+ this.dismissSuggestions();
+ }
+ },
+
+ showSuggestions: function() {
+ if (!this.$hgSuggestionsRoot) {
+ this.$hgSuggestionsRoot = $('<div>')
+ .addClass('suggestions-container')
+ .insertBefore(this.map.$);
+ this.dismissHgSuggestions = hg.app(
+ this.$hgSuggestionsRoot[0], this.hgState, this.hgRenderSuggestions);
+ this.map.invalidateSize();
+ }
+ this.$hgSuggestionsRoot.prop('scrollTop', 0);
+ },
+
+ hgRenderSuggestions: function(state) {
+ return Suggestions.render(state.suggestions);
+ },
+
+ dismissSuggestions: function() {
+ if (this.dismissHgSuggestions) {
+ this.dismissHgSuggestions();
+ delete this.dismissHgSuggestions;
+ this.$hgSuggestionsRoot.remove();
+ delete this.$hgSuggestionsRoot;
+ this.map.invalidateSize();
+ }
+ },
+
/**
* The map widget isn't very sensitive to size updates, so we need to
* continuously invalidate during animations.
@@ -542,12 +601,18 @@
}
},
+ constants: [ 'hgState' ],
+
init: function (opts) {
var self = this;
opts = opts || {};
var vanadiumWrapper = opts.vanadiumWrapper || vanadiumWrapperDefault;
+ this.hgState = hg.state({
+ suggestions: new Suggestions()
+ });
+
var destinations = this.destinations = new Destinations();
destinations.onAdd.add(this.handleDestinationAdd);
destinations.onRemove.add(this.handleDestinationRemove);
@@ -642,7 +707,7 @@
* place and so must pick a new one. */
map.disableLocationSelection();
}
- map.showSearchResults(results);
+ self.handleSearchResults(results);
});
miniDestinationSearch.onPlaceChange.add(function(place) {