Skip to content

Commit

Permalink
userLocationMarker: Redesign location marker
Browse files Browse the repository at this point in the history
Use the accent color to draw the user location
marker, drawn in code rather than using a static
icon.
Also use the accent color to draw the accuracy circle
and implement the "torch" widget indicating heading
as per the mockups.
  • Loading branch information
mlundblad committed Jan 14, 2025
1 parent 1505fe3 commit 135b31f
Showing 1 changed file with 196 additions and 41 deletions.
237 changes: 196 additions & 41 deletions src/userLocationMarker.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,21 @@
* Author: Damián Nohales <[email protected]>
*/

import Cairo from 'cairo';
import Adw from 'gi://Adw';
import Gdk from 'gi://Gdk';
import GObject from 'gi://GObject';
import Graphene from 'gi://Graphene';
import Gsk from 'gi://Gsk';
import Shumate from 'gi://Shumate';

import {IconMarker} from './iconMarker.js';
import {MapMarker} from './mapMarker.js';

const LOCATION_MARKER_SIZE = 32;
const LOCATION_MARKER_MARGIN = 4;
const LOCATION_MARKER_SHADOW_RADIUS = 4;
const WHITE = new Gdk.RGBA({ red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0 });
const SHADOW_COLOR = new Gdk.RGBA({ red: 0.0, green: 0.0, blue: 0.0, alpha: 0.05 });

export class AccuracyCircleMarker extends Shumate.Marker {

constructor({place, ...params}) {
Expand All @@ -39,6 +46,23 @@ export class AccuracyCircleMarker extends Shumate.Marker {
});

this._place = place;
this._styleManager = Adw.StyleManager.get_default();
this._pathBuilder = new Gsk.PathBuilder();
}

vfunc_map() {
this._accentId = this._styleManager.connect('notify::accent-color-rgba', () => {
this.queue_draw();
});
this.queue_draw();

super.vfunc_map();
}

vfunc_unmap() {
this._styleManager.disconnect(this._accentId);

super.vfunc_unmap();
}

refreshGeometry(mapView) {
Expand All @@ -63,31 +87,130 @@ export class AccuracyCircleMarker extends Shumate.Marker {
}

vfunc_snapshot(snapshot) {
let {x, y, width, height} = this.get_allocation();
let rect = new Graphene.Rect();
const width = this.get_width();
const height = this.get_height();
const accentColor = this._styleManager.accent_color_rgba;
const center = new Graphene.Point({ x: width / 2, y: height / 2 });

this._pathBuilder.add_circle(center, width / 2);
snapshot.append_fill(this._pathBuilder.to_path(),
Gsk.FILL_RULE_EVEN_ODD,
new Gdk.RGBA({ red: accentColor.red,
green: accentColor.green,
blue: accentColor.blue,
alpha: 0.05 }));

this._pathBuilder.add_circle(center, width / 2);
snapshot.append_stroke(this._pathBuilder.to_path(),
new Gsk.Stroke(1),
new Gdk.RGBA({ red: accentColor.red,
green: accentColor.green,
blue: accentColor.blue,
alpha: 0.15 }));

super.vfunc_snapshot(snapshot);
}
}

GObject.registerClass(AccuracyCircleMarker);

class HeadingTorch extends Shumate.Marker {
constructor({place, mapView, ...params}) {
super({
...params,
latitude: place.location.latitude,
longitude: place.location.longitude,
can_focus: false,
can_target: false
});

this._place = place;
this._mapView = mapView;
this._styleManager = Adw.StyleManager.get_default();
this._pathBuilder = new Gsk.PathBuilder();
this._place.connect('notify::location', this.refresh.bind(this));
this._mapView.map.viewport.connect('notify::rotation',
this.refresh.bind(this));
this.set_size_request(LOCATION_MARKER_SIZE * 2.5,
LOCATION_MARKER_SIZE * 2.5);
this.refresh();
}

refresh() {
if (this._place.location.heading === -1) {
this.visible = false;
} else {
this.latitude = this._place.location.latitude;
this.longitude = this._place.location.longitude;
this.visible = true;
this.queue_draw();
}
}

rect.init(0, 0, width, height);
vfunc_map() {
this._accentId = this._styleManager.connect('notify::accent-color-rgba', () => {
this.queue_draw();
});
this.queue_draw();

let cr = snapshot.append_cairo(rect);
super.vfunc_map();
}

cr.setOperator(Cairo.Operator.OVER);
vfunc_unmap() {
this._styleManager.disconnect(this._accentId);

super.vfunc_unmap();
}

vfunc_snapshot(snapshot) {
const accentColor = this._styleManager.accent_color_rgba;
const r = this.get_width() / 2;
const angle = this._place.location.heading +
this._mapView.map.viewport.rotation * 180 / Math.PI
const center = new Graphene.Point({ x: r, y: r });
const bounds = new Graphene.Rect();
const gradientStart =
new Gsk.ColorStop({ offset: 0,
color: new Gdk.RGBA({ red: accentColor.red,
green: accentColor.green,
blue: accentColor.blue,
alpha: 0.5 }) });
const gradientEnd =
new Gsk.ColorStop({ offset: 1,
color: new Gdk.RGBA({ red: accentColor.red,
green: accentColor.green,
blue: accentColor.blue,
alpha: 0.0 }) });

cr.setSourceRGBA(0, 0, 255, 0.1);
cr.arc(width / 2, width / 2, width / 2, 0, Math.PI * 2);
cr.fillPreserve();
bounds.init(-r, -r, 2 * r, 2 * r);

this._pathBuilder.line_to(-r * Math.sqrt(2) / 2,
-r * Math.sqrt(2) / 2);
this._pathBuilder.arc_to(0, -r * Math.sqrt(2),
r * Math.sqrt(2) / 2,
-r * Math.sqrt(2) / 2);
this._pathBuilder.line_to(0, 0);
snapshot.translate(center);
snapshot.rotate(angle);
snapshot.push_fill(this._pathBuilder.to_path(), Gsk.FILL_RULE_EVEN_ODD);
snapshot.append_radial_gradient(bounds, new Graphene.Point(), r, r, 0.4, 1,
[gradientStart, gradientEnd]);
snapshot.pop();

super.vfunc_snapshot(snapshot);
}
}

