/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
/* vim: set et ts=4 sw=4: */
/*
* Copyright (c) 2017 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 .
*
* Author: Marcus Lundblad
*/
const Cairo = imports.cairo;
const Champlain = imports.gi.Champlain;
const Clutter = imports.gi.Clutter;
const GObject = imports.gi.GObject;
const Pango = imports.gi.Pango;
const Color = imports.color;
const Gfx = imports.gfx;
const MapSource = imports.mapSource;
const PrintLayout = imports.printLayout;
const Transit = imports.transit;
const TransitArrivalMarker = imports.transitArrivalMarker;
const TransitBoardMarker = imports.transitBoardMarker;
const TransitWalkMarker = imports.transitWalkMarker;
// stroke color for walking paths
const _STROKE_COLOR = new Clutter.Color({ red: 0,
blue: 0,
green: 0,
alpha: 255 });
const _STROKE_WIDTH = 5.0;
// All following constants are ratios of surface size to page size
const _Header = {
SCALE_X: 0.9,
SCALE_Y: 0.03,
SCALE_MARGIN: 0.01
};
const _MapView = {
SCALE_X: 1.0,
SCALE_Y: 0.4,
SCALE_MARGIN: 0.04,
ZOOM_LEVEL: 18
};
const _Instruction = {
SCALE_X: 0.9,
SCALE_Y: 0.1,
SCALE_MARGIN: 0.01
};
// luminance threashhold for drawing outline around route label badges
const OUTLINE_LUMINANCE_THREASHHOLD = 0.9;
var TransitPrintLayout = GObject.registerClass(
class TransitPrintLayout extends PrintLayout.PrintLayout {
_init(params) {
this._itinerary = params.itinerary;
delete params.itinerary;
params.totalSurfaces = this._getNumberOfSurfaces();
super._init(params);
}
_getNumberOfSurfaces() {
// always one fixed surface for the title label
let numSurfaces = 1;
for (let i = 0; i < this._itinerary.legs.length; i++) {
numSurfaces++;
// add a surface when a leg of the itinerary should have a map view
if (this._legHasMiniMap(i))
numSurfaces++;
}
// always include the arrival row
numSurfaces++;
return numSurfaces;
}
_drawMapView(width, height, zoomLevel, index) {
let pageNum = this.numPages - 1;
let x = this._cursorX;
let y = this._cursorY;
let mapSource = MapSource.createPrintSource();
let markerLayer = new Champlain.MarkerLayer();
let view = new Champlain.View({ width: width,
height: height,
zoom_level: zoomLevel });
let leg = this._itinerary.legs[index];
let nextLeg = this._itinerary.legs[index + 1];
let previousLeg = index === 0 ? null : this._itinerary.legs[index - 1];
view.set_map_source(mapSource);
/* we want to add the path layer before the marker layer, so that
* boarding marker are drawn about the walk dash lines
*/
this._addRouteLayer(view, index);
view.add_layer(markerLayer);
markerLayer.add_marker(this._createStartMarker(leg, previousLeg));
if (nextLeg)
markerLayer.add_marker(this._createBoardMarker(nextLeg));
else
markerLayer.add_marker(this._createArrivalMarker(leg));
/* in some cases, we seem to get get zero distance walking instructions
* within station complexes, don't try to show a bounding box for low
* distances, instead center on the spot
*/
if (leg.distance < 10)
view.center_on(leg.fromCoordinate[0], leg.fromCoordinate[1]);
else
view.ensure_visible(this._createBBox(leg), false);
if (view.state !== Champlain.State.DONE) {
let notifyId = view.connect('notify::state', () => {
if (view.state === Champlain.State.DONE) {
view.disconnect(notifyId);
let surface = view.to_surface(true);
if (surface)
this._addSurface(surface, x, y, pageNum);
}
});
} else {
let surface = view.to_surface(true);
if (surface)
this._addSurface(surface, x, y, pageNum);
}
}
_createBBox(leg) {
return Champlain.BoundingBox({ top: leg.bbox.top,
left: leg.bbox.left,
bottom: leg.bbox.bottom,
right: leg.bbox.right });
}
_createStartMarker(leg, previousLeg) {
return new TransitWalkMarker.TransitWalkMarker({ leg: leg,
previousLeg: previousLeg });
}
_createBoardMarker(leg) {
return new TransitBoardMarker.TransitBoardMarker({ leg: leg });
}
_createArrivalMarker(leg) {
return new TransitArrivalMarker.TransitArrivalMarker({ leg: leg });
}
_addRouteLayer(view, index) {
let routeLayer = new Champlain.PathLayer({ stroke_width: _STROKE_WIDTH,
stroke_color: _STROKE_COLOR });
let leg = this._itinerary.legs[index];
routeLayer.set_dash([5, 5]);
view.add_layer(routeLayer);
/* if this is a walking leg and not at the start, "stitch" it
* together with the end point of the previous leg, as the walk
* route might not reach all the way
*/
if (index > 0 && !leg.transit) {
let previousLeg = this._itinerary.legs[index - 1];
let lastPoint =
previousLeg.polyline[previousLeg.polyline.length - 1];
routeLayer.add_node(lastPoint);
}
leg.polyline.forEach((node) => routeLayer.add_node(node));
/* like above, "stitch" the route segment with the next one if it's
* a walking leg, and not the last one
*/
if (index < this._itinerary.legs.length - 1 && !leg.transit) {
let nextLeg = this._itinerary.legs[index + 1];
let firstPoint = nextLeg.polyline[0];
routeLayer.add_node(firstPoint);
}
}
_drawInstruction(width, height, leg, start) {
let pageNum = this.numPages - 1;
let x = this._cursorX;
let y = this._cursorY;
let surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, width, height);
let cr = new Cairo.Context(surface);
let timeWidth = !leg.transit && start ? height : height * 2;
let fromText = Transit.getFromLabel(leg, start);
let routeWidth = 0;
this._drawIcon(cr, leg.iconName, width, height);
this._drawText(cr, fromText, this._rtl ? timeWidth : height, 0,
width - height - timeWidth, height / 2, Pango.Alignment.LEFT);
if (leg.transit) {
let color = leg.color;
let textColor = leg.textColor;
let hasOutline = Color.relativeLuminance(color) > OUTLINE_LUMINANCE_THREASHHOLD;
let routeText =
this._createTextLayout(cr, leg.route, width - height - timeWidth,
height / 2,
this._rtl ? Pango.Alignment.RIGHT :
Pango.Alignment.LEFT);
let [pWidth, pHeight] = routeText.get_pixel_size();
let routePadding = 3;
let routeHeight = pHeight + routePadding * 2;
routeWidth = Math.max(pWidth, pHeight) + routePadding * 2;
let routeX = this._rtl ? width - height - routeWidth - 1 : height;
let routeY = height / 2 + ((height / 2) - routeHeight) / 2;
textColor = Color.getContrastingForegroundColor(color, textColor);
Gfx.drawColoredBagde(cr, color, hasOutline ? textColor : null,
routeX, routeY, routeWidth, routeHeight);
this._drawTextLayoutWithColor(cr, routeText,
routeX + routePadding +
(routeWidth - pWidth -
routePadding * 2) / 2,
routeY + routePadding,
routeWidth - routePadding * 2,
routeHeight - routePadding * 2,
textColor, Pango.Alignment.LEFT);
// introduce some additional padding before the headsign label
routeWidth += routePadding;
}
let headsign = Transit.getHeadsignLabel(leg);
if (headsign) {
let headsignLayout = this._createTextLayout(cr, headsign,
width - height - timeWidth - routeWidth,
height / 2,
Pango.Alignment.LEFT);
let [pWidth, pHeight] = headsignLayout.get_pixel_size();
this._drawTextLayoutWithColor(cr, headsignLayout,
this._rtl ? timeWidth : height + routeWidth,
height / 2 + (height / 2 - pHeight) / 2,
width - height - timeWidth - routeWidth,
height / 2, '888888',
Pango.Alignment.LEFT);
}
this._drawTextVerticallyCentered(cr, leg.prettyPrintTime({ isStart: start }),
timeWidth, height,
this._rtl ? 0 : width - timeWidth - 1,
Pango.Alignment.RIGHT);
this._addSurface(surface, x, y, pageNum);
}
_drawArrival(width, height) {
let pageNum = this.numPages - 1;
let x = this._cursorX;
let y = this._cursorY;
let surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, width, height);
let cr = new Cairo.Context(surface);
let lastLeg = this._itinerary.legs[this._itinerary.legs.length - 1];
this._drawIcon(cr, 'maps-point-end-symbolic', width, height);
// draw the arrival text
this._drawTextVerticallyCentered(cr, Transit.getArrivalLabel(lastLeg),
width - height * 3,
height, this._rtl ? height * 2 : height,
Pango.Alignment.LEFT);
// draw arrival time
this._drawTextVerticallyCentered(cr, lastLeg.prettyPrintArrivalTime(),
height, height,
this._rtl ? 0 : width - height - 1,
Pango.Alignment.RIGHT);
this._addSurface(surface, x, y, pageNum);
}
_legHasMiniMap(index) {
let leg = this._itinerary.legs[index];
return !leg.transit;
}
render() {
let headerWidth = _Header.SCALE_X * this._pageWidth;
let headerHeight = _Header.SCALE_Y * this._pageHeight;
let headerMargin = _Header.SCALE_MARGIN * this._pageHeight;
let mapViewWidth = _MapView.SCALE_X * this._pageWidth;
let mapViewHeight = _MapView.SCALE_Y * this._pageHeight;
let mapViewMargin = _MapView.SCALE_MARGIN * this._pageHeight;
let mapViewZoomLevel = _MapView.ZOOM_LEVEL;
let instructionWidth = _Instruction.SCALE_X * this._pageWidth;
let instructionHeight = _Instruction.SCALE_Y * this._pageHeight;
let instructionMargin = _Instruction.SCALE_MARGIN * this._pageHeight;
let dy = headerHeight + headerMargin;
this._createNewPage();
this._adjustPage(dy);
this._drawHeader(headerWidth, headerHeight);
this._cursorY += dy;
for (let i = 0; i < this._itinerary.legs.length; i++) {
let leg = this._itinerary.legs[i];
let hasMap = this._legHasMiniMap(i);
let instructionDy = instructionHeight + instructionMargin;
let mapDy = hasMap ? mapViewHeight + mapViewMargin : 0;
dy = instructionDy + mapDy;
this._adjustPage(dy);
this._drawInstruction(instructionWidth, instructionHeight, leg,
i === 0);
this._cursorY += instructionDy;
if (hasMap) {
let nextLeg = i < this._itinerary.legs.length - 1 ?
this._itinerary.legs[i + 1] : null;
this._drawMapView(mapViewWidth, mapViewHeight, mapViewZoomLevel, i);
this._cursorY += mapDy;
}
}
this._drawArrival(instructionWidth, instructionHeight);
}
});