feat: add external directions referral

feat: change map page
feat: add error handling and timeout to location fetching in directions

resolves #124
resolves #122
This commit is contained in:
2023-08-02 17:53:49 +02:00
committed by Thea Schöbl
parent 3c49c4cf6d
commit a5c9d22016
38 changed files with 401 additions and 367 deletions

View File

@@ -42,6 +42,7 @@ export class ActionChipListComponent {
event:
item.type === SCThingType.AcademicEvent ||
(item.type === SCThingType.DateSeries && (item as SCDateSeries).dates.length > 0),
navigate: ('inPlace' in item && item.inPlace && 'geo' in item.inPlace) || 'geo' in item,
};
}

View File

@@ -14,5 +14,6 @@
-->
<stapps-locate-action-chip *ngIf="applicable.locate" [item]="item"></stapps-locate-action-chip>
<stapps-navigate-action-chip *ngIf="applicable.navigate" [item]="$any(item)"></stapps-navigate-action-chip>
<!-- Add Event Chip needs to load data and should be the last -->
<stapps-add-event-action-chip *ngIf="applicable.event" [item]="item"></stapps-add-event-action-chip>

View File

@@ -0,0 +1,35 @@
/*
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {SCPlaceWithoutReferences, SCThings} from '@openstapps/core';
@Component({
selector: 'stapps-navigate-action-chip',
templateUrl: 'navigate-action-chip.html',
styleUrls: ['navigate-action-chip.scss'],
})
export class NavigateActionChipComponent {
place: SCPlaceWithoutReferences;
@Input({required: true}) set item(value: SCThings) {
if ('geo' in value) {
this.place = value;
} else if ('inPlace' in value && value.inPlace && 'geo' in value.inPlace) {
this.place = value.inPlace;
} else {
console.error('Invalid place', value);
}
}
}

View File

@@ -1,5 +1,5 @@
<!--
~ Copyright (C) 2022 StApps
~ Copyright (C) 2023 StApps
~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3.
@@ -12,15 +12,7 @@
~ You should have received a copy of the GNU General Public License along with
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<ion-header translucent>
<ion-toolbar color="primary" mode="ios">
<ion-title>{{ 'map.modals.single.TITLE' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="modalController.dismiss()">{{ 'app.ui.CLOSE' | translate }}</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<stapps-data-detail-content [item]="$any(item)" [openAsModal]="true"></stapps-data-detail-content>
</ion-content>
<ion-chip [color]="'primary'" [outline]="true" [geoNavigation]="place">
<ion-icon name="directions"></ion-icon>
<ion-label>{{'map.directions.TITLE' | translate}}</ion-label>
</ion-chip>

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 StApps
/*!
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
@@ -12,20 +12,3 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {SCPlace} from '@openstapps/core';
import {ModalController} from '@ionic/angular';
@Component({
selector: 'app-map-single-modal',
templateUrl: './map-single.html',
styleUrls: ['./map-single.scss'],
})
export class MapSingleModalComponent {
/**
* The item to be shown
*/
@Input() item: SCPlace;
constructor(readonly modalController: ModalController) {}
}

View File

@@ -102,6 +102,8 @@ import {StappsRatingComponent} from './elements/rating.component';
import {DishCharacteristicsComponent} from './types/dish/dish-characteristics.component';
import {SkeletonListComponent} from './list/skeleton-list.component';
import {CertificationsInDetailComponent} from './elements/certifications-in-detail.component';
import {GeoNavigationDirective} from '../map/geo-navigation.directive';
import {NavigateActionChipComponent} from './chips/data/navigate-action-chip.component';
/**
* Module for handling data
@@ -110,6 +112,7 @@ import {CertificationsInDetailComponent} from './elements/certifications-in-deta
declarations: [
ActionChipListComponent,
AddEventActionChipComponent,
NavigateActionChipComponent,
EditEventSelectionComponent,
AddressDetailComponent,
CatalogDetailContentComponent,
@@ -194,6 +197,7 @@ import {CertificationsInDetailComponent} from './elements/certifications-in-deta
TranslateModule.forChild(),
ThingTranslateModule.forChild(),
UtilModule,
GeoNavigationDirective,
],
providers: [
CoordinatedSearchProvider,

View File

@@ -63,3 +63,4 @@
<stapps-data-list-item [item]="$any(item.inPlace)"></stapps-data-list-item>
</ion-card-content>
</ion-card>
<stapps-map-widget *ngIf="item.inPlace?.geo" [place]="item.inPlace"></stapps-map-widget>

View File

@@ -38,9 +38,5 @@
</ion-card>
</ng-container>
<ng-container *ngIf="hasValidLocation">
<stapps-map-widget
class="map-widget expand-when-space"
[place]="item"
expandable="true"
></stapps-map-widget>
<stapps-map-widget [place]="item" expandable="true"></stapps-map-widget>
</ng-container>

View File

@@ -12,10 +12,3 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
.map-widget {
position: relative;
width: auto;
height: 300px;
min-height: 300px;
}

View File

@@ -0,0 +1,92 @@
import {Directive, HostListener, Input} from '@angular/core';
import {SCPlaceWithoutReferences, SCThings, SCThingWithoutReferences} from '@openstapps/core';
import {Device} from '@capacitor/device';
import {ActionSheetController, ActionSheetOptions, ToastController} from '@ionic/angular';
import {TranslateService} from '@ngx-translate/core';
import {ThingTranslateService} from '../../translation/thing-translate.service';
import {Clipboard} from '@capacitor/clipboard';
import {PositionService} from './position.service';
/**
* A button that provides navigation options to the user via an action sheet
* @example
* <ion-button shape="round" [geoNavigation]="place">
* <ion-icon name="directions" slot="start"></ion-icon>
* <ion-label>{{'map.directions.TITLE' | translate}}</ion-label>
* </ion-button>
*/
@Directive({
selector: '[geoNavigation]',
standalone: true,
})
export class GeoNavigationDirective {
@Input({required: true}) geoNavigation: SCThingWithoutReferences &
Pick<SCPlaceWithoutReferences, 'geo' | 'address'>;
constructor(
private actionSheetController: ActionSheetController,
private translateService: TranslateService,
private thingTranslate: ThingTranslateService,
private toastController: ToastController,
private positionService: PositionService,
) {}
@HostListener('click', ['$event'])
async presentActionSheet(event: Event) {
event.stopPropagation();
const {operatingSystem} = await Device.getInfo();
const [lon, lat] = this.geoNavigation.geo.point.coordinates;
const supportedMapProviders =
operatingSystem === 'mac' || operatingSystem === 'ios'
? ['OSM_ROUTING', 'APPLE_MAPS', 'GOOGLE_MAPS']
: ['OSM_ROUTING', 'GOOGLE_MAPS'];
const address = this.geoNavigation.address
? this.translateService.instant(
'map.directions.ADDRESS',
this.thingTranslate.get(this.geoNavigation as SCThings, 'address'),
)
: `${lat}, ${lon}`;
const options: ActionSheetOptions = {
header: this.translateService.instant('map.directions.TITLE_LONG', {
name: this.thingTranslate.get(this.geoNavigation as SCThings, 'name'),
}),
subHeader: address,
buttons: [
{
text: this.translateService.instant('map.directions.COPY_ADDRESS'),
role: 'selected',
handler: async () => {
await Clipboard.write({string: address});
this.toastController
.create({
message: this.translateService.instant('map.directions.ADDRESS_COPIED'),
duration: 500,
})
.then(toast => toast.present());
},
},
...supportedMapProviders.map(provider => ({
text: this.translateService.instant(`map.directions.${provider}.TITLE`),
handler: () => {
const url: string = this.translateService.instant(`map.directions.${provider}.URL`, {
lat,
lon,
posLat: this.positionService.position?.latitude ?? 0,
posLon: this.positionService.position?.longitude ?? 0,
});
window.open(url.replace(/&?\w+=0,0/, ''), '_blank', 'noreferrer');
},
})),
{
text: this.translateService.instant('abort'),
role: 'cancel',
},
],
};
const actionSheet = await this.actionSheetController.create(options);
await actionSheet.present();
}
}

View File

@@ -1,36 +0,0 @@
<!--
~ Copyright (C) 2022 StApps
~ This program is free software: you can redistribute it and/or modify it
~ under the terms of the GNU General Public License as published by the Free
~ Software Foundation, version 3.
~
~ This program is distributed in the hope that it will be useful, but WITHOUT
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along with
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<ion-card class="compact">
<ion-card-header>
<stapps-data-list-item [item]="$any(item)" id="show-more"></stapps-data-list-item>
<stapps-skeleton-list-item *ngIf="!item"></stapps-skeleton-list-item>
</ion-card-header>
<ion-card-content>
<ion-note>
<span *ngIf="item.address as address">
<span *ngIf="$any(item).inPlace">{{ $any(item).inPlace.name }},</span>
{{ address.streetAddress }}, {{ address.addressLocality }}
</span>
</ion-note>
<ion-button
size="small"
class="show-more-button"
fill="clear"
[routerLink]="['/data-detail', item.uid]"
>{{ 'map.page.buttons.MORE' | translate }}</ion-button
>
</ion-card-content>
</ion-card>

View File

@@ -1,72 +0,0 @@
/*!
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
@import '../../../../theme/util/mixins';
:host {
display: block;
max-width: 100%;
ion-card {
overflow: visible;
padding: 0;
ion-card-header {
padding: 0;
border-bottom: var(--border-width-default) solid var(--border-color-default);
stapps-data-list-item {
--ion-margin: 0;
&::ng-deep ion-item {
--padding-start: 0;
--padding-end: 0;
ion-label {
white-space: break-spaces;
}
}
}
.close {
--padding-top: 0;
--padding-bottom: 0;
--padding-start: 0;
--padding-end: 0;
position: absolute;
z-index: 1;
top: -15px;
right: -15px;
ion-icon {
width: 30px;
height: 30px;
}
}
}
ion-card-content {
display: flex;
flex-direction: row;
padding: var(--spacing-md);
.show-more-button {
margin-left: auto;
text-transform: uppercase;
}
}
}
}

View File

@@ -1,41 +0,0 @@
/*
* Copyright (C) 2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {SCPlace} from '@openstapps/core';
import {IonRouterOutlet} from '@ionic/angular';
@Component({
selector: 'stapps-map-item',
templateUrl: './map-item.component.html',
styleUrls: ['./map-item.component.scss'],
})
export class MapItemComponent {
/**
* An item to show
*/
@Input() item: SCPlace;
// eslint-disable-next-line @angular-eslint/no-output-on-prefix
@Output() onClose = new EventEmitter<void>();
constructor(readonly routerOutlet: IonRouterOutlet) {}
/**
* Action when edit is clicked
*/
onCloseClick() {
this.onClose.emit();
}
}

View File

