MapWidget.vala 32.2 KB
Newer Older
1 2 3 4 5 6
/* Copyright 2016 Software Freedom Conservancy Inc.
 *
 * This software is licensed under the GNU LGPL (version 2.1 or later).
 * See the COPYING file in this distribution.
 */

7 8 9 10 11 12 13 14
private class MarkerImageSet {
    public float marker_image_width;
    public float marker_image_height;
    public Clutter.Image? marker_image;
    public Clutter.Image? marker_selected_image;
    public Clutter.Image? marker_highlighted_image;
}

15 16 17 18 19 20
private enum SelectionAction {
    SET,
    ADD,
    REMOVE
}

21
private abstract class PositionMarker : Object {
22
    protected bool _highlighted = false;
23
    protected bool _selected = false;
24 25 26 27 28 29 30 31 32 33 34 35 36
    protected MarkerImageSet image_set;

    protected PositionMarker(Champlain.Marker champlain_marker, MarkerImageSet image_set) {
        this.champlain_marker = champlain_marker;
        this.image_set = image_set;
        champlain_marker.selectable = true;
        champlain_marker.set_content(image_set.marker_image);
        float w = image_set.marker_image_width;
        float h = image_set.marker_image_height;
        champlain_marker.set_size(w, h);
        champlain_marker.set_translation(-w * MapWidget.MARKER_IMAGE_HORIZONTAL_PIN_RATIO,
                                         -h * MapWidget.MARKER_IMAGE_VERTICAL_PIN_RATIO, 0);
    }
37 38 39 40

    public Champlain.Marker champlain_marker { get; protected set; }

    public bool highlighted {
41 42 43
        get {
            return _highlighted;
        }
44
        set {
45 46 47 48 49
            if (_highlighted == value)
                return;
            _highlighted = value;
            var base_image = _selected ? image_set.marker_selected_image : image_set.marker_image;
            champlain_marker.set_content(value ? image_set.marker_highlighted_image : base_image);
50 51 52 53 54 55 56
        }
    }
    public bool selected {
        get {
            return _selected;
        }
        set {
57 58
            if (_selected == value)
                return;
59
            _selected = value;
60 61 62 63
            if (!_highlighted) {
                var base_image = value ?  image_set.marker_selected_image : image_set.marker_image;
                champlain_marker.set_content(base_image);
            }
64 65
            champlain_marker.set_selected(value);
        }
66
    }
67
}
68

69
private class DataViewPositionMarker : PositionMarker {
70 71
    private Gee.LinkedList<weak DataViewPositionMarker> _data_view_position_markers =
            new Gee.LinkedList<weak DataViewPositionMarker>();
72 73 74

    public weak DataView view { get; protected set; }

75 76 77
    public DataViewPositionMarker(DataView view, Champlain.Marker champlain_marker,
            MarkerImageSet image_set) {
        base(champlain_marker, image_set);
78
        this.view = view;
79

80 81 82 83
        this._data_view_position_markers.add(this);
    }

    public void bind_mouse_events(MapWidget map_widget) {
84
        champlain_marker.button_release_event.connect ((event) => {
85
            if (event.button > 1)
86
                return true;
87 88 89 90 91 92 93
            bool mod = (bool)(event.modifier_state &
                    (Clutter.ModifierType.CONTROL_MASK | Clutter.ModifierType.SHIFT_MASK));
            SelectionAction action = SelectionAction.SET;
            if (mod)
                action = _selected ? SelectionAction.REMOVE : SelectionAction.ADD;
            selected = (action != SelectionAction.REMOVE);
            map_widget.select_data_views(_data_view_position_markers, action);
94 95
            return true;
        });
96
        champlain_marker.enter_event.connect ((event) => {
97
            highlighted = true;
98
            map_widget.highlight_data_views(_data_view_position_markers);
99 100
            return true;
        });
101
        champlain_marker.leave_event.connect ((event) => {
102
            highlighted = false;
103
            map_widget.unhighlight_data_views(_data_view_position_markers);
104 105 106
            return true;
        });
    }
107
}
108

109
private class MarkerGroup : PositionMarker {
110 111
    private Gee.Collection<weak DataViewPositionMarker> _data_view_position_markers =
        new Gee.LinkedList<weak DataViewPositionMarker>();
112 113 114
    private Gee.Collection<PositionMarker> _position_markers = new Gee.LinkedList<PositionMarker>();
    private Champlain.BoundingBox bbox = new Champlain.BoundingBox();

115 116
    public void bind_mouse_events(MapWidget map_widget) {
        champlain_marker.button_release_event.connect ((event) => {
117
            if (event.button > 1)
118
                return true;
119 120 121 122 123 124
            bool mod = (bool)(event.modifier_state &
                    (Clutter.ModifierType.CONTROL_MASK | Clutter.ModifierType.SHIFT_MASK));
            SelectionAction action = SelectionAction.SET;
            if (mod)
                action = _selected ? SelectionAction.REMOVE : SelectionAction.ADD;
            selected = (action != SelectionAction.REMOVE);
125
            foreach (var m in _data_view_position_markers) {
126
                m.selected = _selected;
127
            }
128
            map_widget.select_data_views(_data_view_position_markers.read_only_view, action);
129 130 131
            return true;
        });
        champlain_marker.enter_event.connect ((event) => {
132
            highlighted = true;
133 134 135 136
            map_widget.highlight_data_views(_data_view_position_markers.read_only_view);
            return true;
        });
        champlain_marker.leave_event.connect ((event) => {
137
            highlighted = false;
138 139 140
            map_widget.unhighlight_data_views(_data_view_position_markers.read_only_view);
            return true;
        });
141 142 143 144 145 146
    }

    public Gee.Collection<PositionMarker> position_markers {
        owned get { return _position_markers.read_only_view; }
    }

147 148
    public MarkerGroup(Champlain.Marker champlain_marker, MarkerImageSet image_set) {
        base(champlain_marker, image_set);
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
    }

    public void add_position_marker(PositionMarker marker) {
        var data_view_position_marker = marker as DataViewPositionMarker;
        if (data_view_position_marker != null)
            _data_view_position_markers.add(data_view_position_marker);
        var new_champlain_marker = marker.champlain_marker;
        bbox.extend(new_champlain_marker.latitude, new_champlain_marker.longitude);
        double lat, lon;
        bbox.get_center(out lat, out lon);
        champlain_marker.set_location(lat, lon);
        _position_markers.add(marker);
    }
}

private class MarkerGroupRaster : Object {
    private const long MARKER_GROUP_RASTER_WIDTH_PX = 30l;
    private const long MARKER_GROUP_RASTER_HEIGHT_PX = 30l;

168 169 170
    private weak MapWidget map_widget;
    private weak Champlain.View map_view;
    private weak Champlain.MarkerLayer marker_layer;
171

172 173 174 175 176 177
    public bool is_empty {
        get {
            return position_markers.is_empty;
        }
    }

178 179 180 181 182 183 184 185 186 187
    // position_markers_tree is a two-dimensional tree for grouping position
    // markers indexed by x (outer tree) and y (inner tree) raster coordinates.
    // It maps coordinates to the PositionMarker (DataViewMarker or MarkerGroup)
    // corresponding to them.
    // If either raster index keys are empty, there is no marker within the
    // raster cell. If both exist there are two possibilities:
    // (1) the value is a MarkerGroup which means that multiple markers are
    // grouped together, or (2) the value is a PositionMarker (but not a
    // MarkerGroup) which means that there is exactly one marker in the raster
    // cell. The tree is recreated every time the zoom level changes.
188 189 190 191
    private Gee.TreeMap<long, Gee.TreeMap<long, unowned PositionMarker?>?> position_markers_tree =
        new Gee.TreeMap<long, Gee.TreeMap<long, unowned PositionMarker?>?>();
    // The marker group's collection keeps track of and owns all PositionMarkers including the marker groups
    private Gee.Map<DataView, unowned PositionMarker> data_view_map = new Gee.HashMap<DataView, unowned PositionMarker>();
192 193 194 195 196 197 198 199 200 201
    private Gee.Set<PositionMarker> position_markers = new Gee.HashSet<PositionMarker>();

    public MarkerGroupRaster(MapWidget map_widget, Champlain.View map_view, Champlain.MarkerLayer marker_layer) {
        this.map_widget = map_widget;
        this.map_view = map_view;
        this.marker_layer = marker_layer;
        map_widget.zoom_changed.connect(regroup);
    }

