route options UI
This commit is contained in:
parent
08e695a057
commit
d2e9a57343
7 changed files with 145 additions and 28 deletions
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue