Skip to content

Commit

Permalink
Merge pull request #322: minimize confusion when location services ar…
Browse files Browse the repository at this point in the history
…e denied
  • Loading branch information
michaelkirk authored Feb 8, 2024
2 parents 2b850ab + a315f79 commit 6900abf
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 6 deletions.
4 changes: 3 additions & 1 deletion services/frontend/www-app/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,11 @@ module.exports = {
// does not work with type definitions
'no-unused-vars': 'off',

// Any is "bad", and you should feel bad if you use it, but it sure is convenient sometimes
// Make development less painful
'@typescript-eslint/no-explicit-any':
process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'@typescript-eslint/no-unused-vars':
process.env.NODE_ENV === 'production' ? 'error' : 'warn',

// allow debugger during development only
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
Expand Down
31 changes: 31 additions & 0 deletions services/frontend/www-app/src/components/BaseMap.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,37 @@
animation: hideElement 0.2s forwards;
animation-delay: 5s;
}
.headway-location-control-container {
position: relative;
.headway-location-disabled-banner {
position: absolute;
right: 0;
bottom: 24px;
width: 200px;
padding: 8px;
text-align: center;
background: rgba(255, 185, 185, 0.96);
// add shadow
box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.5);
}
.headway-location-control-click-interceptor {
position: absolute;
width: 100%;
height: 100%;
z-index: 1;
pointer-events: all;
cursor: pointer;
}
&:has(button[disabled]) {
.headway-location-control-click-interceptor {
cursor: not-allowed;
}
}
}
</style>

<script lang="ts">
Expand Down
3 changes: 2 additions & 1 deletion services/frontend/www-app/src/i18n/en-US/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export default {
try_driving_directions: 'Try driving directions instead?',
where_to_question: 'Where to?',
my_location: 'My Location',
could_not_get_gps_location: 'Could not get GPS location',
location_permission_denied_banner:
'Location permission was denied. Check your privacy settings.',
dropped_pin: 'Dropped Pin',
via_$place: 'via {place}',
via$transit_route: 'via route {transitRoute}',
Expand Down
77 changes: 75 additions & 2 deletions services/frontend/www-app/src/ui/LocationControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
LngLat,
} from 'maplibre-gl';
import env from 'src/utils/env';
import { i18n } from 'src/i18n/lang';

/**
* A wrapper for maplibre-gl's GeolocateControl that adds a compass to the user's location dot,
Expand All @@ -19,6 +20,7 @@ export default class LocationControl extends Evented implements IControl {
geolocateControl: GeolocateControl;
compassEl: HTMLElement;
svgEl: HTMLElement;
controlContainer: HTMLElement;
currentRotation = 0;
compassMarker: Marker;

Expand All @@ -36,6 +38,21 @@ export default class LocationControl extends Evented implements IControl {
this.svgEl = svgEl;
compassEl.append(svgEl);
this.compassEl = compassEl;
this.controlContainer = document.createElement('div');
this.controlContainer.className = 'headway-location-control-container';

// create click interceptor div inside controlContainer. When it's clicked pass the clicks through
// to the geolocate control button
const clickInterceptor = document.createElement('div');
clickInterceptor.className = 'headway-location-control-click-interceptor';
clickInterceptor.addEventListener('click', () => {
if (this.geolocateControl._container.querySelector('button[disabled]')) {
this.onLocationServiceIsDisabled();
} else {
this.geolocateControl._container.querySelector('button')?.click();
}
});
this.controlContainer.append(clickInterceptor);

this.compassMarker = new Marker({
element: compassEl,
Expand All @@ -55,11 +72,20 @@ export default class LocationControl extends Evented implements IControl {
this._updateMarkerPosition.bind(this),
);

// make sure the compass is hidden when the user stops tracking their location
this.geolocateControl.on(
'trackuserlocationend',
this._updateMarker.bind(this),
);

geolocateControlEl.addEventListener('click', () => {
env.deviceOrientation.startWatching();
});

return geolocateControlEl;
this._setupDisabledObserver(geolocateControlEl);

this.controlContainer.append(geolocateControlEl);
return this.controlContainer;
}

/** {@inheritDoc IControl.onRemove} */
Expand Down Expand Up @@ -88,11 +114,58 @@ export default class LocationControl extends Evented implements IControl {
this._updateMarker();
}

_setupDisabledObserver(geolocateControlEl: HTMLElement): void {
const observer = new MutationObserver((mutations) => {
const button = geolocateControlEl.querySelector('button');
if (!button) {
// button not inserted yet
return;
}

mutations.forEach((mutation) => {
if (
mutation.type === 'attributes' &&
mutation.attributeName === 'disabled'
) {
const isDisabled = button.getAttribute('disabled') !== null;
if (isDisabled) {
this.onLocationServiceIsDisabled();
}
}
});
});

// Specify what to observe (attribute changes in this case)
const config = { attributes: true, childList: true, subtree: true };
observer.observe(geolocateControlEl, config);
}

onLocationServiceIsDisabled() {
// abort if banner already exists
if (
this.controlContainer.querySelector('.headway-location-disabled-banner')
) {
return;
}
const banner = document.createElement('p');
banner.className = 'headway-location-disabled-banner';
banner.textContent = i18n.global.t('location_permission_denied_banner');
this.controlContainer.append(banner);

this.map?.once('click', () => {
banner.remove();
});
banner.addEventListener('click', () => {
banner.remove();
});
}

_updateMarker() {
if (
this.mostRecentPosition === undefined ||
this.mostRecentCompassHeading == undefined ||
this.map === undefined
this.map === undefined ||
this.geolocateControl._watchState == 'OFF'
) {
this.compassMarker.remove();
return;
Expand Down
10 changes: 8 additions & 2 deletions services/frontend/www-app/src/utils/DeviceOrientationPolyfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ type State =
| 'unsupported'
| 'watching';

let alreadySubscribedToFakeDeviceOrientation = false;

export default class DeviceOrientationPolyfill {
private mostRecentHeading: number | null = null;
private state: State = 'init';
Expand Down Expand Up @@ -51,9 +53,13 @@ export default class DeviceOrientationPolyfill {
// it's useful to be able to test this on desktop
const fakeOnDesktop = false;
if (fakeOnDesktop && Platform.is.desktop) {
if (alreadySubscribedToFakeDeviceOrientation) {
return;
}
alreadySubscribedToFakeDeviceOrientation = true;

// console.log('fake device orientation for testing on desktop');
// setInterval(() => {
setTimeout(() => {
setInterval(() => {
// start north and turn clockwise
let alpha;
if (this.mostRecentHeading === null) {
Expand Down

0 comments on commit 6900abf

Please sign in to comment.