Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions data/Application.css
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,9 @@ search-result-item.land-feature image {
background-color: @LIME_500;
-gtk-icon-shadow: 0 1px 3px @LIME_700;
}

/* Avoid styling the button frame we use for custom locations */
map-marker menubutton button.toggle {
background: none;
box-shadow: none;
}
2 changes: 2 additions & 0 deletions po/POTFILES
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
src/Application.vala
src/MainWindow.vala
src/MapWidget.vala
src/Objects/MarkerData.vala
src/SearchResultItem.vala
src/Widgets/Marker.vala
58 changes: 58 additions & 0 deletions src/MainWindow.vala
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,45 @@ public class Maps.MainWindow : Adw.ApplicationWindow {

private const ActionEntry[] ACTION_ENTRIES = {
{ "search", on_search_activate },
{ "pin", on_pin_activate },
};
private int current_busy_reason = 0;
private ListStore location_store;
private Cancellable? search_cancellable = null;

private Gtk.Button current_location;
private Gtk.ToggleButton pin_location;
private Gtk.Spinner spinner;
private Gtk.SearchEntry search_entry;
private Gtk.Popover search_res_popover;
private MapWidget map_widget;

private Gtk.GestureClick gesture_click;
private Gtk.EventControllerKey key_controller;

construct {
title = _("Maps");
add_action_entries (ACTION_ENTRIES, this);

gesture_click = new Gtk.GestureClick ();
key_controller = new Gtk.EventControllerKey ();
((Gtk.Widget)this).add_controller (gesture_click);
((Gtk.Widget)this).add_controller (key_controller);

location_store = new ListStore (typeof (Geocode.Place));

current_location = new Gtk.Button.from_icon_name ("find-location-symbolic") {
tooltip_text = _("Move to the current location"),
valign = CENTER
};

pin_location = new Gtk.ToggleButton () {
tooltip_text = _("Pin a location"),
icon_name = "view-pin-symbolic",
action_name = "win.pin",
valign = CENTER
};

spinner = new Gtk.Spinner ();

search_entry = new Gtk.SearchEntry () {
Expand Down Expand Up @@ -160,6 +177,7 @@ public class Maps.MainWindow : Adw.ApplicationWindow {
title_widget = search_clamp
};
headerbar.pack_start (current_location);
headerbar.pack_start (pin_location);
headerbar.pack_end (menu_button);
headerbar.pack_end (spinner);

Expand Down Expand Up @@ -365,4 +383,44 @@ public class Maps.MainWindow : Adw.ApplicationWindow {
var search_result_item = (SearchResultItem) list_item.child;
search_result_item.place = (Geocode.Place) list_item.item;
}

// We handle a pin mode at the level of the window, so we can disable the pin button,
// or cancel if the user press escape, or clicks anywhere but the map
private void on_pin_activate () {
pin_location.active = true;

set_cursor_from_name ("crosshair");
map_widget.set_cursor_from_name ("crosshair");

gesture_click.released.connect (on_click);
key_controller.key_released.connect (on_keypress);
map_widget.leaving_pin_mode.connect (on_exit_pin_mode);

map_widget.enter_pin_mode ();
}

private void on_exit_pin_mode () {
set_cursor_from_name ("default");
pin_location.active = false;

gesture_click.released.disconnect (on_click);
key_controller.key_released.disconnect (on_keypress);
map_widget.leaving_pin_mode.disconnect (on_exit_pin_mode);
}

// If this handler fires, then the user did not click on the map
private void on_click (int n_press, double x, double y) {
debug ("%.2f, %.2f", x, y);
map_widget.on_exit_pin_mode ();
on_exit_pin_mode ();
return;
}

// We only exit on Escape, as people may need to use other keys to navigate on the map from the keyboard
private void on_keypress (uint keyval, uint keycode, Gdk.ModifierType state) {
if (keyval == Gdk.Key.Escape) {
map_widget.on_exit_pin_mode ();
on_exit_pin_mode ();
}
}
}
38 changes: 37 additions & 1 deletion src/MapWidget.vala
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@ public class Maps.MapWidget : Gtk.Box {
private Shumate.MarkerLayer pin_layer;
// Displays the position of current location
private Shumate.MarkerLayer location_layer;

// Displays the position of user-pinned layers
private Shumate.MarkerLayer user_layer;
private GClue.Location? location = null;
private Gtk.GestureClick gesture_click;

private MarkerManager marker_manager;

private bool is_watching_location = false;

public signal void leaving_pin_mode ();

// The Royal Observatory
private const double DEFAULT_LATITUDE = 51.2840;
private const double DEFAULT_LONGITUDE = 0.0005;
Expand All @@ -40,6 +46,9 @@ public class Maps.MapWidget : Gtk.Box {
hexpand = true
};

gesture_click = new Gtk.GestureClick ();
map_widget.add_controller (gesture_click);

append (map_widget);
base_map = map_widget.map;
}
Expand All @@ -50,6 +59,12 @@ public class Maps.MapWidget : Gtk.Box {

location_layer = new Shumate.MarkerLayer (map_widget.viewport);
map_widget.add_overlay_layer (location_layer);

user_layer = new Shumate.MarkerLayer (map_widget.viewport);
map_widget.add_overlay_layer (user_layer);

marker_manager = new MarkerManager (user_layer);
marker_manager.populate_layer.begin ();
}

// Set the initial location of the map widget.
Expand Down Expand Up @@ -191,4 +206,25 @@ public class Maps.MapWidget : Gtk.Box {

pin_layer.add_marker (marker);
}

public void enter_pin_mode () {
map_widget.map.set_cursor_from_name ("crosshair");
gesture_click.released.connect (on_click);
}

public void on_exit_pin_mode () {
map_widget.map.set_cursor_from_name ("default");
gesture_click.released.disconnect (on_click);
leaving_pin_mode ();
}

private void on_click (int n_press, double x, double y) {
on_exit_pin_mode ();

double latitude, longitude;
map_widget.viewport.widget_coords_to_location (map_widget, x, y, out latitude, out longitude);
marker_manager.add_marker (new MarkerData (latitude, longitude));

return;
}
}
52 changes: 52 additions & 0 deletions src/Objects/MarkerData.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io)
* 2018-2025 Ryo Nakano <ryonakaknock3@gmail.com>
* 2014-2015 Atlas Developers
*/