@@ -29,12 +29,11 @@ import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
import {MenuModule} from '../menu/menu.module';
import {MapProvider} from './map.provider';
import {MapPageComponent} from './page/map-page.component';
import {MapListModalComponent} from './page/modals/map-list-modal.component';
import {MapSingleModalComponent} from './page/modals/map-single-modal.component';
import {MapItemComponent} from './item/map-item.component';
import {MapListModalComponent} from './page/map-list-modal.component';
import {NgModule} from '@angular/core';
import {UtilModule} from '../../util/util.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
import {GeoNavigationDirective} from './geo-navigation.directive';
/**
* Initializes the default area to show in advance (before components are initialized)
@@ -56,7 +55,7 @@ const mapRoutes: Routes = [
* Module containing map related stuff
*/
@NgModule({
declarations: [MapPageComponent, MapListModalComponent, MapSingleModalComponent, MapItemComponent],
declarations: [MapPageComponent, MapListModalComponent],
exports: [],
imports: [
CommonModule,
@@ -71,6 +70,8 @@ const mapRoutes: Routes = [
FormsModule,
ThingTranslateModule,
UtilModule,
GeoNavigationDirective,
GeoNavigationDirective,
],
providers: [Geolocation, MapProvider, DataProvider, DataFacetsProvider, StAppsWebHttpClient],
})

View File

@@ -14,7 +14,7 @@
*/
import {Component, Input, OnInit} from '@angular/core';
import {SCSearchBooleanFilter, SCPlace, SCSearchFilter} from '@openstapps/core';
import {MapProvider} from '../../map.provider';
import {MapProvider} from '../map.provider';
import {ModalController} from '@ionic/angular';
import {LatLngBounds} from 'leaflet';
@@ -23,8 +23,8 @@ import {LatLngBounds} from 'leaflet';
*/
@Component({
selector: 'map-list-modal',
templateUrl: 'map-list.html',
styleUrls: ['map-list.scss'],
templateUrl: 'map-list-modal.html',
styleUrls: ['map-list-modal.scss'],
})
export class MapListModalComponent implements OnInit {
/**

View File

@@ -29,6 +29,7 @@ import {Geolocation, PermissionStatus} from '@capacitor/geolocation';
import {Capacitor} from '@capacitor/core';
import {pauseWhen} from '../../../util/pause-when';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {startViewTransition} from '../../../util/view-transition';
/**
* The main page of the map
@@ -100,7 +101,17 @@ export class MapPageComponent implements OnInit {
/**
* Options of the leaflet map
*/
options: MapOptions;
options: MapOptions = {
center: geoJSON(this.mapProvider.defaultPolygon).getBounds().getCenter(),
layers: [
tileLayer('https://osm.server.uni-frankfurt.de/tiles/roads/x={x}&y={y}&z={z}', {
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
maxZoom: this.MAX_ZOOM,
}),
],
zoom: this.DEFAULT_ZOOM,
zoomControl: false,
};
/**
* Position of the user on the map
@@ -134,20 +145,7 @@ export class MapPageComponent implements OnInit {
private dataRoutingService: DataRoutingService,
private positionService: PositionService,
readonly routerOutlet: IonRouterOutlet,
) {
// initialize the options
this.options = {
center: geoJSON(this.mapProvider.defaultPolygon).getBounds().getCenter(),
layers: [
tileLayer('https://osm.server.uni-frankfurt.de/tiles/roads/x={x}&y={y}&z={z}', {
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
maxZoom: this.MAX_ZOOM,
}),
],
zoom: this.DEFAULT_ZOOM,
zoomControl: false,
};
}
) {}
ngOnInit() {
this.dataRoutingService
@@ -305,6 +303,7 @@ export class MapPageComponent implements OnInit {
*/
async onMapReady(map: Map) {
this.map = map;
this.map.attributionControl.setPosition('topright');
const interval = window.setInterval(() =>
MapProvider.invalidateWhenRendered(map, this.mapContainer, interval),
);
@@ -384,10 +383,12 @@ export class MapPageComponent implements OnInit {
* Resets the map = fetch all the items based on the filters (and go to component's base location)
*/
async resetView() {
this.location.go('/map');
await this.fetchAndUpdateItems(this.items.length > 0);
startViewTransition(async () => {
this.location.go('/map');
await this.fetchAndUpdateItems(this.items.length > 0);
this.ref.detectChanges();
this.ref.detectChanges();
});
}
/**
@@ -414,14 +415,16 @@ export class MapPageComponent implements OnInit {
* @param uid Uuid of the place
*/
async showItem(uid: SCUuid) {
const response = await this.mapProvider.searchPlace(uid);
this.items = response.data as SCPlace[];
this.distance = this.positionService.getDistance(this.items[0].geo.point);
this.addToMap(this.items, true);
this.ref.detectChanges();
const url = this.router.createUrlTree(['/map', uid]).toString();
this.location.go(url);
// center the selected place
this.focus(geoJSON(this.items[0].geo.point).getBounds().getCenter());
startViewTransition(async () => {
const response = await this.mapProvider.searchPlace(uid);
this.items = response.data as SCPlace[];
this.distance = this.positionService.getDistance(this.items[0].geo.point);
this.addToMap(this.items, true);
this.ref.detectChanges();
const url = this.router.createUrlTree(['/map', uid]).toString();
this.location.go(url);
// center the selected place
this.focus(geoJSON(this.items[0].geo.point).getBounds().getCenter());
});
}
}

View File

@@ -43,7 +43,7 @@
</ion-toolbar>
</ion-header>
<ion-content fullscreen id="map">
<ion-content id="map">
<div
class="map-container"
#mapContainer
@@ -57,7 +57,7 @@
<div *ngIf="position" [leafletLayer]="positionMarker"></div>
</div>
<div class="floating-content">
<div class="map-buttons above">
<div class="map-buttons">
<ion-button
*ngIf="items.length > 1"
color="light"
@@ -67,7 +67,13 @@
>
<ion-icon name="list"></ion-icon>&nbsp;&nbsp;{{ 'map.page.buttons.SHOW_LIST' | translate }}
</ion-button>
<ion-button color="light" shape="round" size="small" (click)="onPositionClick()">
<ion-button
color="light"
shape="round"
size="small"
(click)="onPositionClick()"
class="location-button"
>
<ion-icon *ngIf="position !== null; else noLocationIcon" name="my_location"></ion-icon>
<ng-template #noLocationIcon>
<ion-icon
@@ -80,30 +86,12 @@
</ng-template>
</ion-button>
</div>
<stapps-map-item *ngIf="items.length === 1" [item]="items[0]" (onClose)="resetView()"></stapps-map-item>
</div>
<div class="map-buttons floating-buttons">
<ion-button
*ngIf="items.length > 1"
color="light"
shape="round"
size="small"
(click)="mapListModal.present()"
>
<ion-icon name="list"></ion-icon>&nbsp;&nbsp;{{ 'map.page.buttons.SHOW_LIST' | translate }}
</ion-button>
<ion-button color="light" shape="round" size="small" (click)="onPositionClick()">
<ion-icon *ngIf="position !== null; else noLocationIcon" name="my_location"></ion-icon>
<ng-template #noLocationIcon>
<ion-icon
*ngIf="locationStatus && locationStatus.location === 'denied'; else pendingLocationIcon"
name="location_disabled"
></ion-icon>
</ng-template>
<ng-template #pendingLocationIcon>
<ion-icon name="location_searching"></ion-icon>
</ng-template>
</ion-button>
<ion-card class="map-item">
<stapps-data-list-item *ngIf="items.length === 1" [item]="$any(items[0])"></stapps-data-list-item>
<ion-button fill="clear" class="close" (click)="resetView()">
<ion-icon size="22" name="close" slot="icon-only"></ion-icon>
</ion-button>
</ion-card>
</div>
<ion-modal [canDismiss]="true" #mapListModal>

View File

@@ -12,113 +12,87 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
@import '../../../../theme/util/mixins';
ion-content {
// fixes the unexpected issue that the content is not fullscreen (behind the header)
position: absolute;
$bottom-offset: 7px; // no idea what happened here
div.map-container {
position: fixed;
width: 100%;
height: 100%;
}
& > div {
overflow: hidden;
}
.map-container {
width: 100%;
height: 100%;
}
ion-toolbar:first-of-type {
padding: 0 var(--spacing-md) var(--spacing-xs);
}
div.map-buttons {
.floating-content {
position: fixed;
z-index: 1000;
right: 0;
bottom: 0;
left: 0;
display: flex;
flex-flow: row-reverse wrap;
align-items: flex-end;
justify-content: space-between;
}
.map-buttons {
display: flex;
justify-content: flex-end;
ion-button {
// important for iOS
// TODO: find an option that is better suited for the iOS theme
--box-shadow: var(--map-box-shadow);
align-self: flex-end;
margin: 4px;
margin: var(--spacing-md);
&.location-button {
view-transition-name: location-button;
}
}
}
::ng-deep {
.stapps-location {
ion-icon {
width: 100%;
height: 100%;
color: #fd435c;
}
}
.map-item {
position: relative;
max-width: 550px;
margin: var(--spacing-md);
.stapps-device-location {
ion-icon {
width: 100%;
height: 100%;
color: #4387fd;
}
}
div.floating-content {
position: fixed;
z-index: 1000;
.close {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
display: block;
justify-content: center;
::ng-deep ion-item {
margin: 0;
}
}
@include ion-md-down {
.md {
ion-content {
--padding-bottom: $bottom-offset;
}
.floating-content {
bottom: $bottom-offset;
}
}
.map-buttons ion-button {
margin: var(--spacing-sm);
}
.map-item {
width: 100%;
padding: 0 var(--spacing-md) 8vh;
max-width: unset;
margin: 0;
ion-card {
margin: 0;
}
div.map-buttons {
display: flex;
justify-content: flex-end;
}
stapps-map-item {
position: center;
justify-self: center;
width: 550px;
margin: var(--spacing-sm) auto;
}
}
}
div.floating-buttons {
position: absolute;
z-index: 1000;
right: 10px;
bottom: 15px;
}
div.map-buttons.above {
display: none;
min-width: 70%;
}
@media (width <= 667px) {
div.map-buttons.above {
display: flex;
}
div.floating-content {
justify-content: normal;
padding: 0 var(--spacing-md) var(--spacing-lg);
stapps-map-item {
display: grid;
width: 100%;
}
}
div.map-buttons.floating-buttons {
display: none;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
}

View File

@@ -1,4 +0,0 @@
:host {
display: flex;
flex-direction: column;
}

View File

@@ -12,9 +12,9 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core';
import {Component, ElementRef, HostBinding, Input, OnInit, ViewChild} from '@angular/core';
import {Router} from '@angular/router';
import {SCPlace} from '@openstapps/core';
import {SCPlaceWithoutReferences, SCThingWithoutReferences} from '@openstapps/core';
import {geoJSON, Map, MapOptions, tileLayer} from 'leaflet';
import {MapProvider} from '../map.provider';
@@ -27,6 +27,8 @@ import {MapProvider} from '../map.provider';
templateUrl: './map-widget.html',
})
export class MapWidgetComponent implements OnInit {
@HostBinding('class.expand-when-space') expandWhenSpace = true;
/**
* A leaflet map showed
*/
@@ -45,7 +47,7 @@ export class MapWidgetComponent implements OnInit {
/**
* A place to show on the map
*/
@Input() place: SCPlace;
@Input() place: SCThingWithoutReferences & Pick<SCPlaceWithoutReferences, 'geo' | 'address'>;
/**
* Indicates if the expand button should be visible

View File

@@ -21,6 +21,10 @@
[leafletOptions]="options"
></div>
<div class="map-buttons" *ngIf="showExpandButton">
<ion-button color="primary" shape="round" size="small" [geoNavigation]="place">
<ion-icon name="directions" slot="start"></ion-icon>
{{'map.directions.TITLE' | translate}}
</ion-button>
<ion-button color="primary" shape="round" size="small" [routerLink]="['/map', place.uid]">
<ion-icon name="zoom_out_map"></ion-icon>
</ion-button>

View File

@@ -12,6 +12,12 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
:host {
position: relative;
width: auto;
height: 300px;
min-height: 300px;
}
div.map-container {
pointer-events: none;

View File

@@ -17,23 +17,31 @@
import english from '../../assets/i18n/en.json';
import german from '../../assets/i18n/de.json';
const exceptions = new Set([
'login',
'ok',
'protein',
'feedback',
'name',
'status',
'issn',
'ejournal',
'backup',
'export',
'dashboard',
'home',
'email',
'logins',
'https://www.swffm.de/essen-trinken/uebersicht/umweltscore',
]);
const exceptions = new Set(
[
'login',
'ok',
'protein',
'feedback',
'name',
'status',
'issn',
'ejournal',
'backup',
'export',
'dashboard',
'home',
'email',
'logins',
'google maps',
'apple maps',
'openstreetmaps routing',
'https://www.swffm.de/essen-trinken/uebersicht/umweltscore',
'https://www.google.com/maps/dir/?api=1&destination={{lat}},{{lon}}&origin={{posLat}},{{posLon}}',
'https://maps.apple.com/?daddr={{lat}},{{lon}}&saddr={{posLat}},{{posLon}}',
'https://www.openstreetmap.org/directions?from={{posLat}},{{posLon}}&to={{lat}},{{lon}}#map=15/{{lat}}/{{lon}}',
].map(it => it.toLowerCase()),
);
const languages = [
['english', english],

View File

@@ -0,0 +1,14 @@
/**
* Performs a view transition
*
* This is a progressive enhancement for (as of right now) Chromium-based browsers
* - Firefox position: [positive](https://mozilla.github.io/standards-positions/#view-transitions)
* - WebKit position: [support](https://github.com/WebKit/standards-positions/issues/48#issuecomment-1679760489)
*/
export function startViewTransition(runner: () => Promise<void>) {
if ('startViewTransition' in document) {
document.startViewTransition(runner);
} else {
void runner();
}
}

View File

@@ -244,6 +244,25 @@
}
},
"map": {
"directions": {
"TITLE": "Anbindung",
"TITLE_LONG": "Anbindung nach {{name}}",
"ADDRESS": "{{streetAddress}}, {{postalCode}} {{addressLocality}}",
"COPY_ADDRESS": "Adresse kopieren",
"ADDRESS_COPIED": "Adresse wurde kopiert",
"GOOGLE_MAPS": {
"TITLE": "Google Maps",
"URL": "https://www.google.com/maps/dir/?api=1&destination={{lat}},{{lon}}&origin={{posLat}},{{posLon}}"
},
"APPLE_MAPS": {
"TITLE": "Apple Maps",
"URL": "https://maps.apple.com/?daddr={{lat}},{{lon}}&saddr={{posLat}},{{posLon}}"
},
"OSM_ROUTING": {
"TITLE": "OpenStreetMaps Routing",
"URL": "https://routing.openstreetmap.de/?loc={{posLat}},{{posLon}}&loc={{lat}},{{lon}}&srv=2&hl=de"
}
},
"page": {
"TITLE": "Karte",
"search_bar": {

View File

@@ -244,6 +244,25 @@
}
},
"map": {
"directions": {
"TITLE": "Directions",
"TITLE_LONG": "Directions to {{name}}",
"ADDRESS": "{{streetAddress}}, {{addressLocality}} {{postalCode}}",
"COPY_ADDRESS": "Copy Address",
"ADDRESS_COPIED": "Address copied",
"GOOGLE_MAPS": {
"TITLE": "Google Maps",
"URL": "https://www.google.com/maps/dir/?api=1&destination={{lat}},{{lon}}&origin={{posLat}},{{posLon}}"
},
"APPLE_MAPS": {
"TITLE": "Apple Maps",
"URL": "https://maps.apple.com/?daddr={{lat}},{{lon}}&saddr={{posLat}},{{posLon}}"
},
"OSM_ROUTING": {
"TITLE": "OpenStreetMaps Routing",
"URL": "https://routing.openstreetmap.de/?loc={{posLat}},{{posLon}}&loc={{lat}},{{lon}}&srv=2&hl=en"
}
},
"page": {
"TITLE": "Map",
"search_bar": {

View File

@@ -131,6 +131,18 @@ ion-alert {
}
}
.stapps-location ion-icon {
width: 100%;
height: 100%;
color: #fd435c;
}
.stapps-device-location ion-icon {
width: 100%;
height: 100%;
color: #4387fd;
}
.add-event-popover {
--width: fit-content;
--max-width: 95%;