GObject.registerClass(AccuracyCircleMarker);
GObject.registerClass(HeadingTorch);

export class UserLocationMarker extends IconMarker {
export class UserLocationMarker extends MapMarker {

constructor(params) {
super(params);

this._accuracyMarker = new AccuracyCircleMarker({ place: this.place });
this._headingTorch = new HeadingTorch({ place: this.place,
mapView: this._mapView });
this.connect('notify::view-zoom-level',
() => this._accuracyMarker.refreshGeometry(this._mapView));
this._mapView.connect('notify::default-width',
Expand All @@ -97,12 +220,27 @@ export class UserLocationMarker extends IconMarker {
this._accuracyMarker.refreshGeometry(this._mapView);

this.place.connect('notify::location', () => this._updateLocation());
this._image.pixel_size = 24;
this._updateLocation();

this.connect('notify::visible', this._updateAccuracyCircle.bind(this));
this._mapView.map.viewport.connect('notify::rotation',
() => this._updateLocation());
this.connect('notify::visible', this._updateLocation.bind(this));

this._styleManager = Adw.StyleManager.get_default();
this.set_size_request(LOCATION_MARKER_SIZE, LOCATION_MARKER_SIZE);
this._pathBuilder = new Gsk.PathBuilder();
}

vfunc_map() {
this._accentId = this._styleManager.connect('notify::accent-color-rgba', () => {
this.queue_draw();
});
this.queue_draw();

super.vfunc_map();
}

vfunc_unmap() {
this._styleManager.disconnect(this._accentId);

super.vfunc_unmap();
}

_hasBubble() {
Expand All @@ -111,18 +249,21 @@ export class UserLocationMarker extends IconMarker {

addToLayer(layer) {
layer.add_marker(this._accuracyMarker);
layer.add_marker(this._headingTorch);
layer.add_marker(this);
}

_updateLocation() {
if (this.place.location.heading > -1) {
this._image.icon_name = 'user-location-compass'
this.queue_draw();
this._updateAccuracyCircle();
this._updateHeadingTorch();
}

_updateHeadingTorch() {
if (this.visible) {
this._headingTorch.refresh();
} else {
this._image.icon_name = 'user-location';
this._headingTorch.visible = false;
}

this._updateAccuracyCircle();
}

_updateAccuracyCircle() {
Expand All @@ -134,26 +275,40 @@ export class UserLocationMarker extends IconMarker {
}

vfunc_snapshot(snapshot) {
snapshot.save();

if (this.place.location.heading > -1) {
// rotate around the center of the icon
let width = this.get_width();
let height = this.get_height();
let point = new Graphene.Point();
let rotation = this.place.location.heading +
this._mapView.map.viewport.rotation * 180 / Math.PI;

point.init(width / 2, height / 2);
snapshot.translate(point);
snapshot.rotate(rotation);
point.init(-width / 2, -height / 2);
snapshot.translate(point);
}
const accentColor = this._styleManager.accent_color_rgba;
const center = new Graphene.Point({ x: LOCATION_MARKER_SIZE / 2,
y: LOCATION_MARKER_SIZE / 2 });
const shadowBounds = new Graphene.Rect();

shadowBounds.init(-LOCATION_MARKER_SHADOW_RADIUS,
-LOCATION_MARKER_SHADOW_RADIUS,
LOCATION_MARKER_SIZE + 2 * LOCATION_MARKER_SHADOW_RADIUS,
LOCATION_MARKER_SIZE + 2 * LOCATION_MARKER_SHADOW_RADIUS);

// draw shadow
shadowBounds.init(0, 0, LOCATION_MARKER_SIZE, LOCATION_MARKER_SIZE);
const shadowOutline = new Gsk.RoundedRect();
const corner = new Graphene.Size({ width: LOCATION_MARKER_SIZE / 2,
height: LOCATION_MARKER_SIZE / 2 });

shadowOutline.init(shadowBounds, corner, corner, corner, corner);

snapshot.append_outset_shadow(shadowOutline, SHADOW_COLOR, 0, 0,
LOCATION_MARKER_SHADOW_RADIUS,
LOCATION_MARKER_SHADOW_RADIUS);

// draw outer white circle
this._pathBuilder.add_circle(center, LOCATION_MARKER_SIZE / 2);
snapshot.append_fill(this._pathBuilder.to_path(),
Gsk.FILL_RULE_EVEN_ODD, WHITE);

// draw inner accent-colored circle
this._pathBuilder.add_circle(center, LOCATION_MARKER_SIZE / 2 -
LOCATION_MARKER_MARGIN);
snapshot.append_fill(this._pathBuilder.to_path(),
Gsk.FILL_RULE_EVEN_ODD, accentColor);

this.snapshot_child(this._image, snapshot);
super.vfunc_snapshot(snapshot);
snapshot.restore();
}
}

Expand Down

0 comments on commit 135b31f

Please sign in to comment.