route options UI

This commit is contained in:
Laura Klünder 2017-12-16 19:33:13 +01:00
parent 08e695a057
commit d2e9a57343
7 changed files with 145 additions and 28 deletions

View file

@ -1,3 +1,4 @@
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from rest_framework.decorators import list_route
from rest_framework.response import Response
@ -12,6 +13,10 @@ from c3nav.routing.router import Router
class RoutingViewSet(ViewSet):
"""
/route/ Get routes.
/options/ Get or set route options.
"""
@list_route(methods=['get', 'post'])
def route(self, request, *args, **kwargs):
params = request.POST if request.method == 'POST' else request.GET
@ -20,7 +25,15 @@ class RoutingViewSet(ViewSet):
if not form.is_valid():
return Response({
'errors': form.errors,
})
}, status=400)
options = RouteOptions.get_for_request(request)
try:
options.update(params, ignore_unknown=True)
except ValidationError as e:
return Response({
'errors': (str(e), ),
}, status=400)
try:
route = Router.load().get_route(origin=form.cleaned_data['origin'],
@ -44,6 +57,7 @@ class RoutingViewSet(ViewSet):
'origin': form.cleaned_data['origin'].pk,
'destination': form.cleaned_data['destination'].pk,
},
'options': options.serialize(),
'result': route.serialize(locations=visible_locations_for_request(request)),
})
@ -55,13 +69,4 @@ class RoutingViewSet(ViewSet):
pass
options = RouteOptions.get_for_request(request)
return Response({
'options': options.data,
'fields': {
name: {
'type': field.widget.input_type,
'label': field.label,
'choices': dict(field.choices),
}
for name, field in options.get_fields().items()},
})
return Response(options.serialize())

View file

@ -116,14 +116,14 @@ class RouteOptions(models.Model):
except AttributeError:
return self.get_fields()[key].initial
def update(self, value_dict, ignore_errors=False):
def update(self, value_dict, ignore_errors=False, ignore_unknown=False):
if not value_dict:
return
fields = self.get_fields()
for key, value in value_dict.items():
field = fields.get(key)
if not field:
if ignore_errors:
if ignore_errors or ignore_unknown:
continue
raise ValidationError(_('Unknown route option: %s') % key)
if value is None or value not in dict(field.choices):
@ -135,6 +135,24 @@ class RouteOptions(models.Model):
def __setitem__(self, key, value):
self.update({key: value})
def serialize(self):
return [
{
'name': name,
'type': field.widget.input_type,
'label': field.label,
'choices': [
{
'name': choice_name,
'title': choice_title,
}
for choice_name, choice_title in field.choices
],
'value': self[name],
}
for name, field in self.get_fields().items()
]
def save(self, *args, **kwargs):
if self.request is None or self.request.user.is_authenticated:
self.user = self.request.user

View file