    public void clear() {
202 203 204 205 206
        lock (position_markers) {
            data_view_map.clear();
            position_markers_tree.clear();
            position_markers.clear();
        }
207 208
    }

209 210 211 212 213 214 215 216
    public void clear_selection() {
        lock (position_markers) {
            foreach (PositionMarker m in position_markers) {
                m.selected = false;
            }
        }
    }

217
    public unowned PositionMarker? find_position_marker(DataView data_view) {
218 219
        if (!data_view_map.has_key(data_view))
            return null;
220
        unowned PositionMarker? m;
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
        lock (position_markers) {
            m = data_view_map.get(data_view);
        }
        return m;
    }

    public void rasterize_marker(PositionMarker position_marker, bool already_on_map=false) {
        var data_view_position_marker = position_marker as DataViewPositionMarker;
        var champlain_marker = position_marker.champlain_marker;
        long x, y;

        lock (position_markers) {
            rasterize_coords(champlain_marker.longitude, champlain_marker.latitude, out x, out y);
            var yg = position_markers_tree.get(x);
            if (yg == null) {
236
                yg = new Gee.TreeMap<long, unowned PositionMarker?>();
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
                position_markers_tree.set(x, yg);
            }
            var cell = yg.get(y);
            if (cell == null) {
                // first marker in this raster cell
                yg.set(y, position_marker);
                position_markers.add(position_marker);
                if (!already_on_map)
                    marker_layer.add_marker(position_marker.champlain_marker);
                if (data_view_position_marker != null)
                    data_view_map.set(data_view_position_marker.view, position_marker);

            } else {
                var marker_group = cell as MarkerGroup;
                if (marker_group == null) {
                    // single marker already occupies raster cell: create new group
                    GpsCoords rasterized_gps_coords = GpsCoords() {
                        has_gps = 1,
                        longitude = map_view.x_to_longitude(x),
                        latitude = map_view.y_to_latitude(y)
                    };
                    marker_group = map_widget.create_marker_group(rasterized_gps_coords);
                    marker_group.add_position_marker(cell);
260 261
                    if (cell.selected) // group becomes selected if any contained marker is
                        marker_group.selected = true;
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
                    if (cell is DataViewPositionMarker)
                        data_view_map.set(((DataViewPositionMarker) cell).view, marker_group);
                    yg.set(y, marker_group);
                    position_markers.add(marker_group);
                    position_markers.remove(cell);
                    marker_layer.add_marker(marker_group.champlain_marker);
                    marker_layer.remove_marker(cell.champlain_marker);
                }
                // group already exists, add new marker to it
                marker_group.add_position_marker(position_marker);
                if (already_on_map)
                    marker_layer.remove_marker(position_marker.champlain_marker);
                if (data_view_position_marker != null)
                    data_view_map.set(data_view_position_marker.view, marker_group);
            }
        }
278
    }
279 280 281 282 283 284

    private void rasterize_coords(double longitude, double latitude, out long x, out long y) {
        x = (Math.lround(map_view.longitude_to_x(longitude) / MARKER_GROUP_RASTER_WIDTH_PX)) *
            MARKER_GROUP_RASTER_WIDTH_PX + (MARKER_GROUP_RASTER_WIDTH_PX / 2);
        y = (Math.lround(map_view.latitude_to_y(latitude) / MARKER_GROUP_RASTER_HEIGHT_PX)) *
            MARKER_GROUP_RASTER_HEIGHT_PX + (MARKER_GROUP_RASTER_HEIGHT_PX / 2);
285
    }
286

287
    internal void regroup() {
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
        lock (position_markers) {
            var position_markers_current = (owned) position_markers;
            position_markers = new Gee.HashSet<PositionMarker>();
            position_markers_tree.clear();

            foreach (var pm in position_markers_current) {
                var marker_group = pm as MarkerGroup;
                if (marker_group != null) {
                    marker_layer.remove_marker(marker_group.champlain_marker);
                    foreach (var position_marker in marker_group.position_markers) {
                        rasterize_marker(position_marker, false);
                    }
                } else {
                    rasterize_marker(pm, true);
                }
            }
            position_markers_current = null;
        }
306 307 308 309
    }
}

private class MapWidget : Gtk.Bin {
Jens Georg's avatar
Jens Georg committed
310
    private const string MAPBOX_API_TOKEN = "pk.eyJ1IjoiamVuc2dlb3JnIiwiYSI6ImNqZ3FtYmhrMTBkOW8yeHBlNG8xN3hlNTAifQ.ek7i8UHeNIlkKi10fhgFgg";
311 312 313
    private const uint DEFAULT_ZOOM_LEVEL = 8;

