Commit ac88800d authored by Amisha Singla's avatar Amisha Singla Committed by Jonas Danielsson

Add Print Route Feature

Print Operation class is added to draw the cairo surfaces given to it.
Print Layout acts as a tool-box which sub-classes like short Route
layout , long route layout use to layout the surfaces as per the
respective requirements of rendering various surfaces.
GUI is added to give users an ability to access print Route feature.

https://bugzilla.gnome.org/show_bug.cgi?id=746790
parent c6308b14
......@@ -23,13 +23,20 @@
<property name="accelerator">&lt;Primary&gt;F</property>
</object>
</child>
<child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="title" translatable="yes" context="shortcut window">Toggle route planner</property>
<property name="accelerator">&lt;Primary&gt;D</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="title" translatable="yes" context="shortcut window">Print route</property>
<property name="accelerator">&lt;Primary&gt;P</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
......
......@@ -97,6 +97,27 @@
<property name="pack-type">end</property>
</packing>
</child>
<child>
<object class="GtkButton" id="printRouteButton">
<property name="name">print-route</property>
<property name="can-focus">True</property>
<property name="tooltip-text" translatable="yes">Print Route</property>
<property name="action-name">win.print-route</property>
<property name="valign">center</property>
<style>
<class name="image-button"/>
</style>
<child>
<object class="GtkImage" id="print-route-button-image">
<property name="visible">True</property>
<property name="icon-name">document-print-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack-type">end</property>
</packing>
</child>
</object>
</child>
<child>
......
......@@ -38,6 +38,8 @@ src/osmEditDialog.js
src/placeBubble.js
src/placeEntry.js
src/place.js
src/printLayout.js
src/printOperation.js
src/routeService.js
src/sendToDialog.js
src/shapeLayer.js
......
/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
/* vim: set et ts=4 sw=4: */
/*
* 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: Amisha Singla <amishas157@gmail.com>
*/
const Lang = imports.lang;
const PrintLayout = imports.printLayout;
const _NUM_MINIMAPS = 5;
/* All following constants are ratios of surface size to page size */
const _Instruction = {
SCALE_X: 0.57,
SCALE_Y: 0.05,
SCALE_MARGIN: 0.01
};
const _MiniMapView = {
SCALE_X: 0.4,
SCALE_Y: 0.20,
SCALE_MARGIN: 0.03,
ZOOM_LEVEL: 18
};
const LongPrintLayout = new Lang.Class({
Name: 'LongPrintLayout',
Extends: PrintLayout.PrintLayout,
_init: function(params) {
this._route = params.route;
delete params.route;
/* (Header + 3 maps) + instructions */
let totalSurfaces = 4 + this._route.turnPoints.length;
params.totalSurfaces = totalSurfaces;
this.parent(params);
},
render: function() {
this.parent();
let instructionWidth = _Instruction.SCALE_X * this._pageWidth;
let instructionHeight = _Instruction.SCALE_Y * this._pageHeight;
let instructionMargin = _Instruction.SCALE_MARGIN * this._pageHeight;
let miniMapViewWidth = _MiniMapView.SCALE_X * this._pageWidth;
let miniMapViewHeight = _MiniMapView.SCALE_Y * this._pageHeight;
let miniMapViewMargin = _MiniMapView.SCALE_MARGIN * this._pageHeight;
let miniMapViewZoomLevel = _MiniMapView.ZOOM_LEVEL;
let dy = 0;
let locationsLength = this._route.turnPoints.length;
/* Fixed number of locations are plotted on minimaps which requires a
* check on instructions bound. Later on this can be made dynamic
* depending upon factors like total number of instructions, complexity
* of neighbourhood areas, etc.
*/
let nthStartLocation = Math.min(_NUM_MINIMAPS, locationsLength);
let startLocations = this._createLocationArray(0, nthStartLocation);
this._drawMapView(miniMapViewWidth, miniMapViewHeight,
miniMapViewZoomLevel, startLocations);
/* x-cursor is increased temporarily for rendering instructions */
let tmpX = this._cursorX;
this._route.turnPoints.forEach(function(turnPoint) {
dy = instructionHeight + instructionMargin;
this._adjustPage(dy);
this._cursorX = tmpX + miniMapViewWidth + miniMapViewMargin;
this._drawInstruction(instructionWidth, instructionHeight,
turnPoint);
this._cursorY += dy;
}.bind(this));
this._cursorX = tmpX;
let firstEndLocation = Math.max(0, locationsLength - _NUM_MINIMAPS);
let endLocations = this._createLocationArray(firstEndLocation,
locationsLength);
this._cursorY = Math.max(0, this._cursorY - miniMapViewHeight);
this._drawMapView(miniMapViewWidth, miniMapViewHeight,
miniMapViewZoomLevel, endLocations);
}
});
......@@ -38,6 +38,7 @@ const LocationServiceNotification = imports.locationServiceNotification;
const MapView = imports.mapView;
const PlaceEntry = imports.placeEntry;
const PlaceStore = imports.placeStore;
const PrintOperation = imports.printOperation;
const Sidebar = imports.sidebar;
const Utils = imports.utils;
const ZoomControl = imports.zoomControl;
......@@ -57,7 +58,8 @@ const MainWindow = new Lang.Class({
'gotoUserLocationButton',
'toggleSidebarButton',
'layersButton',
'favoritesButton' ],
'favoritesButton',
'printRouteButton' ],
get mapView() {
return this._mapView;
......@@ -202,6 +204,10 @@ const MainWindow = new Lang.Class({
'find': {
accels: ['<Primary>F'],
onActivate: this._placeEntry.grab_focus.bind(this._placeEntry)
},
'print-route': {
accels: ['<Primary>P'],
onActivate: this._printRouteActivate.bind(this)
}
});
},
......@@ -248,6 +254,9 @@ const MainWindow = new Lang.Class({
this._favoritesButton.sensitive = favoritesPopover.rows > 0;
}).bind(this));
this._mapView.bind_property('routeVisible', this._printRouteButton,
'visible', GObject.BindingFlags.DEFAULT);
Application.geoclue.connect('notify::state',
this._updateLocationSensitivity.bind(this));
this.application.connect('notify::connected', (function() {
......@@ -259,6 +268,7 @@ const MainWindow = new Lang.Class({
this._favoritesButton.sensitive = (app.connected &&
favoritesPopover.rows > 0);
this._placeEntry.sensitive = app.connected;
this._printRouteButton.sensitive = app.connected;
}).bind(this));
},
......@@ -365,6 +375,12 @@ const MainWindow = new Lang.Class({
}
},
_printRouteActivate: function() {
if (this._mapView.routeVisible) {
let operation = new PrintOperation.PrintOperation({ mainWindow: this });
}
},
_onMapTypeMenuActivate: function(action) {
let state = action.get_state().get_boolean();
action.set_state(GLib.Variant.new('b', !state));
......
......@@ -26,6 +26,7 @@
<file>layersPopover.js</file>
<file>location.js</file>
<file>locationServiceNotification.js</file>
<file>longPrintLayout.js</file>
<file>main.js</file>
<file>mainWindow.js</file>
<file>mapBubble.js</file>
......@@ -52,6 +53,8 @@
<file>placeMarker.js</file>
<file>placePopover.js</file>
<file>placeStore.js</file>
<file>printLayout.js</file>
<file>printOperation.js</file>
<file>route.js</file>
<file>routeEntry.js</file>
<file>routeQuery.js</file>
......@@ -61,6 +64,7 @@
<file>settings.js</file>
<file>sendToDialog.js</file>
<file>shapeLayer.js</file>
<file>shortPrintLayout.js</file>
<file>sidebar.js</file>
<file>socialPlace.js</file>
<file>socialPlaceListBox.js</file>
......
/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
/* vim: set et ts=4 sw=4: */
/*
* 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: Amisha Singla <amishas157@gmail.com>
*/
const Cairo = imports.cairo;
const Champlain = imports.gi.Champlain;
const Clutter = imports.gi.Clutter;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;
const Pango = imports.gi.Pango;
const PangoCairo = imports.gi.PangoCairo;
const Application = imports.application;
const InstructionRow = imports.instructionRow;
const MapMarker = imports.mapMarker;
const MapView = imports.mapView;
const TurnPointMarker = imports.turnPointMarker;
/* Following constant has unit as meters */
const _SHORT_LAYOUT_MAX_DISTANCE = 3000;
const _STROKE_COLOR = new Clutter.Color({ red: 0,
blue: 255,
green: 0,
alpha: 100 });
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
};
function newFromRoute(route, pageWidth, pageHeight) {
/*
* To avoid the circular dependencies, imports has
* been carried out in this method
*/
if (route.distance > _SHORT_LAYOUT_MAX_DISTANCE) {
return new imports.longPrintLayout.LongPrintLayout({
route: route,
pageWidth: pageWidth,
pageHeight: pageHeight
});
} else {
return new imports.shortPrintLayout.ShortPrintLayout({
route: route,
pageWidth: pageWidth,
pageHeight: pageHeight
});
}
}
const PrintLayout = new Lang.Class({
Name: 'PrintLayout',
Extends: GObject.Object,
Abstract: true,
Signals: {
'render-complete': { }
},
_init: function(params) {
this._pageWidth = params.pageWidth;
delete params.pageWidth;
this._pageHeight = params.pageHeight;
delete params.pageHeight;
this._totalSurfaces = params.totalSurfaces;
delete params.totalSurfaces;
this.parent();
this.numPages = 0;
this.surfaceObjects = [];
this._surfacesRendered = 0;
this.renderFinished = false;
this._initSignals();
},
render: function() {
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;
this._createNewPage();
let dy = 0;
/*
* Before rendering each surface, page adjustment is done. It is checked if it
* can be adjusted in current page, otherwise a new page is created
*/
dy = headerHeight + headerMargin;
this._adjustPage(dy);
this._drawHeader(headerWidth, headerHeight);
this._cursorY += dy;
dy = mapViewHeight + mapViewMargin;
this._adjustPage(dy);
let locationsLength = this._route.turnPoints.length;
let allLocations = this._createLocationArray(0, locationsLength);
this._drawMapView(mapViewWidth, mapViewHeight,
mapViewZoomLevel, allLocations);
this._cursorY += dy;
},
_initSignals: function() {
this.connect('render-complete', (function() {
this.renderFinished = true;
}).bind(this));
},
_drawMapView: function(width, height, zoomLevel, locations) {
let pageNum = this.numPages - 1;
let x = this._cursorX;
let y = this._cursorY;
let factory = Champlain.MapSourceFactory.dup_default();
let mapSource = factory.create_cached_source(MapView.MapType.STREET);
let view = new Champlain.View({ width: width,
height: height,
zoom_level: zoomLevel });
view.set_map_source(mapSource);
this._addRouteLayer(view);
view.ensure_visible(this._route.createBBox(locations), false);
if (view.state !== Champlain.State.DONE) {
let notifyId = view.connect('notify::state', (function() {
if (view.state === Champlain.State.DONE) {
view.disconnect(notifyId);
let surface = view.to_surface(true);
if (surface)
this._addSurface(surface, x, y, pageNum);
}
}).bind(this));
} else {
let surface = view.to_surface(true);
if (surface)
this._addSurface(surface, x, y, pageNum);
}
},
_createLocationArray: function(startIndex, endIndex) {
let locationArray = [];
for (let i = startIndex; i < endIndex; i++) {
locationArray.push(this._route.turnPoints[i].coordinate);
}
return locationArray;
},
_addRouteLayer: function(view) {
let routeLayer = new Champlain.PathLayer({ stroke_width: _STROKE_WIDTH,
stroke_color: _STROKE_COLOR });
view.add_layer(routeLayer);
this._route.path.forEach(routeLayer.add_node.bind(routeLayer));
},
_drawInstruction: function(width, height, turnPoint) {
let pageNum = this.numPages - 1;
let x = this._cursorX;
let y = this._cursorY;
let instructionWidget = new Gtk.OffscreenWindow({ visible: true });
let instructionEntry = new InstructionRow.InstructionRow({
visible: true,
turnPoint: turnPoint
});
instructionWidget.width_request = width;
instructionWidget.height_request = height;
/* Paint the background of the entry to be transparent */
instructionEntry.connect('draw', (function(widget, cr) {
cr.setSourceRGBA(0.0, 0.0, 0.0, 0.0);
cr.setOperator(Cairo.Operator.SOURCE);
cr.paint();
cr.setOperator(Cairo.Operator.OVER);
}).bind(this));
instructionEntry.queue_draw();
instructionWidget.add(instructionEntry);
instructionWidget.set_valign(Gtk.Align.START);
instructionWidget.connect('damage-event', (function(widget) {
let surface = widget.get_surface();
this._addSurface(surface, x, y, pageNum);
}).bind(this));
},
_drawHeader: function(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 layout = PangoCairo.create_layout(cr);
let from = this._formatQueryPlaceName(0);
let to = this._formatQueryPlaceName(-1);
let header = _("From %s to %s").format(from, to);
let desc = Pango.FontDescription.from_string("sans");
layout.set_text(header, -1);
layout.set_height(Pango.units_from_double(height));
layout.set_width(Pango.units_from_double(width));
layout.set_font_description(desc);
layout.set_alignment(Pango.Alignment.CENTER);
PangoCairo.layout_path(cr, layout);
cr.setSourceRGB(0.0,0.0,0.0);
cr.fill();
this._addSurface(surface, x, y, pageNum);
},
_addSurface: function(surface, x, y, pageNum) {
this.surfaceObjects[pageNum].push({ surface: surface, x: x, y: y });
this._surfacesRendered++;
if (this._surfacesRendered === this._totalSurfaces)
this.emit('render-complete');
},
_adjustPage: function(dy) {
if (this._cursorY + dy > this._pageHeight)
this._createNewPage();
},
_createNewPage: function() {
this.numPages++;
this.surfaceObjects[this.numPages - 1] = [];
this._cursorX = 0;
this._cursorY = 0;
},
_formatQueryPlaceName: function(index) {
let query = Application.routeService.query;
if (index === -1)
index = query.filledPoints.length - 1;
let name;
let place = query.filledPoints[index].place;
if (place.name) {
name = place.name;
if (name.length > 25)
name = name.substr(0, 22) + '\u2026';
} else {
let lat = place.location.latitude.toFixed(5);
let lon = place.location.latitude.toFixed(5);
name = '%s, %s'.format(lat, lon);
}
return name;
}
});
/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
/* vim: set et ts=4 sw=4: */
/*
* 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: Amisha Singla <amishas157@gmail.com>
*/
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;
const Mainloop = imports.mainloop;
const Application = imports.application;
const PrintLayout = imports.printLayout;
const Utils = imports.utils;
const _MIN_TIME_TO_ABORT = 3000;
const PrintOperation = new Lang.Class({
Name: 'PrintOperation',
_init: function(params) {
this._mainWindow = params.mainWindow;
delete params.mainWindow;
this._operation = new Gtk.PrintOperation({ embed_page_setup: true });
this._operation.connect('begin-print', this._beginPrint.bind(this));
this._operation.connect('paginate', this._paginate.bind(this));
this._operation.connect('draw-page', this._drawPage.bind(this));
this._abortDialog = new Gtk.MessageDialog({
transient_for: this._mainWindow,
destroy_with_parent: true,
message_type: Gtk.MessageType.OTHER,
modal: true,
text: _("Loading map tiles for printing"),
secondary_text: _("You can abort printing if this takes too long")
});
this._abortDialog.add_button(_("Abort printing"),
Gtk.ResponseType.CANCEL);
this._responseId = this._abortDialog.connect('response',
this.onAbortDialogResponse.bind(this));
this._runPrintOperation();
},
_beginPrint: function(operation, context, data) {
let route = Application.routeService.route;
let width = context.get_width();
let height = context.get_height();
Mainloop.timeout_add(_MIN_TIME_TO_ABORT, (function() {
if (this._operation.get_status() !== Gtk.PrintStatus.FINISHED) {
this._abortDialog.show();
}
return false;
}).bind(this), null);
this._layout = PrintLayout.newFromRoute(route, width, height);
this._layout.render();
},
onAbortDialogResponse: function(dialog, response) {
if (response === Gtk.ResponseType.DELETE_EVENT ||
response === Gtk.ResponseType.CANCEL) {
this._abortDialog.disconnect(this._responseId);
this._operation.cancel();
this._abortDialog.close();
}
},
_paginate: function(operation, context) {
if (this._layout.renderFinished) {
operation.set_n_pages(this._layout.numPages);
this._abortDialog.close();
}
return this._layout.renderFinished;
},
_drawPage: function(operation, context, page_num, data) {
let cr = context.get_cairo_context();
this._layout.surfaceObjects[page_num].forEach((function(so) {
cr.setSourceSurface(so.surface, so.x, so.y);
cr.paint();
}).bind(this));
},
_runPrintOperation: function() {
let result = this._operation.run(Gtk.PrintOperationAction.PRINT_DIALOG,
this._mainWindow, null);
if (result === Gtk.PrintOperationResult.ERROR) {
let error = this._operation.get_error();
Utils.debug('Failed to print: %s'.format(error));
}
}
});
......@@ -53,7 +53,7 @@ const Route = new Lang.Class({
this.turnPoints = turnPoints;
this.distance = distance;
this.time = time;
this.bbox = bbox || this._createBBox(path);
this.bbox = bbox || this.createBBox(path);
this.emit('update');
},
......@@ -67,7 +67,7 @@ const Route = new Lang.Class({
this.emit('reset');
},
_createBBox: function(coordinates) {
createBBox: function(coordinates) {
let bbox = new Champlain.BoundingBox();
coordinates.forEach(function({ latitude, longitude }) {
bbox.extend(latitude, longitude);
......
/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
/* vim: set et ts=4 sw=4: */
/*
* 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: Amisha Singla <amishas157@gmail.com>
*/
const Lang = imports.lang;
const PrintLayout = imports.printLayout;
/* All following constants are ratios of surface size to page size */
const _Instruction = {
SCALE_X: 1.0,
SCALE_Y: 0.05,
SCALE_MARGIN: 0.01
};
const ShortPrintLayout = new Lang.Class({
Name: 'ShortPrintLayout',
Extends: PrintLayout.PrintLayout,
_init: function(params) {
this._route = params.route;
delete params.route;
/* (Header + map) + instructions */
let totalSurfaces = 2 + this._route.turnPoints.length;
params.totalSurfaces = totalSurfaces;
this.parent(params);
},
render: function() {
this.parent();
let instructionWidth = _Instruction.SCALE_X * this._pageWidth;
let instructionHeight = _Instruction.SCALE_Y * this._pageHeight;
let instructionMargin = _Instruction.SCALE_MARGIN * this._pageHeight;
let dy = 0;
this._route.turnPoints.forEach(function(turnPoint) {
dy = instructionHeight + instructionMargin;
this._adjustPage(dy);
this._drawInstruction(instructionWidth, instructionHeight, turnPoint);
this._cursorY += dy;
}.bind(this));
}
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment