mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-21 17:12:43 +00:00
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:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
92
frontend/app/src/app/modules/map/geo-navigation.directive.ts
Normal file
92
frontend/app/src/app/modules/map/geo-navigation.directive.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
/**
|
||||
@@ -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: '© <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: '© <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());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {{ '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> {{ '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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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],
|
||||
|
||||
14
frontend/app/src/app/util/view-transition.ts
Normal file
14
frontend/app/src/app/util/view-transition.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Binary file not shown.
@@ -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%;
|
||||
|
||||
Reference in New Issue
Block a user