/**
* MarkerData represents the informations for a specific location
*
* It is used by {@link Maps.MarkerManager} to save, load, and represent locations on a map.
*
* It can be converted from or into a {@link Json.Object} for convenience
*/
public class Maps.MarkerData : Object {

public double latitude {get; set;}
public double longitude {get; set;}
public string name {get; set; default = _("New location");}
public string comments {get; set; default = "";}

public MarkerData (double latitude, double longitude) {
Object (
latitude: latitude,
longitude: longitude
);
}

public MarkerData.from_json (Json.Object node) {
latitude = node.get_double_member ("lat");
longitude = node.get_double_member ("long");
name = node.get_string_member ("name");
comments = node.get_string_member ("comments");
}

public Json.Object to_json () {
var builder = new Json.Builder ();

builder.begin_object ();
builder.set_member_name ("lat");
builder.add_double_value (latitude);
builder.set_member_name ("long");
builder.add_double_value (longitude);
builder.set_member_name ("name");
builder.add_string_value (name);
builder.set_member_name ("comments");
builder.add_string_value (comments);
builder.end_object ();

return builder.get_root ().get_object ();
}
}
93 changes: 93 additions & 0 deletions src/Services/MarkerManager.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io)
* 2018-2025 Ryo Nakano <ryonakaknock3@gmail.com>
* 2014-2015 Atlas Developers
*/

/**
* MarkerManager manages locations on a {@link Shumate.MarkerLayer}
*
* It loads {@link Maps.MarkerData} from {@link Maps.Storage}, from which it creates {@link Maps.Marker} widgets and adds them to the layer
*
*/
public class Maps.MarkerManager : Object {

private static uint debounce_timer_id;

public signal void pin_created (Marker marker);

private Storage storage;
private unowned Shumate.MarkerLayer markerlayer;

public MarkerManager (Shumate.MarkerLayer markerlayer) {
this.markerlayer = markerlayer;
}

construct {
storage = new Storage ();
}

public async void populate_layer () {
var markerdata_array = storage.load ();

foreach (var markerdata_note in markerdata_array.get_elements ()) {
var markerdata = new MarkerData.from_json (markerdata_note.get_object ());
add_marker (markerdata);
}

// Only care about new pins after loading everything
pin_created.connect (on_pin_created);
}

public void add_marker (MarkerData markerdata) {
var marker = new Marker (markerdata);
markerlayer.add_marker (marker);

marker.changed.connect (save_debounce);
marker.deletion_requested.connect (remove_marker);

pin_created (marker);
}

public void remove_marker (Shumate.Marker marker) {
markerlayer.remove_marker (marker);
save_debounce ();
}

public void save_debounce () {
if (debounce_timer_id != 0) {
GLib.Source.remove (debounce_timer_id);
}

debounce_timer_id = Timeout.add (750, debounce_handler);
}

public bool debounce_handler () {
debounce_timer_id = 0;
immediately_save ();
return GLib.Source.REMOVE;
}

public void immediately_save () {
debug ("Saving");
var json_array = new Json.Array ();

foreach (var marker in markerlayer.get_markers ()) {
var markerdata = ((Maps.Marker)marker).to_markerdata ();
json_array.add_object_element (markerdata.to_json ());
}

storage.save (json_array);
}

private void on_pin_created (Marker marker) {
marker.popup ();
marker.grab_entry ();
save_debounce ();
}

~MarkerManager () {
immediately_save ();
}
}
Loading
Loading