    private static MapWidget instance = null;
314
    private bool hide_map = false;
315 316 317 318 319

    private GtkChamplain.Embed gtk_champlain_widget = new GtkChamplain.Embed();
    private Champlain.View map_view = null;
    private Champlain.Scale map_scale = new Champlain.Scale();
    private Champlain.MarkerLayer marker_layer = new Champlain.MarkerLayer();
320
    public bool map_edit_lock { get; set; }
321
    private MarkerGroupRaster marker_group_raster = null;
322 323
    private Gee.Map<DataView, unowned DataViewPositionMarker> data_view_marker_cache =
        new Gee.HashMap<DataView, unowned DataViewPositionMarker>();
324
    private weak Page? page = null;
325 326 327
    private Clutter.Image? map_edit_locked_image;
    private Clutter.Image? map_edit_unlocked_image;
    private Clutter.Actor map_edit_lock_button = new Clutter.Actor();
328
    private uint position_markers_timeout = 0;
329

330 331
    public const float MARKER_IMAGE_HORIZONTAL_PIN_RATIO = 0.5f;
    public const float MARKER_IMAGE_VERTICAL_PIN_RATIO = 0.825f;
332 333
    public float map_edit_lock_image_width { get; private set; }
    public float map_edit_lock_image_height { get; private set; }
334 335
    public MarkerImageSet marker_image_set { get; private set; }
    public MarkerImageSet marker_group_image_set { get; private set; }
336
    public const Clutter.Color marker_point_color = { 10, 10, 255, 192 };
337

338 339
    public signal void zoom_changed();

340 341
    private MapWidget() {
        setup_map();
342
        add(gtk_champlain_widget);
343 344 345 346 347 348 349 350
    }

    public static MapWidget get_instance() {
        if (instance == null)
            instance = new MapWidget();
        return instance;
    }

351
    public override bool drag_motion(Gdk.DragContext context, int x, int y, uint time) {
352 353 354 355
        if (!map_edit_lock)
            map_view.stop_go_to();
        else
            Gdk.drag_status(context, 0, time);
356 357 358
        return true;
    }

359
    public override void drag_data_received(Gdk.DragContext context, int x, int y,
360
            Gtk.SelectionData selection_data, uint info, uint time) {
361 362 363 364 365 366 367 368 369 370 371 372
        bool success = false;
        Gee.List<MediaSource>? media = unserialize_media_sources(selection_data.get_data(),
            selection_data.get_length());
        if (media != null && media.size > 0) {
            double lat = map_view.y_to_latitude(y);
            double lon = map_view.x_to_longitude(x);
            success = internal_drop_received(media, lat, lon);
        }

        Gtk.drag_finish(context, success, false, time);
    }

373 374 375 376 377 378 379 380 381 382 383
    public new void set_visible(bool visible) {
        /* hides Gtk.Widget.set_visible */
        hide_map = !visible;
        base.set_visible(visible);
    }

    public override void show_all() {
        if (!hide_map)
            base.show_all();
    }

384
    public void set_page(Page page) {
385
        bool page_changed = false;
386 387
        if (this.page != page) {
            this.page = page;
388 389 390 391 392 393 394 395
            page_changed = true;
            clear();
        }
        ViewCollection view_collection = page.get_view();
        if (view_collection == null)
            return;

        if (page_changed) {
396
            data_view_marker_cache.clear();
397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
            foreach (DataObject view in view_collection.get_all()) {
                if (view is DataView)
                    add_data_view((DataView) view);
            }
            show_position_markers();
        }
        // In any case, the selection did change..
        var selected = view_collection.get_selected();
        if (selected != null) {
            marker_group_raster.clear_selection();
            foreach (DataView v in view_collection.get_selected()) {

                var position_marker = marker_group_raster.find_position_marker(v);
                if (position_marker != null)
                    position_marker.selected = true;
                if (position_marker is MarkerGroup) {
                    DataViewPositionMarker? m = data_view_marker_cache.get(v);
                    if (m != null)
                        m.selected = true;
                }
            }
418
        }
419 420 421
    }

    public void clear() {
422
        data_view_marker_cache.clear();
423
        marker_layer.remove_all();
424
        marker_group_raster.clear();
425 426
    }

427
    public void add_data_view(DataView view) {
428
        DataSource view_source = view.get_source();
429
        if (!(view_source is Positionable))
430 431 432
            return;
        Positionable p = (Positionable) view_source;
        GpsCoords gps_coords = p.get_gps_coords();
433
        if (gps_coords.has_gps <= 0)
434 435
            return;
        PositionMarker position_marker = create_position_marker(view);
436
        marker_group_raster.rasterize_marker(position_marker);
437 438 439
    }

    public void show_position_markers() {
440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
        if (marker_group_raster.is_empty)
            return;

        map_view.stop_go_to();
        double lat, lon;
        var bbox = marker_layer.get_bounding_box();
        var zoom_level = map_view.get_zoom_level();
        var zoom_level_test = zoom_level < 2 ? 0 : zoom_level - 2;
        bbox.get_center(out lat, out lon);

        if (map_view.get_bounding_box_for_zoom_level(zoom_level_test).covers(lat, lon)) {
            // Don't zoom in/out if target is in proximity
            map_view.ensure_visible(bbox, true);
        } else if (zoom_level >= DEFAULT_ZOOM_LEVEL) {
            // zoom out to DEFAULT_ZOOM_LEVEL first, then move
            map_view.set_zoom_level(DEFAULT_ZOOM_LEVEL);
456
            map_view.ensure_visible(bbox, true);
457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473
        } else {
            // move first, then zoom in to DEFAULT_ZOOM_LEVEL
            map_view.go_to(lat, lon);
            // There seems to be a runtime issue with the animation_completed signal
            // sig = map_view.animation_completed["go-to"].connect((v) => { ... }
            // so we're using a timeout-based approach instead. It should be kept in sync with
            // the animation time (500ms by default.)
            if (position_markers_timeout > 0)
                Source.remove(position_markers_timeout);
            position_markers_timeout = Timeout.add(500, () => {
                map_view.center_on(lat, lon); // ensure the timeout wasn't too fast
                if (map_view.get_zoom_level() < DEFAULT_ZOOM_LEVEL)
                    map_view.set_zoom_level(DEFAULT_ZOOM_LEVEL);
                map_view.ensure_visible(bbox, true);
                position_markers_timeout = 0;
                return Source.REMOVE;
            });
474 475 476
        }
    }

477 478
    public void select_data_views(Gee.Collection<unowned DataViewPositionMarker> ms,
            SelectionAction action = SelectionAction.SET) {
479 480 481 482 483
        if (page == null)
            return;

        ViewCollection page_view = page.get_view();
        if (page_view != null) {
484
            Marker marked = page_view.start_marking();
485 486 487 488 489
            foreach (var m in ms) {
                if (m.view is CheckerboardItem) {
                    marked.mark(m.view);
                }
            }
490 491 492 493 494 495 496
            if (action == SelectionAction.REMOVE) {
                page_view.unselect_marked(marked);
            } else {
                if (action == SelectionAction.SET)
                    page_view.unselect_all();
                page_view.select_marked(marked);
            }
497 498 499
        }
    }

500
    public void highlight_data_views(Gee.Collection<unowned DataViewPositionMarker> ms) {
501 502
        if (page == null)
            return;
503

504 505
        bool did_adjust_view = false;
        foreach (var m in ms) {
Jens Georg's avatar
Jens Georg committed
506 507 508
            if (!(m.view is CheckerboardItem)) {
                continue;
            }
509

Jens Georg's avatar
Jens Georg committed
510
            CheckerboardItem item = m.view as CheckerboardItem;
511

512 513
            if (!did_adjust_view && page is CheckerboardPage) {
                (page as CheckerboardPage).scroll_to_item(item);
Jens Georg's avatar
Jens Georg committed
514
                did_adjust_view = true;
515
            }
Jens Georg's avatar
Jens Georg committed
516
            item.brighten();
517 518 519
        }
    }

520
    public void unhighlight_data_views(Gee.Collection<unowned DataViewPositionMarker> ms) {
521 522 523 524 525 526 527 528
        if (page == null)
            return;

        foreach (var m in ms) {
            if (m.view is CheckerboardItem) {
                CheckerboardItem item = (CheckerboardItem) m.view;
                item.unbrighten();
            }
529 530 531 532
        }
    }