@ -219,7 +219,7 @@ section.details > * {
section.details > .details-head {
padding: 11px 10px 8px;
}
.details-head > .button {
section.details > .details-head > .button {
margin: -2px 0 0;
transition: none;
line-height: 2.5;
@ -304,7 +304,9 @@ section.details {
main:not([data-view$=search]) #autocomplete,
main:not([data-view=location]) #location-details,
main:not([data-view=route-result]) #route-details,
main:not(.show-details) #resultswrapper .details {
main:not([data-view=route-result]) #route-options,
main:not(.show-details) #resultswrapper .details:not(#route-options),
main:not(.show-options) #resultswrapper #route-options {
display:none;
}
main .buttons .details .material-icons {
@ -313,7 +315,8 @@ main .buttons .details .material-icons {
main.show-details .buttons .details .material-icons {
transform: scale(1, -1);
}
main.show-details #resultswrapper .details {
main.show-details #resultswrapper .details,
main.show-options #resultswrapper #route-options {
animation: show-details;
animation-duration: 150ms;
animation-timing-function: ease-out;
@ -328,6 +331,27 @@ main.show-details #resultswrapper .details {
top: 0;
}
}
.route-options-fields {
padding: 0 10px 5px;
}
.route-options-fields input, .route-options-fields select {
margin-bottom: 1rem;
}
.route-options-buttons {
display: flex;
flex-wrap: wrap;
padding: 0 0 15px 10px;
}
.route-options-buttons button {
padding: 0 1rem;
margin: 0 10px 0 0;
flex-grow: 1;
}
#route-options .details-head button {
font-size: 30px;
line-height: 1.0;
color: #b2b2b2;
}
.location {
position: relative;
@ -519,7 +543,7 @@ main:not([data-view=route-result]) #route-dots {
.buttons > *:hover, .buttons > *:active {
background-color: #eeeeee;
}
main.map button, main.map .button {
#search button, .leaflet-popup button, .details-head .button {
font-size: 1.3rem;
line-height: 1.3;
height: 3.3rem;
@ -563,6 +587,19 @@ main:not([data-view=route-result]) #route-summary {
opacity: 0;
}
#route-summary button.options {
position: absolute;
top: 8px;
right: 6px;
padding: 0;
width: 37px;
height: 37px;
border-width: 0;
font-size: 36px;
color: #b2b2b2;
line-height: 1;
}
@media not all and (min-height: 700px) and (min-width: 1100px) {
main[data-view=route-result] #sidebar #search:not(.focused) .locationinput {
margin-bottom: -21px;

View file

@ -83,6 +83,8 @@ c3nav = {
$result_buttons.find('.details').on('click', c3nav._buttons_details_click);
$('#route-search-buttons, #route-result-buttons').find('.swap').on('click', c3nav._route_buttons_swap_click);
$('#route-search-buttons').find('.close').on('click', c3nav._route_buttons_close_click);
$('#route-summary').find('.options').on('click', c3nav._buttons_options_click);
$('#route-options').find('.close').on('click', c3nav._route_options_close_click);
$('#map').on('click', '.location-popup .button-clear', c3nav._popup_button_click);
$('#modal').on('click', c3nav._modal_click)
@ -99,9 +101,15 @@ c3nav = {
},
state: {},
update_state: function(routing, replace, details) {
update_state: function(routing, replace, details, options) {
if (typeof routing !== "boolean") routing = c3nav.state.routing;
if (details) {
options = false;
} else if (options) {
details = false;
}
var destination = $('#destination-input').data('location'),
origin = $('#origin-input').data('location'),
new_state = {
@ -109,7 +117,8 @@ c3nav = {
origin: origin,
destination: destination,
sidebar: true,
details: !!details
details: !!details,
options: !!options
};
c3nav._push_state(new_state, replace);
@ -147,6 +156,7 @@ c3nav = {
if (view === 'route-result') {
if (state.route_result) {
c3nav._display_route_result(state.route_result, nofly);
c3nav._display_route_options(state.route_options);
} else {
c3nav.load_route(state.origin, state.destination, nofly);
}
@ -155,7 +165,9 @@ c3nav = {
c3nav._clear_route_layers();
}
$('main').attr('data-view', view).toggleClass('show-details', state.details);
$('main').attr('data-view', view)
.toggleClass('show-details', !!state.details)
.toggleClass('show-options', !!state.options);
var $search = $('#search');
$search.removeClass('loading');
@ -233,11 +245,13 @@ c3nav = {
},
load_route: function (origin, destination, nofly) {
var $route = $('#route-summary'),
$details_wrapper = $('#route-details');
$details_wrapper = $('#route-details'),
$options_wrapper = $('#route-options');
if ($route.attr('data-origin') !== String(origin.id) || $route.attr('data-destination') !== String(destination.id)) {
c3nav._clear_route_layers();
$route.addClass('loading').attr('data-origin', origin.id).attr('data-destination', destination.id);
$details_wrapper.addClass('loading');
$options_wrapper.addClass('loading');
$.post('/api/routing/route/', {
'origin': origin.id,
'destination': destination.id,
@ -262,8 +276,9 @@ c3nav = {
// loaded too late, information no longer needed
return;
}
c3nav._push_state({route_result: data.result}, true);
c3nav._push_state({route_result: data.result, route_options: data.options}, true);
c3nav._display_route_result(data.result, nofly);
c3nav._display_route_options(data.options);
},
_display_route_result: function(result, nofly) {
var $route = $('#route-summary'),
@ -415,9 +430,31 @@ c3nav = {
new_coords.push(coords[coords.length-1]);
return new_coords
},
_display_route_options: function(options) {
var $options_wrapper = $('#route-options'),
$options = $options_wrapper.find('.route-options-fields'),
option, field, field_id, choice;
$options.html('');
for (var i=0; i<options.length; i++) {
option = options[i];
field_id = 'option_id_'+option.name;
$options.append($('<label for="'+field_id+'">').text(option.label));
if (option.type === 'select') {
field = $('<select name="'+name+'" id="'+field_id+'">');
for (j=0; j<option.choices.length; j++) {
choice = option.choices[j];
field.append($('<option name="'+choice.name+'">').text(choice.title));
}
}
field.val(option.value);
$options.append(field);
}
$options_wrapper.removeClass('loading');
},
_equal_states: function (a, b) {
if (a.modal !== b.modal) return false;
if (a.routing !== b.routing || a.details !== b.details) return false;
if (a.routing !== b.routing || a.details !== b.details || a.options !== b.options) return false;
if ((a.origin && a.origin.id) !== (b.origin && b.origin.id)) return false;
if ((a.destination && a.destination.id) !== (b.destination && b.destination.id)) return false;
if (a.level !== b.level || a.zoom !== b.zoom) return false;
@ -438,6 +475,9 @@ c3nav = {
if (state.details && (url.startsWith('/l/') || url.startsWith('/r/'))) {
url += 'details/'
}
if (state.options && url.startsWith('/r/')) {
url += 'options/'
}
if (state.center) {
url += '@'+String(c3nav.level_labels_by_id[state.level])+','+String(state.center[0])+','+String(state.center[1])+','+String(state.zoom);
}
@ -495,6 +535,12 @@ c3nav = {
_buttons_details_click: function () {
c3nav.update_state(null, null, !c3nav.state.details);
},
_buttons_options_click: function () {
c3nav.update_state(null, null, null, !c3nav.state.options);
},
_route_options_close_click: function () {
c3nav.update_state(null, null, null, false);
},
_location_buttons_route_click: function () {
c3nav.update_state(true);
},
@ -976,7 +1022,10 @@ c3nav = {
// add padding information for the current ui layout to fitBoudns options
var $search = $('#search'),
$main = $('main'),
padBesideSidebar = ($main.width() > 1000 && ($main.height() < 250 || c3nav.state.details)),
padBesideSidebar = (
$main.width() > 1000 &&
($main.height() < 250 || c3nav.state.details || c3nav.state.options)
),
left = padBesideSidebar ? ($search.width() || 0)+10 : 0,
top = padBesideSidebar ? 10 : ($search.height() || 0)+10;
options[topleft || 'paddingTopLeft'] = L.point(left+13, top+41);

View file

@ -63,6 +63,7 @@
<i class="icon material-icons">directions</i>
<span>10min (100m)… sorry no routing yet</span>
<small><em>default options</em></small>
<button class="button-clear options material-icons">settings</button>
</div>
<div class="buttons" id="route-search-buttons">
<button class="button-clear swap">
@ -103,12 +104,17 @@
</div>
<div class="details-body"></div>
</section>
<section id="route-settings" class="details">
<section id="route-options" class="details">
<div class="details-head">
<button class="button close button-clear material-icons float-right">close</button>
<h2>{% trans 'Route options' %}</h2>
</div>
<div class="details-body">
<div class="route-options-fields"></div>
<div class="route-options-buttons">
<button>{% trans 'Save and reroute' %}</button>
<button class="button-outline">{% trans 'Just reroute' %}</button>
</div>
</div>
</section>
</div>

View file

@ -6,13 +6,14 @@ from c3nav.site.views import (access_redeem_view, account_view, change_password_
slug = r'(?P<slug>[a-z0-9-_.:]+)'
slug2 = r'(?P<slug2>[a-z0-9-_.:]+)'
details = r'(?P<details>details/)?'
options = r'(?P<options>options/)?'
pos = r'(@(?P<level>[a-z0-9-_:]+),(?P<x>-?\d+(\.\d+)?),(?P<y>-?\d+(\.\d+)?),(?P<zoom>-?\d+(\.\d+)?))?'
embed = r'(?P<embed>embed/)?'
urlpatterns = [
url(r'^%s(?P<mode>[l])/%s/%s%s$' % (embed, slug, details, pos), map_index, name='site.index'),
url(r'^%s(?P<mode>[od])/%s/%s$' % (embed, slug, pos), map_index, name='site.index'),
url(r'^%sr/%s/%s/%s%s$' % (embed, slug, slug2, details, pos), map_index, name='site.index'),
url(r'^%sr/%s/%s/(%s|%s)%s$' % (embed, slug, slug2, details, options, pos), map_index, name='site.index'),
url(r'^%s(?P<mode>r)/%s$' % (embed, pos), map_index, name='site.index'),
url(r'^%s%s$' % (embed, pos), map_index, name='site.index'),
url(r'^qr/(?P<path>.*)$', qr_code, name='site.qr'),

View file

@ -47,7 +47,7 @@ def check_location(location: Optional[str], request) -> Optional[SpecificLocatio
return location
def map_index(request, mode=None, slug=None, slug2=None, details=None,
def map_index(request, mode=None, slug=None, slug2=None, details=None, options=None,
level=None, x=None, y=None, zoom=None, embed=None):
origin = None
destination = None
@ -71,6 +71,7 @@ def map_index(request, mode=None, slug=None, slug2=None, details=None,
if destination else None),
'sidebar': routing or destination is not None,
'details': True if details else False,
'options': True if options else False,
}
levels = levels_by_short_label_for_request(request)