/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
/* vim: set et ts=4 sw=4: */
/*
* Copyright (c) 2011, 2012, 2013 Red Hat, Inc.
*
* GNOME Maps is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*
* GNOME Maps is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License along
* with GNOME Maps; if not, see .
*
* Author: Zeeshan Ali (Khattak)
* Mattias Bengtsson
*/
import Cairo from 'cairo';
import Gdk from 'gi://Gdk';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Gtk from 'gi://Gtk';
import {Application} from './application.js';
import {InstructionRow} from './instructionRow.js';
import {PlaceStore} from './placeStore.js';
import {QueryPoint} from './routeQuery.js';
import {RouteEntry} from './routeEntry.js';
import {RouteQuery} from './routeQuery.js';
import {StoredRoute} from './storedRoute.js';
import {TransitArrivalRow} from './transitArrivalRow.js';
import {TransitItineraryRow} from './transitItineraryRow.js';
import {TransitLegRow} from './transitLegRow.js';
import {TransitMoreRow} from './transitMoreRow.js';
import {TransitOptionsPanel} from './transitOptionsPanel.js';
import * as Utils from './utils.js';
export class Sidebar extends Gtk.Revealer {
constructor(mapView) {
super({ transition_type: Gtk.RevealerTransitionType.SLIDE_LEFT });
this._mapView = mapView;
this._query = Application.routeQuery;
this._initInstructionList();
/* I could not get the custom GTK+ template widget to init properly
* from the UI file, we also need to manually insert the transit
* itinerary header widget into the GtkStack to get the correct
* animation direction.
*/
this._transitOptionsPanel = new TransitOptionsPanel({ visible: true });
this._transitHeader.add_named(this._transitOptionsPanel, 'options');
this._transitHeader.add_named(this._transitItineraryHeader,
'itinerary-header');
this._initTransportationToggles(this._modePedestrianToggle,
this._modeBikeToggle,
this._modeCarToggle,
this._modeTransitToggle);
this._initQuerySignals();
this._query.addPoint(0);
this._query.addPoint(1);
this._switchRoutingMode(Application.routeQuery.transportation);
/* Enable/disable transit mode switch based on the presence of
* public transit providers.
*/
this._modeTransitToggle.sensitive =
Application.routingDelegator.transitRouter.enabled;
}
_initTransportationToggles(pedestrian, bike, car, transit) {
let transport = RouteQuery.Transportation;
let onToggle = function(mode, button) {
let previousMode = this._query.transportation;
if (button.active && previousMode !== mode) {
this._switchRoutingMode(mode);
this._query.transportation = mode;
}
};
pedestrian.connect('toggled', onToggle.bind(this, transport.PEDESTRIAN));
car.connect('toggled', onToggle.bind(this, transport.CAR));
bike.connect('toggled', onToggle.bind(this, transport.BIKE));
transit.connect('toggled', onToggle.bind(this, transport.TRANSIT))
let setToggles = function() {
switch(Application.routeQuery.transportation) {
case transport.PEDESTRIAN:
pedestrian.active = true;
break;
case transport.CAR:
car.active = true;
break;
case transport.BIKE:
bike.active = true;
break;
case transport.TRANSIT:
transit.active = true;
break;
}
this._switchRoutingMode(Application.routeQuery.transportation);
};
setToggles.bind(this)();
this._query.connect('notify::transportation', setToggles.bind(this));
}
_switchRoutingMode(mode) {
if (mode === RouteQuery.Transportation.TRANSIT) {
Application.routingDelegator.useTransit = true;
this._linkButtonStack.visible_child_name = 'transit';
this._transitOptionsPanel.reset();
this._transitRevealer.reveal_child = true;
} else {
Application.routingDelegator.useTransit = false;
this._linkButtonStack.visible_child_name = 'turnByTurn';
this._transitRevealer.reveal_child = false;
Application.routingDelegator.transitRouter.plan.deselectItinerary();
}
this._clearInstructions();
}
_initQuerySignals() {
this._numRouteEntries = 0;
this._query.connect('point-added', (obj, point, index) => {
this._createRouteEntry(index, point);
this._numRouteEntries++;
});
this._query.connect('point-removed', (obj, point, index) => {
let row = this._entryList.get_row_at_index(index);
this._entryList.remove(row);
this._numRouteEntries--;
});
}
_cancelStore() {
GLib.source_remove(this._storeRouteTimeoutId);
this._storeRouteTimeoutId = 0;
}
_createRouteEntry(index, point) {
let type;
if (index === 0)
type = RouteEntry.Type.FROM;
else if (index === this._numRouteEntries)
type = RouteEntry.Type.TO;
else
type = RouteEntry.Type.VIA;
let routeEntry = new RouteEntry({ type: type,
point: point,
mapView: this._mapView });
// add handler overriding tab focus behavior on route entries
// TODO: how to set up tab handling using GTK4?
//routeEntry.entry.connect('focus', this._onRouteEntryFocus.bind(this));
// add handler for
routeEntry.entry.connect('notify::place', () => {
this._onRouteEntrySelectedPlace(routeEntry.entry);
});
this._entryList.insert(routeEntry, index);
if (type === RouteEntry.Type.FROM) {
routeEntry.button.connect('clicked', () => {
let lastIndex = this._numRouteEntries;
this._query.addPoint(lastIndex - 1);
// focus on the newly added point's entry
this._entryList.get_row_at_index(lastIndex - 1).get_child().entry.grab_focus();
});
this.connect('notify::child-revealed', () => {
if (this.child_revealed)
routeEntry.entry.grab_focus();
});
} else if (type === RouteEntry.Type.VIA) {
routeEntry.button.connect('clicked', () => {
let row = routeEntry.get_parent();
this._query.removePoint(row.get_index());
});
} else if (type === RouteEntry.Type.TO) {
routeEntry.button.connect('clicked',
this._reverseRoutePoints.bind(this));
}
this._initRouteDragAndDrop(routeEntry);
}
_onRouteEntryFocus(entry, direction) {
let index = this._getIndexForRouteEntry(entry);
/* if tabbing forward from the last entry or backward from the first,
* let the default handler handle it
*/
if ((direction === Gtk.DirectionType.TAB_FORWARD &&
index === this._entryList.get_children().length - 1) ||
(direction === Gtk.DirectionType.TAB_BACKWARD && index === 0)) {
return false;
}
if (direction === Gtk.DirectionType.TAB_FORWARD) {
index++;
} else if (direction === Gtk.DirectionType.TAB_BACKWARD) {
index--;
} else {
// don't handle other directions
return false;
}
this._entryList.get_row_at_index(index).get_child().entry.grab_focus();
return true;
}
_onRouteEntrySelectedPlace(entry) {
let [index, numEntries] = this._getIndexForRouteEntryAndNumEntries(entry);
/* if a new place is selected and it's not the last entry, focus next
* entry
*/
if (entry.place && index < numEntries - 1) {
let nextPlaceEntry =
this._entryList.get_row_at_index(index + 1).get_child().entry;
if (!nextPlaceEntry.place)
nextPlaceEntry.grab_focus();
}
}
_getIndexForRouteEntryAndNumEntries(entry) {
let index = 0;
let foundIndex = -1;
for (let item of this._entryList) {
let routeEntry = item.get_child();
if (routeEntry.entry === entry)
foundIndex = index;
index++;
}
return [foundIndex, index];
}
// this is needed to be called on shutdown to avoid a GTK warning
unparentSearchPopovers() {
for (let item of this._entryList) {
item.get_child().entry.popover.unparent();
}
}
_initInstructionList() {
let route = Application.routingDelegator.graphHopper.route;
let transitPlan = Application.routingDelegator.transitRouter.plan;
route.connect('reset', () => {
this._clearInstructions();
let length = this._entryList.get_children().length;
for (let index = 1; index < (length - 1); index++) {
this._query.removePoint(index);
}
});
transitPlan.connect('reset', () => {
this._clearTransitOverview();
this._showTransitOverview();
this._instructionStack.visible_child = this._transitWindow;
/* don't remove query points as with the turn-based routing,
* since we might get "no route" because of the time selected
* and so on */
this._transitAttributionLabel.label = '';
});
transitPlan.connect('no-more-results', () => {
// set the "load more" row to indicate no more results
let loadMoreRow;
for (let row of this._transitOverviewListBox) {
if (row instanceof TransitMoreRow)
loadMoreRow = row;
}
loadMoreRow.showNoMore();
});
this._query.connect('run', () => {
this._instructionStack.visible_child = this._instructionSpinner;
});
this._query.connect('notify', () => {
if (this._instructionStack.visible_child !== this._instructionSpinner &&
this._instructionStack.visible_child !== this._errorLabel) {
if (this._query.transportation === RouteQuery.Transportation.TRANSIT) {
this._clearTransitOverview();
this._showTransitOverview();
this._transitAttributionLabel.label = '';
} else {
this._clearInstructions();
}
}
if (this._storeRouteTimeoutId)
this._cancelStore();
});
route.connect('update', () => {
this._clearInstructions();
if (this._storeRouteTimeoutId)
this._cancelStore();
this._storeRouteTimeoutId = GLib.timeout_add(null, 5000, () => {
let placeStore = Application.placeStore;
let places = this._query.filledPoints.map(function(point) {
return point.place;
});
let storedRoute = new StoredRoute({
transportation: this._query.transportation,
route: route,
places: places,
geoclue: Application.geoclue
});
if (!storedRoute.containsNull) {
placeStore.addPlace(storedRoute,
PlaceStore.PlaceType.RECENT_ROUTE);
}
this._storeRouteTimeoutId = 0;
});
route.turnPoints.forEach((turnPoint) => {
let row = new InstructionRow({ visible: true,
turnPoint: turnPoint });
this._instructionList.insert(row, -1);
});
/* Translators: %s is a time expression with the format "%f h" or "%f min" */
this._timeInfo.label = _("Estimated time: %s").format(Utils.prettyTime(route.time));
this._distanceInfo.label = Utils.prettyDistance(route.distance);
});
this._instructionList.connect('row-selected', (listbox, row) => {
if (row)
this._mapView.showTurnPoint(row.turnPoint);
});
transitPlan.connect('update', () => {
this._updateTransitAttribution();
this._clearTransitOverview();
this._showTransitOverview();
this._populateTransitItineraryOverview();
});
/* use list separators for the transit itinerary overview list */
this._transitOverviewListBox.set_header_func((row, prev) => {
if (prev)
row.set_header(new Gtk.Separator());
});
this._transitOverviewListBox.connect('row-activated',
this._onItineraryOverviewRowActivated.bind(this));
this._transitItineraryBackButton.connect('clicked',
this._showTransitOverview.bind(this));
// connect error handlers
route.connect('error', (route, msg) => this._showError(msg));
transitPlan.connect('error', (plan, msg) => this._showError(msg));
}
_showError(msg) {
this._instructionStack.visible_child = this._errorLabel;
this._errorLabel.label = msg;
}
_clearListBox(listBox) {
let rows = [];
for (let row of listBox) {
if (row instanceof Gtk.ListBoxRow)
rows.push(row);
}
for (let row of rows) {
listBox.remove(row);
}
}
_clearTransitOverview() {
let listBox = this._transitOverviewListBox;
this._clearListBox(listBox);
this._instructionStack.visible_child = this._transitWindow;
this._timeInfo.label = '';
this._distanceInfo.label = '';
}
_clearTransitItinerary() {
let listBox = this._transitItineraryListBox;
this._clearListBox(listBox);
}
_updateTransitAttribution() {
let plan = Application.routingDelegator.transitRouter.plan;
if (plan.attribution) {
let attributionLabel =
_("Itineraries provided by %s").format(plan.attribution);
if (plan.attributionUrl) {
this._transitAttributionLabel.label =
'%s'.format([plan.attributionUrl],
attributionLabel);
} else {
this._transitAttributionLabel.label = attributionLabel;
}
} else {
this._transitAttributionLabel.label = '';
}
}
_showTransitOverview() {
let plan = Application.routingDelegator.transitRouter.plan;
this._transitListStack.visible_child_name = 'overview';
this._transitHeader.visible_child_name = 'options';
plan.deselectItinerary();
}
_showTransitItineraryView() {
this._transitListStack.visible_child_name = 'itinerary';
this._transitHeader.visible_child_name = 'itinerary-header';
}
_populateTransitItineraryOverview() {
let plan = Application.routingDelegator.transitRouter.plan;
plan.itineraries.forEach((itinerary) => {
let row = new TransitItineraryRow({ visible: true,
itinerary: itinerary });
this._transitOverviewListBox.insert(row, -1);
});
/* add the "load more" row */
this._transitOverviewListBox.insert(new TransitMoreRow({ visible: true }),
-1);
/* add an empty list row to get a final separator */
this._transitOverviewListBox.insert(new Gtk.ListBoxRow({ visible: true }),
-1);
}
_onItineraryActivated(itinerary) {
let plan = Application.routingDelegator.transitRouter.plan;
this._populateTransitItinerary(itinerary);
this._showTransitItineraryView();
plan.selectItinerary(itinerary);
}
_onMoreActivated(row) {
row.startLoading();
Application.routingDelegator.transitRouter.fetchMoreResults();
}
_onItineraryOverviewRowActivated(listBox, row) {
this._transitOverviewListBox.unselect_all();
if (row.itinerary)
this._onItineraryActivated(row.itinerary);
else
this._onMoreActivated(row);
}
_populateTransitItinerary(itinerary) {
this._transitItineraryTimeLabel.label =
itinerary.prettyPrintTimeInterval();
this._transitItineraryDurationLabel.label =
itinerary.prettyPrintDuration();
this._clearTransitItinerary();
for (let i = 0; i < itinerary.legs.length; i++) {
let leg = itinerary.legs[i];
let row = new TransitLegRow({ leg: leg,
start: i === 0,
mapView: this._mapView });
this._transitItineraryListBox.insert(row, -1);
}
/* insert the additional arrival row, showing the arrival place and time */
this._transitItineraryListBox.insert(
new TransitArrivalRow({ itinerary: itinerary, mapView: this._mapView }),
-1);
}
_clearInstructions() {
let listBox = this._instructionList;
this._clearListBox(listBox);
this._instructionStack.visible_child = this._instructionWindow;
this._timeInfo.label = '';
this._distanceInfo.label = '';
}
// Iterate over points and establish the new order of places
_reorderRoutePoints(srcIndex, destIndex) {
let points = this._query.points;
let srcPlace = this._query.points[srcIndex].place;
// Determine if we are swapping from "above" or "below"
let step = (srcIndex < destIndex) ? -1 : 1;
// Hold off on notifying the changes to query.points until
// we have re-arranged the places.
this._query.freeze_notify();
for (let i = destIndex; i !== (srcIndex + step); i += step) {
// swap
[points[i].place, srcPlace] = [srcPlace, points[i].place];
}
this._query.thaw_notify();
}
/* The reason we don't just use the array .reverse() function is that we
* need to update the place parameters on the actual point objects in the
* array to fire the query notify signal that will initiate an update.
*/
_reverseRoutePoints() {
let points = this._query.points;
let length = points.length;
this._query.freeze_notify();
for (let i = 0; i < length / 2; i++) {
let p1 = points[i].place;
let p2 = points[length - i - 1].place;
points[i].place = p2;
points[length - i - 1].place = p1;
}
this._query.thaw_notify();
}
_onDragDrop(row, point) {
let srcIndex = this._query.points.indexOf(point);
let destIndex = row.get_index();
this._reorderRoutePoints(srcIndex, destIndex);
return true;
}
// Drag ends, show the dragged row again.
_onDragEnd(row) {
row.opacity = 1.0;
}
_onDragPrepare(point, source, x, y) {
return Gdk.ContentProvider.new_for_value(point);
}
// Drag begins, set the correct drag icon and dim the dragged row.
_onDragBegin(source, row) {
let routeEntry = row.get_child();
let {x, y, width, height} = row.get_allocation();
let paintable = new Gtk.WidgetPaintable({ widget: routeEntry });
source.set_icon(paintable, 0, 0);
row.opacity = 0.6;
}
// Set up drag and drop between RouteEntrys. The drag source is from a
// GtkEventBox that contains the start/end icon next in the entry. And
// the drag destination is the ListBox row.
_initRouteDragAndDrop(routeEntry) {
let dragIcon = routeEntry.icon;
let row = routeEntry.get_parent();
let dragSource = new Gtk.DragSource();
dragIcon.add_controller(dragSource);
dragSource.connect('prepare',
this._onDragPrepare.bind(this, routeEntry.point));
dragSource.connect('drag-begin',
(source, drag, widget) =>
this._onDragBegin(source, row));
dragSource.connect('drag-end',
(source, dele, data) => this._onDragEnd(row));
let dropTarget = Gtk.DropTarget.new(QueryPoint, Gdk.DragAction.COPY);
row.add_controller(dropTarget);
dropTarget.connect('drop',
(target, value, x, y, data) =>
this._onDragDrop(row, value));
}
}
GObject.registerClass({
Template: 'resource:///org/gnome/Maps/ui/sidebar.ui',
InternalChildren: [ 'distanceInfo',
'entryList',
'instructionList',
'instructionWindow',
'instructionSpinner',
'instructionStack',
'errorLabel',
'modeBikeToggle',
'modeCarToggle',
'modePedestrianToggle',
'modeTransitToggle',
'timeInfo',
'linkButtonStack',
'transitWindow',
'transitRevealer',
//'transitOptionsPanel',
'transitHeader',
'transitListStack',
'transitOverviewListBox',
'transitItineraryHeader',
'transitItineraryListBox',
'transitItineraryBackButton',
'transitItineraryTimeLabel',
'transitItineraryDurationLabel',
'transitAttributionLabel']
}, Sidebar);