    public void highlight_position_marker(DataView v) {
533
        var position_marker = marker_group_raster.find_position_marker(v);
534 535
        if (position_marker != null) {
            position_marker.highlighted = true;
536 537 538 539
        }
    }

    public void unhighlight_position_marker(DataView v) {
540
        var position_marker = marker_group_raster.find_position_marker(v);
541 542
        if (position_marker != null) {
            position_marker.highlighted = false;
543 544 545
        }
    }

546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569
    public void media_source_position_changed(Gee.List<MediaSource> media, GpsCoords gps_coords) {
        if (page == null)
            return;
        var view_collection = page.get_view();
        foreach (var source in media) {
            var view = view_collection.get_view_for_source(source);
            if (view == null)
                continue;
            var marker = data_view_marker_cache.get(view);
            if (marker != null) {
                if (gps_coords.has_gps > 0) {
                    // update individual marker cache
                    marker.champlain_marker.set_location(gps_coords.latitude, gps_coords.longitude);
                } else {
                    // TODO: position removal not supported by GUI
                    // remove marker from cache, map_layer
                    // remove from marker_group_raster (needs a removal method which also removes the
                    // item from the group if (marker_group_raster.find_position_marker(view) is MarkerGroup)
                }
            }
        }
        marker_group_raster.regroup();
    }

Jens Georg's avatar
Jens Georg committed
570 571 572
    private Champlain.MapSource create_map_source() {
        var map_source = new Champlain.MapSourceChain();
        var file_cache = new Champlain.FileCache.full(10 * 1024 * 1024,
Jens Georg's avatar
Jens Georg committed
573 574
            AppDirs.get_cache_dir().get_child("tiles").get_child("mapbox-outdoors").get_path(),
            new Champlain.ImageRenderer());
Jens Georg's avatar
Jens Georg committed
575 576
        var memory_cache = new Champlain.MemoryCache.full(10 * 1024 * 1024, new Champlain.ImageRenderer());
        var error_source = new Champlain.NullTileSource.full(new Champlain.ImageRenderer());
Jens Georg's avatar
Jens Georg committed
577 578 579 580 581 582 583 584 585 586 587 588 589

        var tile_source = new Champlain.NetworkTileSource.full("mapbox-outdoors",
                                                               "Mapbox outdoors tiles",
                                                               "",
                                                               "",
                                                               0,
                                                               19,
                                                               256,
                                                               Champlain.MapProjection.MERCATOR,
                                                               "https://a.tiles.mapbox.com/v4/mapbox.outdoors/#Z#/#X#/#Y#.png?access_token=" +
                                                               MAPBOX_API_TOKEN,
                                                               new Champlain.ImageRenderer());

Jens Georg's avatar
Jens Georg committed
590
        var user_agent = "Shotwell/%s libchamplain/%s".printf(_VERSION, Champlain.VERSION_S);
Jens Georg's avatar
Jens Georg committed
591 592
        tile_source.set_user_agent(user_agent);
        tile_source.max_conns = 2;
Jens Georg's avatar
Jens Georg committed
593 594

        map_source.push(error_source);
Jens Georg's avatar
Jens Georg committed
595
        map_source.push(tile_source);
Jens Georg's avatar
Jens Georg committed
596 597 598 599 600 601
        map_source.push(file_cache);
        map_source.push(memory_cache);

        return map_source;
    }

Jens Georg's avatar
Jens Georg committed
602 603 604 605 606 607 608 609 610
    private Clutter.Actor create_attribution_actor() {
        const string IMPROVE_TEXT = N_("Improve this map");
        var label = new Gtk.Label(null);
        label.set_markup("<a href=\"https://www.mapbox.com/about/maps/\">© Mapbox</a> <a href=\"https://openstreetmap.org/about/\">© OpenStreetMap</a> <a href=\"https://www.mapbox.com/map-feedback/\">%s</a>".printf(IMPROVE_TEXT));
        label.get_style_context().add_class("map-attribution");

        return new GtkClutter.Actor.with_contents(label);
    }

611 612 613
    private void setup_map() {
        map_view = gtk_champlain_widget.get_view();
        map_view.add_layer(marker_layer);
Jens Georg's avatar
Jens Georg committed
614
        map_view.set_map_source(create_map_source());
615

Jens Georg's avatar
Jens Georg committed
616 617
        var map_attribution_text = create_attribution_actor();
        map_attribution_text.content_gravity = Clutter.ContentGravity.BOTTOM_RIGHT;
Jens Georg's avatar
Jens Georg committed
618 619 620 621
        map_attribution_text.set_x_align(Clutter.ActorAlign.END);
        map_attribution_text.set_x_expand(true);
        map_attribution_text.set_y_align(Clutter.ActorAlign.END);
        map_attribution_text.set_y_expand(true);
Jens Georg's avatar
Jens Georg committed
622

623 624 625
        // add lock/unlock button to top left corner of map
        map_edit_lock_button.content_gravity = Clutter.ContentGravity.TOP_RIGHT;
        map_edit_lock_button.reactive = true;
Jens Georg's avatar
Jens Georg committed
626 627 628 629 630
        map_edit_lock_button.set_x_align(Clutter.ActorAlign.END);
        map_edit_lock_button.set_x_expand(true);
        map_edit_lock_button.set_y_align(Clutter.ActorAlign.START);
        map_edit_lock_button.set_y_expand(true);

631 632 633 634 635 636 637 638
        map_edit_lock_button.button_release_event.connect((a, e) => {
            if (e.button != 1 /* CLUTTER_BUTTON_PRIMARY */)
                return false;
            map_edit_lock = !map_edit_lock;
            map_edit_lock_button.set_content(map_edit_lock ?
                map_edit_locked_image : map_edit_unlocked_image);
            return true;
        });
Jens Georg's avatar
Jens Georg committed
639 640 641
        map_view.add_child(map_edit_lock_button);
        map_view.add_child(map_attribution_text);

642 643 644 645 646 647 648 649 650 651 652 653 654 655 656
        gtk_champlain_widget.has_tooltip = true;
        gtk_champlain_widget.query_tooltip.connect((x, y, keyboard_tooltip, tooltip) => {
            Gdk.Rectangle lock_rect = {
                (int) map_edit_lock_button.x,
                (int) map_edit_lock_button.y,
                (int) map_edit_lock_button.width,
                (int) map_edit_lock_button.height,
            };
            Gdk.Rectangle mouse_pos = { x, y, 1, 1 };
            if (!lock_rect.intersect(mouse_pos, null))
                return false;
            tooltip.set_text(_("Lock or unlock map for geotagging by dragging pictures onto the map"));
            return true;
        });

657 658 659
        // add scale to bottom left corner of the map
        map_scale.content_gravity = Clutter.ContentGravity.BOTTOM_LEFT;
        map_scale.connect_view(map_view);
Jens Georg's avatar
Jens Georg committed
660 661 662 663 664
        map_scale.set_x_align(Clutter.ActorAlign.START);
        map_scale.set_x_expand(true);
        map_scale.set_y_align(Clutter.ActorAlign.END);
        map_scale.set_y_expand(true);
        map_view.add_child(map_scale);
665 666

        map_view.set_zoom_on_double_click(false);
667 668 669 670
        map_view.notify.connect((o, p) => {
            if (p.name == "zoom-level")
                zoom_changed();
        });
671 672 673 674 675 676 677 678 679 680

        Gtk.TargetEntry[] dnd_targets = {
            LibraryWindow.DND_TARGET_ENTRIES[LibraryWindow.TargetType.URI_LIST],
            LibraryWindow.DND_TARGET_ENTRIES[LibraryWindow.TargetType.MEDIA_LIST]
        };
        Gtk.drag_dest_set(this, Gtk.DestDefaults.ALL, dnd_targets,
            Gdk.DragAction.COPY | Gdk.DragAction.LINK | Gdk.DragAction.ASK);
        button_press_event.connect(map_zoom_handler);
        set_size_request(200, 200);

681 682
        marker_group_raster = new MarkerGroupRaster(this, map_view, marker_layer);

683 684
        // Load icons
        float w, h;
685 686 687
        marker_image_set = new MarkerImageSet();
        marker_group_image_set = new MarkerImageSet();
        marker_image_set.marker_image = Resources.get_icon_as_clutter_image(
688
                Resources.ICON_GPS_MARKER, out w, out h);
689 690 691
        marker_image_set.marker_image_width = w;
        marker_image_set.marker_image_height = h;
        marker_image_set.marker_selected_image = Resources.get_icon_as_clutter_image(
692
                Resources.ICON_GPS_MARKER_SELECTED, out w, out h);
693 694 695 696
        marker_image_set.marker_highlighted_image = Resources.get_icon_as_clutter_image(
                Resources.ICON_GPS_MARKER_HIGHLIGHTED, out w, out h);

        marker_group_image_set.marker_image = Resources.get_icon_as_clutter_image(
697
                Resources.ICON_GPS_GROUP_MARKER, out w, out h);
698 699 700
        marker_group_image_set.marker_image_width = w;
        marker_group_image_set.marker_image_height = h;
        marker_group_image_set.marker_selected_image = Resources.get_icon_as_clutter_image(
701
                Resources.ICON_GPS_GROUP_MARKER_SELECTED, out w, out h);
702 703 704
        marker_group_image_set.marker_highlighted_image = Resources.get_icon_as_clutter_image(
                Resources.ICON_GPS_GROUP_MARKER_HIGHLIGHTED, out w, out h);

705 706 707 708 709 710 711 712 713 714 715 716
        map_edit_locked_image = Resources.get_icon_as_clutter_image(
                Resources.ICON_MAP_EDIT_LOCKED, out w, out h);
        map_edit_unlocked_image = Resources.get_icon_as_clutter_image(
                Resources.ICON_MAP_EDIT_UNLOCKED, out w, out h);
        map_edit_lock_image_width = w;
        map_edit_lock_image_height = h;
        if (map_edit_locked_image == null) {
            warning("Couldn't load map edit lock image");
        } else {
            map_edit_lock_button.set_content(map_edit_locked_image);
            map_edit_lock_button.set_size(map_edit_lock_image_width, map_edit_lock_image_height);
            map_edit_lock = true;
717 718 719
        }
    }

720
    private Champlain.Marker create_champlain_marker(GpsCoords gps_coords) {
721
        assert(gps_coords.has_gps > 0);
722

723
        Champlain.Marker champlain_marker;
724
        champlain_marker = new Champlain.Marker();
725 726
        champlain_marker.set_pivot_point(0.5f, 0.5f); // set center of marker
        champlain_marker.set_location(gps_coords.latitude, gps_coords.longitude);
727
        return champlain_marker;
728 729
    }

730
    private DataViewPositionMarker create_position_marker(DataView view) {
731 732 733
        var position_marker = data_view_marker_cache.get(view);
        if (position_marker != null)
            return position_marker;
734 735 736
        DataSource data_source = view.get_source();
        Positionable p = (Positionable) data_source;
        GpsCoords gps_coords = p.get_gps_coords();
737 738
        Champlain.Marker champlain_marker = create_champlain_marker(gps_coords);
        position_marker = new DataViewPositionMarker(view, champlain_marker, marker_image_set);
739
        position_marker.bind_mouse_events(this);
740
        data_view_marker_cache.set(view, position_marker);
741
        return (owned) position_marker;
742 743 744
    }

