Codebase list gnome-maps / feb76c24-716e-4fcd-b3e1-357626f6d9cc/main src / printLayout.js
feb76c24-716e-4fcd-b3e1-357626f6d9cc/main

Tree @feb76c24-716e-4fcd-b3e1-357626f6d9cc/main (Download .tar.gz)

printLayout.js @feb76c24-716e-4fcd-b3e1-357626f6d9cc/mainraw · history · blame

/* -*- 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 Gdk = imports.gi.Gdk;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
const Pango = imports.gi.Pango;
const PangoCairo = imports.gi.PangoCairo;

const Application = imports.application;
const Color = imports.color;
const MapView = imports.mapView;
const MapSource = imports.mapSource;
const TurnPointMarker = imports.turnPointMarker;
const Utils = imports.utils;

/* 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: 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
};

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
        });
    }
}

var PrintLayout = GObject.registerClass({
    Abstract: true,
    Signals: {
        'render-complete': { }
    }
}, class PrintLayout extends GObject.Object {

    _init(params) {
        this._pageWidth = params.pageWidth;
        delete params.pageWidth;

        this._pageHeight = params.pageHeight;
        delete params.pageHeight;

        this._totalSurfaces = params.totalSurfaces;
        delete params.totalSurfaces;

        super._init(params);

        this.numPages = 0;
        this.surfaceObjects = [];
        this._surfacesRendered = 0;
        this.renderFinished = false;
        this._initSignals();

        this._rtl = Gtk.get_locale_direction() === Gtk.TextDirection.RTL;
        // directional override character to force alignment of labels
        this._dirChar = this._rtl ? '\u200F' : '\u200E';
        this._fontDescription = Pango.FontDescription.from_string("sans");
    }

    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;

        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 turnPointsLength = this._route.turnPoints.length;
        let allTurnPoints = this._createTurnPointArray(0, turnPointsLength);
        this._drawMapView(mapViewWidth, mapViewHeight,
                          mapViewZoomLevel, allTurnPoints);
        this._cursorY += dy;
    }

    _initSignals() {
        this.connect('render-complete', () => this.renderFinished = true);
    }

    _createMarker(turnPoint) {
        return new TurnPointMarker.TurnPointMarker({
            turnPoint: turnPoint,
            queryPoint: {}
        });
    }

    _drawMapView(width, height, zoomLevel, turnPoints) {
        let pageNum = this.numPages - 1;
        let x = this._cursorX;
        let y = this._cursorY;
        let mapSource = MapSource.createPrintSource();
        let locations = [];
        let markerLayer = new Champlain.MarkerLayer();
        let view = new Champlain.View({ width: width,
                                        height: height,
                                        zoom_level: zoomLevel });
        view.set_map_source(mapSource);
        view.add_layer(markerLayer);

        this._addRouteLayer(view);

        turnPoints.forEach((turnPoint) => {
            locations.push(turnPoint.coordinate);
            if (turnPoint.isStop()) {
                markerLayer.add_marker(this._createMarker(turnPoint));
            }
        });

        view.ensure_visible(this._createBBox(locations), 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(locations) {
        let bbox = this._route.createBBox(locations);

        return new Champlain.BoundingBox({ top:    bbox.top,
                                           left:   bbox.left,
                                           bottom: bbox.bottom,
                                           right:  bbox.right });
    }

    _createTurnPointArray(startIndex, endIndex) {
        let turnPointArray = [];
        for (let i = startIndex; i < endIndex; i++) {
            turnPointArray.push(this._route.turnPoints[i]);
        }
        return turnPointArray;
    }

    _addRouteLayer(view) {
        let routeLayer = new Champlain.PathLayer({ stroke_width: _STROKE_WIDTH,
                                                   stroke_color: _STROKE_COLOR });
        view.add_layer(routeLayer);
        this._route.path.forEach((node) => routeLayer.add_node(node));
    }

    _drawIcon(cr, iconName, width, height) {
        let theme = Gtk.IconTheme.get_default();
        let pixbuf = theme.load_icon(iconName, height, 0);
        let iconWidth = pixbuf.width;
        let iconHeight = pixbuf.height;

        Gdk.cairo_set_source_pixbuf(cr, pixbuf,
                                    this._rtl ?
                                    width - height + (height - iconWidth) / 2 :
                                    (height - iconWidth) / 2,
                                    (height - iconWidth) / 2);
        cr.paint();
    }

    _createTextLayout(cr, text, width, height, alignment) {
        let layout = PangoCairo.create_layout(cr);

        layout.set_text(this._dirChar + text, -1);
        layout.set_height(Pango.units_from_double(height));
        layout.set_width(Pango.units_from_double(width));
        layout.set_font_description(this._fontDescription);
        layout.set_alignment(alignment);
        layout.set_ellipsize(Pango.EllipsizeMode.END);

        return layout;
    }

    _drawText(cr, text, x, y, width, height, alignment) {
        this._drawTextWithColor(cr, text, x, y, width, height, '000000', alignment);
    }

    _drawTextWithColor(cr, text, x, y, width, height, color, alignment) {
        let layout = this._createTextLayout(cr, text, width, height, alignment);

        this._drawTextLayoutWithColor(cr, layout, x, y, width, height, color,
                                      alignment);
    }

    _drawTextLayoutWithColor(cr, layout, x, y, width, height, color, alignment) {
        let red = Color.parseColor(color, 0);
        let green = Color.parseColor(color, 1);
        let blue = Color.parseColor(color, 2);

        cr.moveTo(x, y);
        cr.setSourceRGB(red, green, blue);
        PangoCairo.show_layout(cr, layout);
    }

    _drawTextVerticallyCentered(cr, text, width, height, x, alignment) {
        this._drawTextVerticallyCenteredWithColor(cr, text, width, height, x,
                                                  '000000', alignment);
    }

    _drawTextVerticallyCenteredWithColor(cr, text, width, height, x, color,
                                         alignment) {
        let layout = this._createTextLayout(cr, text, width, height, alignment);
        let [pWidth, pHeight] = layout.get_pixel_size();
        let red = Color.parseColor(color, 0);
        let green = Color.parseColor(color, 1);
        let blue = Color.parseColor(color, 2);

        // place text centered
        cr.moveTo(x, (height - pHeight) / 2);
        cr.setSourceRGB(red, green, blue);
        PangoCairo.show_layout(cr, layout);
    }

    _drawInstruction(width, height, turnPoint) {
        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 iconName = turnPoint.iconName;

        if (iconName) {
            this._drawIcon(cr, iconName, width, height);
        }

        // draw the instruction text
        this._drawTextVerticallyCentered(cr, turnPoint.instruction,
                                         width - height * 3, height,
                                         this._rtl ? height * 2 : height,
                                         Pango.Alignment.LEFT);

        // draw the distance text
        if (turnPoint.distance > 0) {
            this._drawTextVerticallyCentered(cr,
                                             Utils.prettyDistance(turnPoint.distance),
                                             height * 2,
                                             height,
                                             this._rtl ? 0 : width - height * 2,
                                             Pango.Alignment.RIGHT);
        }

        this._addSurface(surface, x, y, pageNum);
    }

    _drawHeader(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(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(dy) {
        if (this._cursorY + dy > this._pageHeight)
            this._createNewPage();
    }

    _createNewPage() {
        this.numPages++;
        this.surfaceObjects[this.numPages - 1] = [];
        this._cursorX = 0;
        this._cursorY = 0;
    }

    _formatQueryPlaceName(index) {
        let query = Application.routeQuery;
        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;
    }
});