/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
/* vim: set et ts=4 sw=4: */
/*
* Copyright (c) 2019 Marcus Lundblad
*
* 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 <http://www.gnu.org/licenses/>.
*
* Author: Marcus Lundblad <ml@update.uu.se>
*/
const Champlain = imports.gi.Champlain;
const Service = imports.service;
const TransitPlan = imports.transitPlan;
const Utils = imports.utils;
/**
* Class responsible for delegating requests to perform routing in transit
* mode.
* Holds the the shared plan instance (filled with journeys on successful
* requests).
*/
var TransitRouter = class TransitRoute {
constructor(params) {
this._plan = new TransitPlan.Plan();
this._query = params.query;
this._providers = Service.getService().transitProviders;
this._providerCache = [];
this._language = Utils.getLanguage();
this._probePlugins();
}
get enabled() {
return this._providers !== undefined;
}
get plan() {
return this._plan;
}
/**
* Called when the query has been updated to trigger the first set
* of results-
*/
fetchFirstResults() {
let bestProvider = this._getBestProviderForQuery();
if (bestProvider) {
let provider = bestProvider[0];
this._currPluginInstance = bestProvider[1];
this._plan.attribution = this._getAttributionForProvider(provider);
if (provider.attributionUrl)
this._plan.attributionUrl = provider.attributionUrl;
this._currPluginInstance.fetchFirstResults();
} else {
this._plan.reset();
this._plan.noProvider();
}
}
/**
* Called to fetch additional (later or earlier) results depending on the
* query settings.
*/
fetchMoreResults() {
if (this._currPluginInstance)
this._currPluginInstance.fetchMoreResults();
else
throw new Error('No previous provider');
}
_probePlugins() {
this._availablePlugins = [];
for (let module in imports.transitplugins) {
for (let pluginClass in imports.transitplugins[module]) {
this._availablePlugins[pluginClass] = module;
}
}
}
/**
* Get attribution for a provider. Returns a language-specific
* 'attribution:<lang>' tag if available, otherwise 'attribution'
*/
_getAttributionForProvider(provider) {
if (provider['attribution:' + this._language])
return provider['attribution:' + this._language];
else if (provider.attribution)
return provider.attribution;
else
return null;
}
_getMatchingProvidersForLocation(location) {
let country = Utils.getCountryCodeForCoordinates(location.latitude,
location.longitude);
let matchingProviders = [];
this._providers.forEach((p) => {
let provider = p.provider;
let areas = provider.areas;
if (!areas) {
Utils.debug('No coverage info for provider ' + provider.name);
return;
}
areas.forEach((area) => {
/* if the area has a specified priority, override the
* overall area priority, this allows sub-areas of of
* coverage for a provider to have higher or lowe priorities
* than other providers (e.g. one "native" to that area
*/
if (area.priority)
provider.priority = area.priority;
let countries = area.countries;
if (countries) {
if (countries.includes(country)) {
matchingProviders[provider.name] = provider;
return;
}
}
let bbox = area.bbox;
if (bbox) {
if (bbox.length !== 4) {
Utils.debug('malformed bounding box for provider ' + provider.name);
return;
}
let [x1, y1, x2, y2] = bbox;
let cbbox = new Champlain.BoundingBox({ bottom: x1,
left: y1,
top: x2,
right: y2 });
if (cbbox.covers(location.latitude,
location.longitude)) {
matchingProviders[provider.name] = provider;
return;
}
}
});
});
return matchingProviders;
}
/**
* Get the most preferred provider for a given query.
* Return: an array with the provider definition and the plugin instance,
* or null if no matching provider was found.
*/
_getBestProviderForQuery() {
let startLocation = this._query.filledPoints[0].place.location;
let endLocation =
this._query.filledPoints.last().place.location;
let matchingProvidersForStart =
this._getMatchingProvidersForLocation(startLocation);
let matchingProvidersForEnd =
this._getMatchingProvidersForLocation(endLocation);
let matchingProviders = [];
// check all candidate providers matching on the start location
for (let name in matchingProvidersForStart) {
let providerAtStart = matchingProvidersForStart[name];
let providerAtEnd = matchingProvidersForEnd[name];
/* if the provider also matches on the end location, consider it
* as a potential candidate
*/
if (providerAtEnd) {
let order = this._sortProviders(providerAtStart, providerAtEnd);
/* add the provider at it lowest priority to favor higher
* priority providers in "fringe cases"
*/
if (order < 0)
matchingProviders.push(providerAtStart);
else
matchingProviders.push(providerAtEnd);
}
}
matchingProviders.sort(this._sortProviders);
for (let i = 0; i < matchingProviders.length; i++) {
let provider = matchingProviders[i];
let plugin = provider.plugin;
if (this._providerCache[provider.name])
return [provider, this._providerCache[provider.name]];
let module = this._availablePlugins[plugin];
if (module) {
try {
let params = provider.params;
let instance =
params ? new imports.transitplugins[module][plugin](params) :
new imports.transitplugins[module][plugin]();
this._providerCache[provider.name] = instance;
return [provider, instance];
} catch (e) {
Utils.debug('Failed to instanciate transit plugin: ' +
plugin + ": " + e);
}
} else {
Utils.debug('Transit provider plugin not available: ' + plugin);
}
}
Utils.debug('No suitable transit provider found');
return null;
}
/**
* Sort function to sort providers in by preference.
* If both providers have a priority set, prefers the one with a lower
* value (higher prio), otherwise the one that has a priority set (if any),
* else no specific order.
*/
_sortProviders(p1, p2) {
if (p1.priority && p2.priority)
return p1.priority - p2.priority;
else if (p1.priority)
return -1;
else if (p2.priority)
return 1;
else
return 0;
}
};