    internal MarkerGroup create_marker_group(GpsCoords gps_coords) {
745 746
        Champlain.Marker champlain_marker = create_champlain_marker(gps_coords);
        var g = new MarkerGroup(champlain_marker, marker_group_image_set);
747 748
        g.bind_mouse_events(this);
        return (owned) g;
749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768
    }

    private bool map_zoom_handler(Gdk.EventButton event) {
        if (event.type == Gdk.EventType.2BUTTON_PRESS) {
            if (event.button == 1 || event.button == 3) {
                double lat = map_view.y_to_latitude(event.y);
                double lon = map_view.x_to_longitude(event.x);
                if (event.button == 1) {
                    map_view.zoom_in();
                } else {
                    map_view.zoom_out();
                }
                map_view.center_on(lat, lon);
                return true;
            }
        }
        return false;
    }

    private bool internal_drop_received(Gee.List<MediaSource> media, double lat, double lon) {
769
        if (map_edit_lock)
770
            return false;
771

772
        bool success = false;
773 774 775 776 777
        GpsCoords gps_coords = GpsCoords() {
            has_gps = 1,
            latitude = lat,
            longitude = lon
        };
778 779
        foreach (var m in media) {
            Positionable p = m as Positionable;
780 781 782 783 784
            if (p != null) {
                p.set_gps_coords(gps_coords);
                success = true;
            }
        }
785
        media_source_position_changed(media, gps_coords);
786 787 788
        return success;
    }
}