Skip to content

Commit

Permalink
Use advanced markers for tracks
Browse files Browse the repository at this point in the history
  • Loading branch information
vicb committed Mar 19, 2024
1 parent 420432e commit ba2c56a
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 93 deletions.
86 changes: 86 additions & 0 deletions apps/fxc-front/src/app/components/2d/adv-marker-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';

// Wrapper component for Advanced Markers.
@customElement('adv-marker-element')
export class AdvancedMarkerElement extends LitElement {
@property({ attribute: false })
set lat(value: number) {
this.lat_ = value;
this.requestUpdate();
}

get lat(): number {
return this.lat_;
}

@property({ attribute: false })
set lng(value: number) {
this.lng_ = value;
this.requestUpdate();
}

get lng(): number {
return this.lng_;
}

@property({ attribute: false })
set title(value: string) {
this.marker_.title = value;
}

get title(): string {
return this.marker_.title;
}

@property({ attribute: false })
set zindex(value: number) {
this.marker_.zIndex = value;
}

get zindex(): number {
return this.marker_.zIndex ?? 0;
}

@property({ attribute: false })
set map(value: google.maps.Map | null | undefined) {
this.marker_.map = value ?? null;
}

get map() {
return this.marker_.map;
}

@property({ attribute: false })
set content(value: Node | null | undefined) {
this.marker_.content = value;
}

get content() {
return this.marker_.content;
}

private lat_ = 0;
private lng_ = 0;
private marker_ = new google.maps.marker.AdvancedMarkerElement();

connectedCallback(): void {
super.connectedCallback();
this.marker_.addListener('click', () => this.dispatchEvent(new CustomEvent('click')));
}

disconnectedCallback() {
super.disconnectedCallback();
google.maps.event.clearInstanceListeners(this.marker_);
// setTimeout is needed otherwise the marker isn't removed.
setTimeout(() => (this.marker_.map = null), 0);
}

render() {
this.marker_.position = { lat: this.lat_, lng: this.lng_ };
}

protected createRenderRoot(): HTMLElement | DocumentFragment {
return this;
}
}
45 changes: 23 additions & 22 deletions apps/fxc-front/src/app/components/2d/map-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function loadGMaps(): Promise<void> {
new Loader({
apiKey: getApiKey('gmaps', store.getState().track.domain),
version: 'weekly',
libraries: ['geometry'],
libraries: ['geometry', 'marker'],
})
.load()
.then(resolve);
Expand Down Expand Up @@ -234,8 +234,7 @@ export class MapElement extends connect(store)(LitElement) {
}

protected render(): TemplateResult {
return html`
<style>
return html` <style>
#drw-container {
width: 100%;
height: 100%;
Expand All @@ -244,17 +243,17 @@ export class MapElement extends connect(store)(LitElement) {
left: 0px;
z-index: 1;
}
path {
#drw-container path {
stroke-width: 1px;
stroke: black;
fill: none;
}
svg {
#drw-container svg {
width: 100%;
height: 100%;
touch-action: none;
}
rect {
#drw-container rect {
width: 100%;
height: 100%;
opacity: 0.1;
Expand All @@ -263,7 +262,7 @@ export class MapElement extends connect(store)(LitElement) {
}
</style>
<div id="drw-container" style=${`display:${this.isFreeDrawing ? 'block' : 'none'}`}>
<svg width="100%" height="100%">
<svg>
${when(this.pathPoints.length > 1, () => svg`<path d=${this.freeDrawPath}></path>`)}
<rect
@pointerdown=${this.svgPointerDown}
Expand All @@ -275,21 +274,23 @@ export class MapElement extends connect(store)(LitElement) {
</svg>
</div>
<div id="map"></div>
<topo-eu .map=${this.map}></topo-eu>
<topo-spain .map=${this.map}></topo-spain>
<topo-france .map=${this.map}></topo-france>
<topo-otm .map=${this.map}></topo-otm>
<segments-element .map=${this.map} .query=${document.location.search}></segments-element>
${repeat(
this.tracks,
(track) => track.id,
(track) =>
html`
<marker-element .map=${this.map} .track=${track} .timeSec=${this.timeSec}></marker-element>
<line-element .map=${this.map} .track=${track}></line-element>
`,
)}
`;
${when(
this.map,
() => html` <topo-eu .map=${this.map}></topo-eu>
<topo-spain .map=${this.map}></topo-spain>
<topo-france .map=${this.map}></topo-france>
<topo-otm .map=${this.map}></topo-otm>
<segments-element .map=${this.map} .query=${document.location.search}></segments-element>
${repeat(
this.tracks,
(track) => track.id,
(track) =>
html`
<marker-element .map=${this.map} .track=${track} .timeSec=${this.timeSec}></marker-element>
<line-element .map=${this.map} .track=${track}></line-element>
`,
)}`,
)}`;
}

private svgPointerDown(e: PointerEvent) {
Expand Down
134 changes: 63 additions & 71 deletions apps/fxc-front/src/app/components/2d/marker-element.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import * as common from '@flyxc/common';
import { LitElement, PropertyValues } from 'lit';
import { LitElement, PropertyValues, html, nothing } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { connect } from 'pwa-helpers';
import * as units from '../../logic/units';
import * as sel from '../../redux/selectors';
import { RootState, store } from '../../redux/store';
import { setCurrentTrackId } from '../../redux/track-slice';

import './adv-marker-element';

const INACTIVE_OPACITY = 0.5;

@customElement('marker-element')
export class MarkerElement extends connect(store)(LitElement) {
@property({ attribute: false })
map!: google.maps.Map;

@property()
@property({ attribute: false })
timeSec = 0;
@property({ attribute: false })
track?: common.RuntimeTrack;
Expand All @@ -26,98 +28,88 @@ export class MarkerElement extends connect(store)(LitElement) {
@state()
private displayLabels = true;
@state()
private color = '';
private color = 'white';

private offsetSeconds = 0;
private marker?: google.maps.Marker;
private icon?: google.maps.Symbol;
private markerContent: HTMLDivElement;
private svg: SVGElement;
private path: SVGPathElement;
private label: HTMLParagraphElement;

constructor() {
super();
this.markerContent = document.createElement('div');
this.markerContent.className = 'fxc-marker';
this.markerContent.innerHTML =
'<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"><path fill="orange" stroke="#000" d="M134.5-168.8A121.8 121.8 0 0 0 99-149.6C86.9-86 55.2-64 30.5-57a5.4 5.4 0 0 1-6.8-4c-.2-.8-.2-1 .2-25.4L11.6-78c-1 .7-2.3 1-3.5 1H-7.3c-1.2 0-2.5-.3-3.5-1l-12.3-8.4c.4 24.4.3 24.6.2 25.3a5.4 5.4 0 0 1-6.8 4.1c-24.6-7-56.3-29-68.4-92.5-3.6-3-15.4-11.9-35.6-19.2-21-7.5-55.2-14.6-102.7-8.2a118.3 118.3 0 0 1 41 93.2c11.9.2 23.7 2.4 34.8 6.6 18 7 40.5 22.6 50.3 57.3 10.9-8.7 27.8-12.6 53.8-12.6A60.8 60.8 0 0 1-8.5-10 71.9 71.9 0 0 1 .3 3.5 73 73 0 0 1 9.2-10a60.8 60.8 0 0 1 48-22.5c26 0 43 4 54 12.7A82.2 82.2 0 0 1 161.5-77a101 101 0 0 1 34.8-6.6 118.3 118.3 0 0 1 41-93.2c-47.5-6.4-81.8.7-102.7 8.2z"/></svg><p class="max-content" style="transform: translate(-50%, 5px)"></p>';
this.svg = this.markerContent.querySelector('svg')!;
this.path = this.svg.querySelector('path')!;
this.label = this.markerContent.querySelector('p')!;
}

stateChanged(state: RootState): void {
if (this.track) {
const id = this.track.id;
this.offsetSeconds = sel.offsetSeconds(state)[id];
this.offsetSeconds = sel.offsetSeconds(state)[id] ?? 0;
this.color = sel.trackColors(state)[id];
this.active = sel.currentTrackId(state) == id;
}
this.units = state.units;
this.displayLabels = state.track.displayLabels;
}

disconnectedCallback(): void {
super.disconnectedCallback();
this.destroyMarker();
}

private destroyMarker(): void {
this.marker?.setMap(null);
this.marker = undefined;
this.icon = undefined;
}

shouldUpdate(changedProps: PropertyValues): boolean {
if (this.map == null) {
this.destroyMarker();
return false;
if (changedProps.has('color')) {
this.path.setAttribute('fill', this.color);
changedProps.delete('color');
}
if (changedProps.has('map') || changedProps.has('track')) {
this.destroyMarker();
this.maybeCreateMarker();
}
if (this.marker && this.icon && this.track) {
const timeSec = this.timeSec + this.offsetSeconds;
const { lat, lon, alt } = sel.getTrackLatLonAlt(store.getState())(timeSec, this.track) as common.LatLonAlt;
const scale = (50 * ((alt ?? 0) - this.track.minAlt)) / (this.track.maxAlt - this.track.minAlt) + 20;
this.marker.setPosition({ lat, lng: lon });
const icon = this.icon as google.maps.Symbol;
icon.fillColor = this.color;
icon.fillOpacity = this.active ? 1 : INACTIVE_OPACITY;
icon.strokeColor = this.active ? '#000' : '#555';
icon.scale = scale / 512;
this.marker.setIcon(icon);
// Display higher markers on top.
this.marker.setZIndex(Math.floor(alt ?? 0));
if (this.displayLabels) {
const altitude = `${units.formatUnit(alt ?? 0, this.units.altitude)}`;
this.marker.setLabel({
color: this.active ? '#000' : '#555',
fontWeight: '500',
className: 'gm-label-outline',
text: (this.track.name == 'unknown' ? '' : `${this.track.name} · `) + altitude,
} as any);
} else {
this.marker.setLabel('');
}
if (changedProps.has('active')) {
this.path.setAttribute('opacity', `${this.active ? 1 : INACTIVE_OPACITY}`);
this.path.setAttribute('stroke-color', `${this.active ? '#000' : '#555'}`);
changedProps.delete('active');
}

return false;
return super.shouldUpdate(changedProps);
}

// Creates the marker when there is a track.
private maybeCreateMarker() {
if (this.track == null) {
return;
render() {
if (!this.track) {
return nothing;
}

const timeSec = this.timeSec + this.offsetSeconds;
const { lat, lon, alt } = sel.getTrackLatLonAlt(store.getState())(timeSec, this.track) as common.LatLonAlt;
const altAboveMin = (alt ?? 0) - this.track.minAlt;
const altDelta = this.track.maxAlt - this.track.minAlt;
const scale = (50 * altAboveMin) / altDelta + 20;
// todo
this.path.setAttribute('transform', `scale(${scale / 512})`);
this.path.setAttribute('stroke-width', `${2 / (scale / 512)}`);

let label = '';
if (this.displayLabels) {
const altitude = `${units.formatUnit(alt ?? 0, this.units.altitude)}`;
label = (this.track.name == 'unknown' ? '' : `${this.track.name} · `) + altitude;
}

this.marker = new google.maps.Marker({
map: this.map,
clickable: true,
title: this.track.name,
});

google.maps.event.addListener(this.marker, 'click', () => {
store.dispatch(setCurrentTrackId(this.track?.id));
});

this.icon = {
path: 'M390 169.3c-20.1 7.3-31.9 16.2-35.5 19.2C342.4 252 310.7 274 286 281a5.4 5.4 0 0 1-6.8-4.1c-.2-.7-.2-.9.2-25.3l-12.3 8.4c-1 .7-2.3 1-3.5 1h-15.4c-1.2 0-2.5-.3-3.5-1l-12.3-8.4c.4 24.4.3 24.6.2 25.3a5.4 5.4 0 0 1-6.8 4.1c-24.6-7.1-56.3-29-68.4-92.5-3.6-3-15.4-11.9-35.6-19.2-21-7.5-55.2-14.6-102.7-8.2a118.3 118.3 0 0 1 41 93.2 101 101 0 0 1 34.8 6.6c18 7 40.5 22.6 50.3 57.3 10.9-8.7 27.8-12.6 53.8-12.6a60.8 60.8 0 0 1 48 22.5 72 72 0 0 1 8.8 13.5c2.3-4.5 5.3-9.2 8.9-13.5a60.8 60.8 0 0 1 48-22.5c26 0 43 4 54 12.7A82.2 82.2 0 0 1 417 261a101 101 0 0 1 34.8-6.6 118.3 118.3 0 0 1 41-93.2c-47.5-6.4-81.8.7-102.7 8.2z',
strokeWeight: 2,
labelOrigin: new google.maps.Point(256, 500),
anchor: new google.maps.Point(256, 330),
scale: 1,
};
this.label.textContent = label;

return html`<adv-marker-element
.map=${this.map}
.lat=${lat}
.lng=${lon}
.content=${this.markerContent}
.zindex=${Math.floor(alt ?? 0)}
.title=${label}
@click=${this.onClick}
}
></adv-marker-element>`;
}

private onClick() {
store.dispatch(setCurrentTrackId(this.track?.id));
}

// There is not content - no need to create a shadow root.
createRenderRoot(): HTMLElement {
return this;
}
Expand Down
22 changes: 22 additions & 0 deletions apps/fxc-front/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,25 @@ ion-select.small-text::part(text) {
vertical-align: middle;
}

.fxc-marker {
width: 1px;
height: 1px;
overflow: visible;
position: relative;
font-size: 14px;
text-shadow: 0 0 5px #fff, 2px 0 5px #fff, -2px 0 5px #fff, 0 0 5px #fff,
2px 0 5px #fff, -2px 0 5px #fff;
font-weight: 500;
}

.fxc-marker svg {
display: block;
position: absolute;
overflow: visible;
}

.max-content {
margin: 0;
padding: 0;
width: max-content;
}

0 comments on commit ba2c56a

Please sign in to comment.