mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-02-22 17:02:09 +00:00
feat: add map module
This commit is contained in:
19
angular.json
19
angular.json
@@ -31,6 +31,16 @@
|
||||
"input": "node_modules/ionicons/dist/ionicons/svg",
|
||||
"output": "./svg"
|
||||
},
|
||||
{
|
||||
"glob": "**/*.svg",
|
||||
"input": "src/assets/custom-ionicons",
|
||||
"output": "./svg"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "./node_modules/leaflet/dist/images",
|
||||
"output": "assets/"
|
||||
},
|
||||
{
|
||||
"glob": "**/*.svg",
|
||||
"input": "src/assets/custom-ion-icons",
|
||||
@@ -43,7 +53,9 @@
|
||||
},
|
||||
{
|
||||
"input": "src/global.scss"
|
||||
}
|
||||
},
|
||||
"./node_modules/leaflet/dist/leaflet.css",
|
||||
"./node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
@@ -158,6 +170,11 @@
|
||||
"glob": "**/*",
|
||||
"input": "src/assets",
|
||||
"output": "/assets"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "./node_modules/leaflet/dist/images",
|
||||
"output": "assets/"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -31,4 +31,5 @@
|
||||
<engine name="ios" spec="6.2.0" />
|
||||
<engine name="browser" spec="6.0.0" />
|
||||
<engine name="android" spec="9.0.0" />
|
||||
<plugin name="cordova.plugins.diagnostic" spec="6.0.3" />
|
||||
</widget>
|
||||
|
||||
75
package-lock.json
generated
75
package-lock.json
generated
@@ -814,6 +814,16 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/router/-/router-9.1.12.tgz",
|
||||
"integrity": "sha512-+qCaXa9y0nsRhzjAYBqmGoQ2YkrdXgftZwuFDf6t4qEi30EXa0oS97KrlFq0M5GKdLIDGrbUm9PcdHSTOI+ZhA=="
|
||||
},
|
||||
"@asymmetrik/ngx-leaflet": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@asymmetrik/ngx-leaflet/-/ngx-leaflet-5.0.2.tgz",
|
||||
"integrity": "sha512-y4+U9nUnukdTTO2Z6fCt0V85U605bveERNIzrovjKELkHbxz70TvJSvCU7iSSc5ZwS5ZJI1a24SiBJ5sGt9WMQ=="
|
||||
},
|
||||
"@asymmetrik/ngx-leaflet-markercluster": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@asymmetrik/ngx-leaflet-markercluster/-/ngx-leaflet-markercluster-2.1.1.tgz",
|
||||
"integrity": "sha512-ngsPNpVNrWpTaNqiHnepdJJjYmV0OH3Q7ubAH8qcxMtlH1Imqjp3lF7sdq4Qa8z3mrbJQSxFwLNCsKHbEFk6ow=="
|
||||
},
|
||||
"@babel/code-frame": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz",
|
||||
@@ -2266,6 +2276,21 @@
|
||||
"@types/cordova": "^0.0.34"
|
||||
}
|
||||
},
|
||||
"@ionic-native/diagnostic": {
|
||||
"version": "5.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@ionic-native/diagnostic/-/diagnostic-5.32.0.tgz",
|
||||
"integrity": "sha512-1DRub6i8hNhHYy5HWlun0CjhmE1cwZZWyLB9ptkwLkZdfFEazwj0eKjz3BmpCs6wy593smNn/x9G0lCwvLCZlA==",
|
||||
"requires": {
|
||||
"@types/cordova": "^0.0.34"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/cordova": {
|
||||
"version": "0.0.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz",
|
||||
"integrity": "sha1-6nrd907Ow9dimCegw54smt3HPQQ="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@ionic-native/geolocation": {
|
||||
"version": "5.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@ionic-native/geolocation/-/geolocation-5.29.0.tgz",
|
||||
@@ -3207,6 +3232,24 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/leaflet": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.4.4.tgz",
|
||||
"integrity": "sha512-CROxHvsRDFyR1OQKZv/WJsCVFv8Wj6wFF/FOI/yiGwX7GMivyvF8Ks5AT3/JYk269pGiE43wP9JOgcr7EK2eUw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"@types/leaflet.markercluster": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/leaflet.markercluster/-/leaflet.markercluster-1.0.3.tgz",
|
||||
"integrity": "sha512-rz4xQcsD3Ha9TcX4nMba9wpNe7HPQ03Hvo8Osi3SLpfaDCydHMoTquOG1IsjQ2aFm/LIHz4Uo4hYoeLv7q082w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/leaflet": "*"
|
||||
}
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.170",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.170.tgz",
|
||||
@@ -7227,6 +7270,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"cordova.plugins.diagnostic": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cordova.plugins.diagnostic/-/cordova.plugins.diagnostic-6.0.3.tgz",
|
||||
"integrity": "sha512-Pj3pPHYtZEVHQyDmDcUKI78arzcpmIod+5DnOAtVvAvOa5AErUlPou4/MPAv08cw/REYsJ8Siy6YIWkdMnMaFA==",
|
||||
"requires": {
|
||||
"colors": "^1.1.2",
|
||||
"elementtree": "^0.1.6",
|
||||
"minimist": "1.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimist": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
|
||||
}
|
||||
}
|
||||
},
|
||||
"core-js": {
|
||||
"version": "2.6.5",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz",
|
||||
@@ -10007,6 +10067,11 @@
|
||||
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
||||
"dev": true
|
||||
},
|
||||
"geojson": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/geojson/-/geojson-0.5.0.tgz",
|
||||
"integrity": "sha1-PNbJY5m+ZbVu5VWWEW/pGRznAcA="
|
||||
},
|
||||
"get-assigned-identifiers": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz",
|
||||
@@ -12648,6 +12713,16 @@
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
||||
"dev": true
|
||||
},
|
||||
"leaflet": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.4.0.tgz",
|
||||
"integrity": "sha512-x9j9tGY1+PDLN9pcWTx9/y6C5nezoTMB8BLK5jTakx+H7bPlnbCHfi9Hjg+Qt36sgDz/cb9lrSpNQXmk45Tvhw=="
|
||||
},
|
||||
"leaflet.markercluster": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.4.1.tgz",
|
||||
"integrity": "sha512-ZSEpE/EFApR0bJ1w/dUGwTSUvWlpalKqIzkaYdYB7jaftQA/Y2Jav+eT4CMtEYFj+ZK4mswP13Q2acnPBnhGOw=="
|
||||
},
|
||||
"less": {
|
||||
"version": "3.11.3",
|
||||
"resolved": "https://registry.npmjs.org/less/-/less-3.11.3.tgz",
|
||||
|
||||
18
package.json
18
package.json
@@ -26,7 +26,7 @@
|
||||
"docker:serve": "sudo docker run -p 8100:8100 -p 35729:35729 -p 53703:53703 -v $PWD:/app -it registry.gitlab.com/openstapps/app bash -c \"npm start\"",
|
||||
"documentation": "compodoc -p tsconfig.json -d docs",
|
||||
"lint": "ng lint",
|
||||
"lint:fix": "ng lint:fix",
|
||||
"lint-fix": "eslint --fix -c .eslintrc.json --ignore-path .eslintignore --ext .ts,.html src/",
|
||||
"ng": "ng",
|
||||
"postversion": "npm run changelog",
|
||||
"pree2e": "webdriver-manager clean && webdriver-manager update --gecko false --versions.chrome $(google-chrome --product-version)",
|
||||
@@ -47,8 +47,11 @@
|
||||
"@angular/platform-browser": "9.1.12",
|
||||
"@angular/platform-browser-dynamic": "9.1.12",
|
||||
"@angular/router": "9.1.12",
|
||||
"@asymmetrik/ngx-leaflet": "5.0.2",
|
||||
"@asymmetrik/ngx-leaflet-markercluster": "2.1.1",
|
||||
"@capacitor/core": "2.4.6",
|
||||
"@ionic-native/core": "5.29.0",
|
||||
"@ionic-native/diagnostic": "5.32.0",
|
||||
"@ionic-native/geolocation": "5.29.0",
|
||||
"@ionic-native/network": "5.31.1",
|
||||
"@ionic-native/splash-screen": "5.29.0",
|
||||
@@ -65,6 +68,7 @@
|
||||
"cordova-ios": "6.2.0",
|
||||
"cordova-plugin-androidx-adapter": "1.1.3",
|
||||
"cordova-plugin-device": "2.0.3",
|
||||
"cordova.plugins.diagnostic": "6.0.3",
|
||||
"cordova-plugin-geolocation": "4.1.0",
|
||||
"cordova-plugin-ionic-keyboard": "2.2.0",
|
||||
"cordova-plugin-ionic-webview": "5.0.0",
|
||||
@@ -74,6 +78,9 @@
|
||||
"core-js": "2.6.5",
|
||||
"deepmerge": "3.3.0",
|
||||
"form-data": "2.5.0",
|
||||
"geojson": "0.5.0",
|
||||
"leaflet": "1.4.0",
|
||||
"leaflet.markercluster": "1.4.1",
|
||||
"lodash-es": "4.17.21",
|
||||
"moment": "2.29.1",
|
||||
"ngx-logger": "4.1.9",
|
||||
@@ -103,6 +110,8 @@
|
||||
"@types/form-data": "2.5.0",
|
||||
"@types/jasmine": "3.3.12",
|
||||
"@types/jasminewd2": "2.0.6",
|
||||
"@types/leaflet": "1.4.4",
|
||||
"@types/leaflet.markercluster": "1.0.3",
|
||||
"@types/lodash-es": "4.17.4",
|
||||
"@types/node": "14.14.37",
|
||||
"@typescript-eslint/eslint-plugin": "4.3.0",
|
||||
@@ -134,11 +143,16 @@
|
||||
"cordova-plugin-whitelist": {},
|
||||
"cordova-plugin-device": {},
|
||||
"cordova-plugin-splashscreen": {},
|
||||
"cordova-plugin-ionic-webview": {},
|
||||
"cordova-plugin-ionic-webview": {
|
||||
"ANDROID_SUPPORT_ANNOTATIONS_VERSION": "27.+"
|
||||
},
|
||||
"cordova-plugin-ionic-keyboard": {},
|
||||
"cordova-plugin-geolocation": {
|
||||
"GEOLOCATION_USAGE_DESCRIPTION": "The app will use your location to provide features for navigation or distances information.",
|
||||
"GPS_REQUIRED": "true"
|
||||
},
|
||||
"cordova.plugins.diagnostic": {
|
||||
"ANDROIDX_VERSION": "1.+"
|
||||
}
|
||||
},
|
||||
"platforms": [
|
||||
|
||||
@@ -23,6 +23,7 @@ import localeDe from '@angular/common/locales/de';
|
||||
import {APP_INITIALIZER, NgModule, Provider} from '@angular/core';
|
||||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {RouteReuseStrategy} from '@angular/router';
|
||||
import {Diagnostic} from '@ionic-native/diagnostic/ngx';
|
||||
import {SplashScreen} from '@ionic-native/splash-screen/ngx';
|
||||
import {StatusBar} from '@ionic-native/status-bar/ngx';
|
||||
import {IonicModule, IonicRouteStrategy} from '@ionic/angular';
|
||||
@@ -41,6 +42,7 @@ import {AppComponent} from './app.component';
|
||||
import {ConfigModule} from './modules/config/config.module';
|
||||
import {ConfigProvider} from './modules/config/config.provider';
|
||||
import {DataModule} from './modules/data/data.module';
|
||||
import {MapModule} from './modules/map/map.module';
|
||||
import {MenuModule} from './modules/menu/menu.module';
|
||||
import {NewsModule} from './modules/news/news.module';
|
||||
import {SettingsModule} from './modules/settings/settings.module';
|
||||
@@ -48,7 +50,6 @@ import {SettingsProvider} from './modules/settings/settings.provider';
|
||||
import {StorageModule} from './modules/storage/storage.module';
|
||||
import {ThingTranslateModule} from './translation/thing-translate.module';
|
||||
import {fakeBackendProvider} from './_helpers/fake-backend.interceptor';
|
||||
|
||||
import {initLogger} from './_helpers/ts-logger';
|
||||
|
||||
registerLocaleData(localeDe);
|
||||
@@ -105,6 +106,7 @@ export function createTranslateLoader(http: HttpClient) {
|
||||
const providers: Provider[] = [
|
||||
StatusBar,
|
||||
SplashScreen,
|
||||
Diagnostic,
|
||||
{
|
||||
provide: RouteReuseStrategy,
|
||||
useClass: IonicRouteStrategy,
|
||||
@@ -134,6 +136,7 @@ const providers: Provider[] = [
|
||||
ConfigModule,
|
||||
DataModule,
|
||||
IonicModule.forRoot(),
|
||||
MapModule,
|
||||
MenuModule,
|
||||
NewsModule,
|
||||
SettingsModule,
|
||||
|
||||
@@ -36,44 +36,46 @@ import {DataProvider} from './data.provider';
|
||||
import {DataDetailContentComponent} from './detail/data-detail-content.component';
|
||||
import {DataDetailComponent} from './detail/data-detail.component';
|
||||
import {AddressDetailComponent} from './elements/address-detail.component';
|
||||
import {LongInlineTextComponent} from './elements/long-inline-text.component';
|
||||
import {OffersDetailComponent} from './elements/offers-detail.component';
|
||||
import {OffersInListComponent} from './elements/offers-in-list.component';
|
||||
import {OriginDetailComponent} from './elements/origin-detail.component';
|
||||
import {OriginInListComponent} from './elements/origin-in-list.component';
|
||||
import {SimpleCardComponent} from './elements/simple-card.component';
|
||||
import {SkeletonListItemComponent} from './elements/skeleton-list-item.component';
|
||||
import {SkeletonSegmentComponent} from './elements/skeleton-segment-button.component';
|
||||
import {SkeletonSimpleCardComponent} from './elements/skeleton-simple-card.component';
|
||||
import {DataListItemComponent} from './list/data-list-item.component';
|
||||
import {DataListComponent} from './list/data-list.component';
|
||||
import {FoodDataListComponent} from './list/food-data-list.component';
|
||||
import {SearchPageComponent} from './list/search-page.component';
|
||||
import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
|
||||
import {ArticleDetailContentComponent} from './types/article/article-detail-content.component';
|
||||
import {ArticleListItemComponent} from './types/article/article-list-item.component';
|
||||
import {CatalogDetailContentComponent} from './types/catalog/catalog-detail-content.component';
|
||||
import {CatalogListItemComponent} from './types/catalog/catalog-list-item.component';
|
||||
import {DateSeriesDetailContentComponent} from './types/date-series/date-series-detail-content.component';
|
||||
import {DateSeriesListItemComponent} from './types/date-series/date-series-list-item.component';
|
||||
import {DishDetailContentComponent} from './types/dish/dish-detail-content.component';
|
||||
import {DishListItemComponent} from './types/dish/dish-list-item.component';
|
||||
import {EventDetailContentComponent} from './types/event/event-detail-content.component';
|
||||
import {EventListItemComponent} from './types/event/event-list-item.component';
|
||||
import {FavoriteDetailContentComponent} from './types/favorite/favorite-detail-content.component';
|
||||
import {FavoriteListItemComponent} from './types/favorite/favorite-list-item.component';
|
||||
import {MessageDetailContentComponent} from './types/message/message-detail-content.component';
|
||||
import {MessageListItemComponent} from './types/message/message-list-item.component';
|
||||
import {OrganizationDetailContentComponent} from './types/organization/organization-detail-content.component';
|
||||
import {OrganizationListItemComponent} from './types/organization/organization-list-item.component';
|
||||
import {PersonDetailContentComponent} from './types/person/person-detail-content.component';
|
||||
import {PersonListItemComponent} from './types/person/person-list-item.component';
|
||||
import {PlaceDetailContentComponent} from './types/place/place-detail-content.component';
|
||||
import {PlaceListItemComponent} from './types/place/place-list-item.component';
|
||||
import {PlaceMensaDetailComponent} from './types/place/special/mensa/place-mensa-detail.component';
|
||||
import {SemesterDetailContentComponent} from './types/semester/semester-detail-content.component';
|
||||
import {SemesterListItemComponent} from './types/semester/semester-list-item.component';
|
||||
import {VideoDetailContentComponent} from './types/video/video-detail-content.component';
|
||||
import {MapWidgetComponent} from '../map/widget/map-widget.component';
|
||||
import {LeafletModule} from '@asymmetrik/ngx-leaflet';
|
||||
import {ArticleListItemComponent} from './types/article/article-list-item.component';
|
||||
import {SkeletonSimpleCardComponent} from './elements/skeleton-simple-card.component';
|
||||
import {CatalogListItemComponent} from './types/catalog/catalog-list-item.component';
|
||||
import {DataListItemComponent} from './list/data-list-item.component';
|
||||
import {DateSeriesListItemComponent} from './types/date-series/date-series-list-item.component';
|
||||
import {DishListItemComponent} from './types/dish/dish-list-item.component';
|
||||
import {FavoriteListItemComponent} from './types/favorite/favorite-list-item.component';
|
||||
import {LongInlineTextComponent} from './elements/long-inline-text.component';
|
||||
import {MessageListItemComponent} from './types/message/message-list-item.component';
|
||||
import {OrganizationListItemComponent} from './types/organization/organization-list-item.component';
|
||||
import {PersonListItemComponent} from './types/person/person-list-item.component';
|
||||
import {SemesterListItemComponent} from './types/semester/semester-list-item.component';
|
||||
import {SkeletonListItemComponent} from './elements/skeleton-list-item.component';
|
||||
import {SkeletonSegmentComponent} from './elements/skeleton-segment-button.component';
|
||||
import {VideoListItemComponent} from './types/video/video-list-item.component';
|
||||
|
||||
/**
|
||||
@@ -81,17 +83,19 @@ import {VideoListItemComponent} from './types/video/video-list-item.component';
|
||||
*/
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ActionChipListComponent,
|
||||
AddEventActionChipComponent,
|
||||
AddEventPopoverComponent,
|
||||
OffersDetailComponent,
|
||||
OffersInListComponent,
|
||||
AddressDetailComponent,
|
||||
ArticleDetailContentComponent,
|
||||
ArticleListItemComponent,
|
||||
SimpleCardComponent,
|
||||
SkeletonSimpleCardComponent,
|
||||
CatalogDetailContentComponent,
|
||||
CatalogListItemComponent,
|
||||
DataDetailComponent,
|
||||
DataDetailContentComponent,
|
||||
DataIconPipe,
|
||||
FoodDataListComponent,
|
||||
DataListComponent,
|
||||
DataListItemComponent,
|
||||
DateSeriesDetailContentComponent,
|
||||
@@ -102,13 +106,10 @@ import {VideoListItemComponent} from './types/video/video-list-item.component';
|
||||
EventListItemComponent,
|
||||
FavoriteDetailContentComponent,
|
||||
FavoriteListItemComponent,
|
||||
FoodDataListComponent,
|
||||
LocateActionChipComponent,
|
||||
LongInlineTextComponent,
|
||||
MapWidgetComponent,
|
||||
MessageDetailContentComponent,
|
||||
MessageListItemComponent,
|
||||
OffersDetailComponent,
|
||||
OffersInListComponent,
|
||||
OrganizationDetailContentComponent,
|
||||
OrganizationListItemComponent,
|
||||
OriginDetailComponent,
|
||||
@@ -121,20 +122,22 @@ import {VideoListItemComponent} from './types/video/video-list-item.component';
|
||||
SearchPageComponent,
|
||||
SemesterDetailContentComponent,
|
||||
SemesterListItemComponent,
|
||||
SimpleCardComponent,
|
||||
SkeletonListItemComponent,
|
||||
SkeletonSegmentComponent,
|
||||
SkeletonSimpleCardComponent,
|
||||
VideoDetailContentComponent,
|
||||
VideoListItemComponent,
|
||||
DataIconPipe,
|
||||
ActionChipListComponent,
|
||||
AddEventActionChipComponent,
|
||||
LocateActionChipComponent,
|
||||
],
|
||||
entryComponents: [DataListComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
DataRoutingModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
IonicModule.forRoot(),
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
DataRoutingModule,
|
||||
HttpClientModule,
|
||||
LeafletModule,
|
||||
MarkdownModule.forRoot(),
|
||||
MenuModule,
|
||||
MomentModule.forRoot({
|
||||
@@ -148,5 +151,15 @@ import {VideoListItemComponent} from './types/video/video-list-item.component';
|
||||
ThingTranslateModule.forChild(),
|
||||
],
|
||||
providers: [DataProvider, DataFacetsProvider, Network, StAppsWebHttpClient],
|
||||
exports: [
|
||||
DataListComponent,
|
||||
DataListItemComponent,
|
||||
DataDetailComponent,
|
||||
SkeletonSimpleCardComponent,
|
||||
SkeletonListItemComponent,
|
||||
DataIconPipe,
|
||||
PlaceListItemComponent,
|
||||
DataDetailContentComponent,
|
||||
],
|
||||
})
|
||||
export class DataModule {}
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
<ion-item class="ion-text-wrap" lines="inset">
|
||||
<ion-thumbnail slot="start" class="ion-margin-end">
|
||||
<ion-icon color="medium" [attr.name]="item.type | dataIcon"></ion-icon>
|
||||
</ion-thumbnail>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
<div class="ion-text-wrap">
|
||||
<h2 class="name">{{ item.name }}</h2>
|
||||
<ion-note>{{ item.type }}</ion-note>
|
||||
</div>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-item>
|
||||
<stapps-simple-card
|
||||
*ngIf="item.description"
|
||||
[title]="'Description'"
|
||||
|
||||
@@ -38,24 +38,6 @@
|
||||
<stapps-skeleton-simple-card></stapps-skeleton-simple-card>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchDefault>
|
||||
<ion-item class="ion-text-wrap" lines="inset">
|
||||
<ion-thumbnail slot="start" class="ion-margin-end">
|
||||
<ion-icon
|
||||
color="medium"
|
||||
[attr.name]="item.type | dataIcon"
|
||||
></ion-icon>
|
||||
</ion-thumbnail>
|
||||
<ion-grid *ngSwitchDefault>
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
<div class="ion-text-wrap">
|
||||
<h2 class="name">{{ item.name }}</h2>
|
||||
<ion-note>{{ item.type }}</ion-note>
|
||||
</div>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-item>
|
||||
<stapps-data-detail-content [item]="item"></stapps-data-detail-content>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@@ -15,4 +15,18 @@
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
ion-note {
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
li {
|
||||
list-style-type: none;
|
||||
display: inline;
|
||||
}
|
||||
li:not(:first-child):before {
|
||||
content: " • ";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,23 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {DataListComponent} from './data-list.component';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||
import {ConfigProvider} from '../../config/config.provider';
|
||||
|
||||
describe('DataListComponent', () => {
|
||||
let component: DataListComponent;
|
||||
let fixture: ComponentFixture<DataListComponent>;
|
||||
let configProviderMock: jasmine.SpyObj<ConfigProvider>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
configProviderMock = jasmine.createSpyObj('ConfigProvider', {
|
||||
getValue: () => Promise.resolve({lat: 123, lng: 123}),
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [DataListComponent],
|
||||
imports: [TranslateModule.forRoot()],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
providers: [{provide: ConfigProvider, useValue: configProviderMock}],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<stapps-context></stapps-context>
|
||||
<stapps-context contentId="data-list"></stapps-context>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
@@ -13,7 +13,9 @@
|
||||
<ion-searchbar
|
||||
(ngModelChange)="searchStringChanged($event)"
|
||||
[(ngModel)]="queryText"
|
||||
></ion-searchbar>
|
||||
showClearButton="always"
|
||||
>
|
||||
</ion-searchbar>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import {DataProvider} from '../../data.provider';
|
||||
*/
|
||||
@Component({
|
||||
providers: [DataProvider],
|
||||
styleUrls: ['place-detail-content.scss'],
|
||||
selector: 'stapps-place-detail-content',
|
||||
templateUrl: 'place-detail-content.html',
|
||||
})
|
||||
@@ -41,7 +42,9 @@ export class PlaceDetailContentComponent {
|
||||
*
|
||||
* @param item TODO
|
||||
*/
|
||||
// tslint:disable-next-line:completed-docs prefer-function-over-method
|
||||
hasCategories(item: SCThings): item is SCThings & {categories: string[]} {
|
||||
// tslint:disable-next-line:completed-docs
|
||||
return typeof (item as {categories: string[]}).categories !== 'undefined';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<stapps-place-mensa-detail-content
|
||||
[item]="item"
|
||||
[language]="language"
|
||||
*ngIf="isMensaThing(item)"
|
||||
></stapps-place-mensa-detail-content>
|
||||
<ng-container *ngIf="item.type !== 'floor'">
|
||||
@@ -26,8 +25,9 @@
|
||||
}}</a>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
<stapps-address-detail
|
||||
*ngIf="item.inPlace && item.inPlace.address"
|
||||
[address]="item.inPlace.address"
|
||||
></stapps-address-detail>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="item.type !== 'floor'">
|
||||
<ion-card *ngIf="item.geo" class="map-widget">
|
||||
<stapps-map-widget [place]="item" expandable="true"></stapps-map-widget>
|
||||
</ion-card>
|
||||
</ng-container>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
ion-card.map-widget {
|
||||
height: 300px;
|
||||
width: auto;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2019 StApps
|
||||
* Copyright (C) 2019-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.
|
||||
@@ -14,18 +14,55 @@
|
||||
*/
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {SCBuilding, SCFloor, SCPointOfInterest, SCRoom} from '@openstapps/core';
|
||||
import {DataListItemComponent} from '../../list/data-list-item.component';
|
||||
import {PositionService} from '../../../map/position.service';
|
||||
|
||||
type placeTypes = (SCBuilding | SCRoom | SCPointOfInterest | SCFloor) & {
|
||||
distance?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* Provide information if a place is a floor
|
||||
*
|
||||
* @param place A place to check
|
||||
*/
|
||||
function isSCFloor(place: placeTypes): place is SCFloor {
|
||||
return place.type === 'floor';
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a place as a list item
|
||||
*/
|
||||
@Component({
|
||||
selector: 'stapps-place-list-item',
|
||||
templateUrl: 'place-list-item.html',
|
||||
})
|
||||
export class PlaceListItemComponent extends DataListItemComponent {
|
||||
export class PlaceListItemComponent {
|
||||
/**
|
||||
* TODO
|
||||
* Item getter
|
||||
*/
|
||||
@Input() item: SCBuilding | SCRoom | SCPointOfInterest | SCFloor;
|
||||
get item(): placeTypes {
|
||||
return this._item;
|
||||
}
|
||||
|
||||
/**
|
||||
* An item to show (setter is used as there were issues assigning the distance to the right place in a list)
|
||||
*/
|
||||
@Input() set item(item: placeTypes) {
|
||||
this._item = item;
|
||||
if (!isSCFloor(item)) {
|
||||
this.distance = this.positionService.getDistance(item.geo.point);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An item to show
|
||||
*/
|
||||
private _item: placeTypes;
|
||||
|
||||
/**
|
||||
* Distance in meters
|
||||
*/
|
||||
distance?: number;
|
||||
|
||||
constructor(private positionService: PositionService) {}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,15 @@
|
||||
<p *ngIf="item.description">
|
||||
{{ 'description' | thingTranslate: item }}
|
||||
</p>
|
||||
<ion-note>{{ 'type' | thingTranslate: item }} </ion-note>
|
||||
<ion-note>
|
||||
<ul>
|
||||
<li>{{ 'type' | thingTranslate: item }}</li>
|
||||
<li *ngIf="distance">
|
||||
<ion-icon name="walk"></ion-icon
|
||||
>{{ distance | numberLocalized: '1.0-0' }} m
|
||||
</li>
|
||||
</ul>
|
||||
</ion-note>
|
||||
</div>
|
||||
</ion-col>
|
||||
<ion-col width-20 text-right *ngIf="item.type !== 'building'">
|
||||
|
||||
28
src/app/modules/map/item/map-item.component.html
Normal file
28
src/app/modules/map/item/map-item.component.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<ion-card>
|
||||
<ion-card-header>
|
||||
<stapps-data-list-item
|
||||
[item]="item"
|
||||
(click)="showMore()"
|
||||
></stapps-data-list-item>
|
||||
<stapps-skeleton-list-item *ngIf="!item"></stapps-skeleton-list-item>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col size="7">
|
||||
<ion-note>
|
||||
<span *ngIf="item.address as address">
|
||||
<span *ngIf="item.inPlace">{{ item.inPlace.name }},</span>
|
||||
{{ address.streetAddress }}, {{ address.addressLocality }}
|
||||
</span>
|
||||
</ion-note>
|
||||
</ion-col>
|
||||
<ion-col size="5">
|
||||
<ion-button size="small" (click)="showMore()"
|
||||
>More <ion-icon name="information-circle"></ion-icon
|
||||
></ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
8
src/app/modules/map/item/map-item.component.scss
Normal file
8
src/app/modules/map/item/map-item.component.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
ion-col:nth-child(2) {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
ion-button {
|
||||
margin-top: auto;
|
||||
}
|
||||
}
|
||||
40
src/app/modules/map/item/map-item.component.ts
Normal file
40
src/app/modules/map/item/map-item.component.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
@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;
|
||||
|
||||
/**
|
||||
* An item to show
|
||||
*/
|
||||
@Output() showDetails = new EventEmitter<SCPlace>();
|
||||
|
||||
/**
|
||||
* Emit event to signalize to show more information
|
||||
*/
|
||||
showMore() {
|
||||
this.showDetails.emit(this.item);
|
||||
}
|
||||
}
|
||||
97
src/app/modules/map/map.module.ts
Normal file
97
src/app/modules/map/map.module.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (C) 2019-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 {CommonModule} from '@angular/common';
|
||||
import {APP_INITIALIZER, NgModule} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {RouterModule, Routes} from '@angular/router';
|
||||
import {LeafletModule} from '@asymmetrik/ngx-leaflet';
|
||||
import {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster';
|
||||
import {Geolocation} from '@ionic-native/geolocation/ngx';
|
||||
import {IonicModule} from '@ionic/angular';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {Polygon} from 'geojson';
|
||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {DataFacetsProvider} from '../data/data-facets.provider';
|
||||
import {DataModule} from '../data/data.module';
|
||||
import {DataProvider} from '../data/data.provider';
|
||||
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';
|
||||
|
||||
/**
|
||||
* Initializes the default area to show in advance (before components are initialized)
|
||||
*
|
||||
* @param configProvider An instance of the ConfigProvider to read the campus polygon from
|
||||
* @param mapProvider An instance of the MapProvider to set the default polygon (area to show on the map)
|
||||
*/
|
||||
export function initMapConfigFactory(
|
||||
configProvider: ConfigProvider,
|
||||
mapProvider: MapProvider,
|
||||
) {
|
||||
return async () => {
|
||||
mapProvider.defaultPolygon = (await configProvider.getValue(
|
||||
'campusPolygon',
|
||||
)) as Polygon;
|
||||
};
|
||||
}
|
||||
|
||||
const mapRoutes: Routes = [
|
||||
{path: 'map', component: MapPageComponent},
|
||||
{path: 'map/:uid', component: MapPageComponent},
|
||||
];
|
||||
|
||||
/**
|
||||
* Module containing map related stuff
|
||||
*/
|
||||
@NgModule({
|
||||
declarations: [
|
||||
MapPageComponent,
|
||||
MapListModalComponent,
|
||||
MapSingleModalComponent,
|
||||
MapItemComponent,
|
||||
],
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule.forRoot(),
|
||||
LeafletModule,
|
||||
LeafletMarkerClusterModule,
|
||||
RouterModule.forChild(mapRoutes),
|
||||
TranslateModule.forChild(),
|
||||
MenuModule,
|
||||
DataModule,
|
||||
FormsModule,
|
||||
ThingTranslateModule,
|
||||
],
|
||||
providers: [
|
||||
Geolocation,
|
||||
MapProvider,
|
||||
DataProvider,
|
||||
DataFacetsProvider,
|
||||
StAppsWebHttpClient,
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
deps: [ConfigProvider, MapProvider],
|
||||
useFactory: initMapConfigFactory,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class MapModule {}
|
||||
48
src/app/modules/map/map.provider.spec.ts
Normal file
48
src/app/modules/map/map.provider.spec.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2019-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 {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {Geolocation} from '@ionic-native/geolocation/ngx';
|
||||
import {Diagnostic} from '@ionic-native/diagnostic/ngx';
|
||||
import {MapProvider} from './map.provider';
|
||||
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
|
||||
import {HttpClientModule} from '@angular/common/http';
|
||||
import {StorageProvider} from '../storage/storage.provider';
|
||||
import {MapModule} from './map.module';
|
||||
import {StorageModule} from '../storage/storage.module';
|
||||
import {LoggerConfig, LoggerModule, NGXLogger} from 'ngx-logger';
|
||||
|
||||
describe('MapProvider', () => {
|
||||
let provider: MapProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MapModule, HttpClientModule, StorageModule, LoggerModule],
|
||||
providers: [
|
||||
Geolocation,
|
||||
Diagnostic,
|
||||
StAppsWebHttpClient,
|
||||
StorageProvider,
|
||||
NGXLogger,
|
||||
LoggerConfig,
|
||||
],
|
||||
});
|
||||
provider = TestBed.inject(MapProvider);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(provider).toBeTruthy();
|
||||
});
|
||||
});
|
||||
232
src/app/modules/map/map.provider.ts
Normal file
232
src/app/modules/map/map.provider.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Copyright (C) 2019-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 {ElementRef, Injectable} from '@angular/core';
|
||||
import {
|
||||
SCSearchFilter,
|
||||
SCSearchQuery,
|
||||
SCSearchResponse,
|
||||
SCThingType,
|
||||
SCUuid,
|
||||
} from '@openstapps/core';
|
||||
import {Point, Polygon} from 'geojson';
|
||||
import {divIcon, geoJSON, icon, LatLng, Map, marker, Marker} from 'leaflet';
|
||||
import {DataProvider} from '../data/data.provider';
|
||||
import {MapPosition, PositionService} from './position.service';
|
||||
import Timeout = NodeJS.Timeout;
|
||||
|
||||
/**
|
||||
* Provides methods for presenting the map
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MapProvider {
|
||||
/**
|
||||
* Area to show when the map is initialized (shown for the first time)
|
||||
*/
|
||||
defaultPolygon: Polygon;
|
||||
|
||||
/**
|
||||
* Provide a point marker for a leaflet map
|
||||
*
|
||||
* @param point Point to get marker for
|
||||
*/
|
||||
static getPointMarker(point: Point) {
|
||||
return marker(geoJSON(point).getBounds().getCenter(), {
|
||||
icon: icon({
|
||||
// tslint:disable-next-line:no-magic-numbers
|
||||
iconAnchor: [13, 41],
|
||||
// tslint:disable-next-line:no-magic-numbers
|
||||
iconSize: [25, 41],
|
||||
iconUrl: '../assets/marker-icon.png',
|
||||
shadowUrl: '../assets/marker-shadow.png',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a position marker for a leaflet map
|
||||
*
|
||||
* @param position Current position
|
||||
* @param className CSS class name
|
||||
* @param iconSize Size of the position icon
|
||||
*/
|
||||
static getPositionMarker(
|
||||
position: MapPosition,
|
||||
className: string,
|
||||
iconSize: number,
|
||||
) {
|
||||
return new Marker(new LatLng(position.latitude, position.longitude), {
|
||||
icon: divIcon({
|
||||
className: className,
|
||||
html:
|
||||
typeof position.heading !== 'undefined'
|
||||
? `<ion-icon name="navigate-straight"
|
||||
style="transform-origin: center; transform: rotate(${position.heading}deg);">
|
||||
</ion-icon>`
|
||||
: '<ion-icon name="locate"></ion-icon>',
|
||||
iconSize: [iconSize, iconSize],
|
||||
}),
|
||||
zIndexOffset: 1000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes the issue of missing tiles when map renders before its container element
|
||||
*
|
||||
* @param map The initialized map
|
||||
* @param element The element containing the map
|
||||
* @param interval Interval to clear when map's appearance is corrected
|
||||
*/
|
||||
static invalidateWhenRendered = (
|
||||
map: Map,
|
||||
element: ElementRef,
|
||||
interval: Timeout,
|
||||
) => {
|
||||
if (element.nativeElement.offsetWidth === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// map's container is ready
|
||||
map.invalidateSize();
|
||||
// stop repeating when it's rendered and invalidateSize done
|
||||
clearInterval(interval);
|
||||
};
|
||||
|
||||
constructor(
|
||||
private dataProvider: DataProvider,
|
||||
private positionService: PositionService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Provide the specific place by its UID
|
||||
*
|
||||
* @param uid UUID of the place to look for
|
||||
*/
|
||||
async searchPlace(uid: SCUuid): Promise<SCSearchResponse> {
|
||||
const uidFilter: SCSearchFilter = {
|
||||
arguments: {
|
||||
field: 'uid',
|
||||
value: uid,
|
||||
},
|
||||
type: 'value',
|
||||
};
|
||||
|
||||
return this.dataProvider.search({filter: uidFilter});
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide places (buildings and canteens)
|
||||
*
|
||||
* @param contextFilter Additional contextual filter (e.g. from the context menu)
|
||||
* @param queryText Query (text) of the search query
|
||||
*/
|
||||
async searchPlaces(
|
||||
contextFilter?: SCSearchFilter,
|
||||
queryText?: string,
|
||||
): Promise<SCSearchResponse> {
|
||||
const buildingFilter: SCSearchFilter = {
|
||||
arguments: {
|
||||
field: 'type',
|
||||
value: SCThingType.Building,
|
||||
},
|
||||
type: 'value',
|
||||
};
|
||||
|
||||
const mensaFilter: SCSearchFilter = {
|
||||
arguments: {
|
||||
filters: [
|
||||
{
|
||||
arguments: {
|
||||
field: 'categories',
|
||||
value: 'canteen',
|
||||
},
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
arguments: {
|
||||
field: 'categories',
|
||||
value: 'student canteen',
|
||||
},
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
arguments: {
|
||||
field: 'categories',
|
||||
value: 'cafe',
|
||||
},
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
arguments: {
|
||||
field: 'categories',
|
||||
value: 'restaurant',
|
||||
},
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
operation: 'or',
|
||||
},
|
||||
type: 'boolean',
|
||||
};
|
||||
|
||||
// initial filter for the places
|
||||
const baseFilter: SCSearchFilter = {
|
||||
arguments: {
|
||||
operation: 'or',
|
||||
filters: [buildingFilter, mensaFilter],
|
||||
},
|
||||
type: 'boolean',
|
||||
};
|
||||
|
||||
let filter = baseFilter;
|
||||
|
||||
if (typeof contextFilter !== 'undefined') {
|
||||
filter = {
|
||||
arguments: {
|
||||
operation: 'and',
|
||||
filters: [baseFilter, contextFilter],
|
||||
},
|
||||
type: 'boolean',
|
||||
};
|
||||
}
|
||||
|
||||
const query: SCSearchQuery = {
|
||||
filter,
|
||||
};
|
||||
|
||||
if (queryText && queryText.length > 0) {
|
||||
query.query = queryText;
|
||||
}
|
||||
|
||||
if (this.positionService.position) {
|
||||
query.sort = [
|
||||
{
|
||||
type: 'distance',
|
||||
order: 'asc',
|
||||
arguments: {
|
||||
field: 'geo.point.coordinates',
|
||||
position: [
|
||||
this.positionService.position.longitude,
|
||||
this.positionService.position.latitude,
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return this.dataProvider.search(query);
|
||||
}
|
||||
}
|
||||
472
src/app/modules/map/page/map-page.component.ts
Normal file
472
src/app/modules/map/page/map-page.component.ts
Normal file
@@ -0,0 +1,472 @@
|
||||
/*
|
||||
* Copyright (C) 2019-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 {Location} from '@angular/common';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {AlertController, ModalController, Platform} from '@ionic/angular';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {
|
||||
SCBuilding,
|
||||
SCPlace,
|
||||
SCRoom,
|
||||
SCSearchFilter,
|
||||
SCUuid,
|
||||
} from '@openstapps/core';
|
||||
import {
|
||||
featureGroup,
|
||||
geoJSON,
|
||||
LatLng,
|
||||
Layer,
|
||||
Map,
|
||||
MapOptions,
|
||||
Marker,
|
||||
tileLayer,
|
||||
} from 'leaflet';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {DataRoutingService} from '../../data/data-routing.service';
|
||||
import {ContextMenuService} from '../../menu/context/context-menu.service';
|
||||
import {MapProvider} from '../map.provider';
|
||||
import {
|
||||
LocationStatus,
|
||||
MapPosition,
|
||||
PositionService,
|
||||
} from '../position.service';
|
||||
import {MapListModalComponent} from './modals/map-list-modal.component';
|
||||
import {MapSingleModalComponent} from './modals/map-single-modal.component';
|
||||
import Timeout = NodeJS.Timeout;
|
||||
|
||||
/**
|
||||
* The main page of the map
|
||||
*/
|
||||
@Component({
|
||||
styleUrls: ['./map-page.scss'],
|
||||
templateUrl: './map-page.html',
|
||||
providers: [ContextMenuService],
|
||||
})
|
||||
export class MapPageComponent {
|
||||
/**
|
||||
* Default map zoom level
|
||||
*/
|
||||
DEFAULT_ZOOM = 16;
|
||||
|
||||
/**
|
||||
* Distance to the shown place
|
||||
*/
|
||||
distance?: number;
|
||||
|
||||
/**
|
||||
* Api query filter
|
||||
*/
|
||||
filterQuery?: SCSearchFilter;
|
||||
|
||||
/**
|
||||
* Places to show
|
||||
*/
|
||||
items: SCPlace[] = [];
|
||||
|
||||
/**
|
||||
* Leaflet (map) layers to show items on (not the position)
|
||||
*/
|
||||
layers: Layer[] = [];
|
||||
|
||||
/**
|
||||
* Location settings on the user's device
|
||||
*/
|
||||
locationStatus: LocationStatus = {enabled: undefined, allowed: undefined};
|
||||
|
||||
/**
|
||||
* The leaflet map
|
||||
*/
|
||||
map: Map;
|
||||
|
||||
/**
|
||||
* Container element of the map
|
||||
*/
|
||||
@ViewChild('mapContainer') mapContainer: ElementRef;
|
||||
|
||||
/**
|
||||
* Map layers to show as marker clusters
|
||||
*/
|
||||
markerClusterData: Layer[] = [];
|
||||
|
||||
/**
|
||||
* Options how to show the marker clusters
|
||||
*/
|
||||
markerClusterOptions = {
|
||||
// don't show rectangles containing the markers in a cluster
|
||||
showCoverageOnHover: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Maximal map zoom level
|
||||
*/
|
||||
MAX_ZOOM = 18;
|
||||
|
||||
/**
|
||||
* Modal for additional information on places or for a list of places
|
||||
*/
|
||||
modal: HTMLIonModalElement;
|
||||
|
||||
/**
|
||||
* Options of the leaflet map
|
||||
*/
|
||||
options: MapOptions;
|
||||
|
||||
/**
|
||||
* Position of the user on the map
|
||||
*/
|
||||
position: MapPosition | null;
|
||||
|
||||
/**
|
||||
* Marker showing the position of the user on the map
|
||||
*/
|
||||
positionMarker: Marker;
|
||||
|
||||
/**
|
||||
* Search value from search bar
|
||||
*/
|
||||
queryText: string;
|
||||
|
||||
/**
|
||||
* Subscriptions used by the page
|
||||
*/
|
||||
subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
private translateService: TranslateService,
|
||||
private router: Router,
|
||||
private mapProvider: MapProvider,
|
||||
public location: Location,
|
||||
private ref: ChangeDetectorRef,
|
||||
private readonly contextMenuService: ContextMenuService,
|
||||
private alertController: AlertController,
|
||||
private route: ActivatedRoute,
|
||||
private modalController: ModalController,
|
||||
private dataRoutingService: DataRoutingService,
|
||||
private positionService: PositionService,
|
||||
private platform: Platform,
|
||||
) {
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate to coordinates
|
||||
*
|
||||
* @param latLng Coordinates to animate to
|
||||
*/
|
||||
private focus(latLng?: LatLng) {
|
||||
if (typeof latLng !== 'undefined') {
|
||||
this.map.flyTo(latLng, this.MAX_ZOOM);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add places to the map
|
||||
*
|
||||
* @param items Places to add to the map
|
||||
* @param clean Remove everything from the map first
|
||||
* @param focus Animate to the item(s)
|
||||
*/
|
||||
addToMap(items: SCPlace[], clean = false, focus = false) {
|
||||
if (clean) {
|
||||
this.removeAll();
|
||||
}
|
||||
const addSCPlace = (place: SCPlace): Layer | Marker => {
|
||||
if (typeof place.geo.polygon !== 'undefined') {
|
||||
const polygonLayer = geoJSON(place.geo.polygon, {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
bubblingMouseEvents: false,
|
||||
}).getLayers()[0];
|
||||
|
||||
return polygonLayer.on('click', this.showItem.bind(this, place.uid));
|
||||
}
|
||||
|
||||
const markerLayer = MapProvider.getPointMarker(place.geo.point);
|
||||
|
||||
return markerLayer.on('click', this.showItem.bind(this, place.uid));
|
||||
};
|
||||
|
||||
items.map(thing => {
|
||||
// IMPORTANT: change this to support inPlace.geo when there is a need to show floors (the building of the floor)
|
||||
if (typeof thing.geo !== 'undefined') {
|
||||
this.layers.push(addSCPlace(thing as SCPlace));
|
||||
}
|
||||
});
|
||||
|
||||
this.markerClusterData = this.layers;
|
||||
|
||||
if (!focus || this.items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.items.length === 1) {
|
||||
this.focus(geoJSON(this.items[0].geo.point).getBounds().getCenter());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const groupedLayers = featureGroup(this.layers);
|
||||
this.map.flyToBounds(groupedLayers.getBounds());
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches items with set query configuration
|
||||
*
|
||||
* @param animate Should the fly animation be used
|
||||
*/
|
||||
async fetchAndUpdateItems(animate?: boolean): Promise<void> {
|
||||
try {
|
||||
const result = await this.mapProvider.searchPlaces(
|
||||
this.filterQuery,
|
||||
this.queryText,
|
||||
);
|
||||
// override items with results
|
||||
this.items = result.data as SCPlace[];
|
||||
this.addToMap(result.data as Array<SCBuilding | SCRoom>, true, animate);
|
||||
// update filter options if result contains facets
|
||||
if (typeof result.facets !== 'undefined') {
|
||||
this.contextMenuService.updateContextFilter(result.facets);
|
||||
}
|
||||
} catch (error) {
|
||||
const alert: HTMLIonAlertElement = await this.alertController.create({
|
||||
buttons: ['Dismiss'],
|
||||
header: 'Error',
|
||||
subHeader: error.message,
|
||||
});
|
||||
|
||||
await alert.present();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to needed observables and get the location status when user is entering the page
|
||||
*/
|
||||
async ionViewWillEnter() {
|
||||
this.subscriptions.push(
|
||||
this.dataRoutingService.itemSelectListener().subscribe(async item => {
|
||||
// in case the list item is clicked
|
||||
if (this.items.length > 1) {
|
||||
await this.modal.dismiss();
|
||||
await this.showItem(item.uid);
|
||||
}
|
||||
}),
|
||||
this.positionService.watchCurrentLocation({maximumAge: 3000}).subscribe({
|
||||
next: (position: MapPosition) => {
|
||||
this.position = position;
|
||||
this.positionMarker = MapProvider.getPositionMarker(
|
||||
position,
|
||||
'stapps-device-location',
|
||||
30,
|
||||
);
|
||||
},
|
||||
error: error => {
|
||||
if (error.code === 1) {
|
||||
this.locationStatus.allowed = false;
|
||||
}
|
||||
// eslint-disable-next-line unicorn/no-null
|
||||
this.position = null;
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// get detailed location status (diagnostics only supports devices)
|
||||
if (this.platform.is('cordova')) {
|
||||
this.locationStatus = await this.positionService.getLocationStatus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from all subscriptions when user leaves page
|
||||
*/
|
||||
ionViewWillLeave() {
|
||||
for (const sub of this.subscriptions) {
|
||||
sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* What happens when the leaflet map is ready (note: doesn't mean that tiles are loaded)
|
||||
*/
|
||||
async onMapReady(map: Map) {
|
||||
this.map = map;
|
||||
const interval: Timeout = setInterval(() =>
|
||||
MapProvider.invalidateWhenRendered(map, this.mapContainer, interval),
|
||||
);
|
||||
|
||||
const uid = this.route.snapshot.paramMap.get('uid');
|
||||
const response = await (uid !== null
|
||||
? this.mapProvider.searchPlace(uid)
|
||||
: this.mapProvider.searchPlaces());
|
||||
|
||||
if (response.data.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.items = response.data as SCBuilding[];
|
||||
this.addToMap(this.items, true, uid !== null);
|
||||
this.contextMenuService.updateContextFilter(response.facets);
|
||||
|
||||
this.subscriptions.push(
|
||||
this.contextMenuService.filterQueryChanged$.subscribe(query => {
|
||||
this.filterQuery = query;
|
||||
this.fetchAndUpdateItems(true);
|
||||
}),
|
||||
);
|
||||
|
||||
this.distance = this.positionService.getDistance(this.items[0].geo.point);
|
||||
}
|
||||
|
||||
/**
|
||||
* What happens when position button is clicked
|
||||
*/
|
||||
async onPositionClick() {
|
||||
if (this.position) {
|
||||
this.focus(this.positionMarker.getLatLng());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.translateService
|
||||
.get(['map.page.geolocation', 'app.errors.UNKNOWN'])
|
||||
.subscribe(async translations => {
|
||||
const [location, unknownError] = [
|
||||
translations['map.page.geolocation'],
|
||||
translations['app.errors.UNKNOWN'],
|
||||
];
|
||||
const {enabled, allowed} = this.locationStatus;
|
||||
await (
|
||||
await this.alertController.create({
|
||||
header: location.TITLE,
|
||||
subHeader: location.SUBTITLE,
|
||||
message: `${
|
||||
enabled === false
|
||||
? location.NOT_ENABLED
|
||||
: allowed === false
|
||||
? location.NOT_ALLOWED
|
||||
: unknownError
|
||||
}.`,
|
||||
buttons: ['OK'],
|
||||
})
|
||||
).present();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all of the layers
|
||||
*/
|
||||
removeAll() {
|
||||
for (const layer of this.layers) {
|
||||
this.map.removeLayer(layer);
|
||||
}
|
||||
|
||||
this.layers = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.ref.detectChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search event of search bar
|
||||
*/
|
||||
searchStringChanged(queryText?: string) {
|
||||
this.queryText = queryText || '';
|
||||
void this.fetchAndUpdateItems(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an single place
|
||||
*
|
||||
* @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());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a single place
|
||||
*/
|
||||
async showItemModal(item: SCPlace) {
|
||||
const placeWithoutGeo = {...item, geo: undefined};
|
||||
this.modal = await this.modalController.create({
|
||||
component: MapSingleModalComponent,
|
||||
swipeToClose: true,
|
||||
componentProps: {
|
||||
item: placeWithoutGeo,
|
||||
dismissAction: () => {
|
||||
this.modal.dismiss();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await this.modal.present();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the list of shown places
|
||||
*/
|
||||
async showListModal() {
|
||||
this.modal = await this.modalController.create({
|
||||
component: MapListModalComponent,
|
||||
swipeToClose: true,
|
||||
componentProps: {
|
||||
filterQuery: this.filterQuery,
|
||||
queryText: this.queryText,
|
||||
dismissAction: () => {
|
||||
this.modal.dismiss();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await this.modal.present();
|
||||
}
|
||||
}
|
||||
105
src/app/modules/map/page/map-page.html
Normal file
105
src/app/modules/map/page/map-page.html
Normal file
@@ -0,0 +1,105 @@
|
||||
<stapps-context [contentId]="'map'"></stapps-context>
|
||||
|
||||
<ion-header class="ion-no-border" translucent="true">
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button
|
||||
*ngIf="items.length === 1"
|
||||
default-href="/"
|
||||
(click)="resetView()"
|
||||
></ion-back-button>
|
||||
<ion-menu-button></ion-menu-button>
|
||||
</ion-buttons>
|
||||
<ion-searchbar
|
||||
(keyup.enter)="searchStringChanged($event.target.value)"
|
||||
[(ngModel)]="queryText"
|
||||
(ionClear)="searchStringChanged()"
|
||||
placeholder="{{ 'map.page.search.PLACEHOLDER' | translate }}"
|
||||
showClearButton="always"
|
||||
>
|
||||
</ion-searchbar>
|
||||
<ion-buttons slot="end">
|
||||
<ion-menu-button menu="context" auto-hide="false">
|
||||
<ion-icon name="filter"></ion-icon>
|
||||
</ion-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content fullscreen id="map">
|
||||
<div
|
||||
class="map-container"
|
||||
#mapContainer
|
||||
leaflet
|
||||
(leafletMapReady)="onMapReady($event)"
|
||||
(leafletClick)="resetView()"
|
||||
[leafletOptions]="options"
|
||||
[leafletMarkerCluster]="markerClusterData"
|
||||
[leafletMarkerClusterOptions]="markerClusterOptions"
|
||||
>
|
||||
<div *ngIf="position" [leafletLayer]="positionMarker"></div>
|
||||
</div>
|
||||
<div class="floating-content">
|
||||
<div class="map-buttons above">
|
||||
<ion-button
|
||||
*ngIf="items.length > 1"
|
||||
color="light"
|
||||
shape="round"
|
||||
size="small"
|
||||
(click)="showListModal()"
|
||||
>
|
||||
<ion-icon name="list"></ion-icon> {{
|
||||
'map.page.buttons.SHOW_LIST' | translate
|
||||
}}
|
||||
</ion-button>
|
||||
<ion-button
|
||||
[disabled]="position === undefined"
|
||||
color="light"
|
||||
shape="round"
|
||||
size="small"
|
||||
(click)="onPositionClick()"
|
||||
>
|
||||
<ion-icon
|
||||
*ngIf="position !== null; else questionIcon"
|
||||
name="locate"
|
||||
></ion-icon>
|
||||
<ng-template #questionIcon>
|
||||
<ion-icon name="help-circle-outline"></ion-icon>
|
||||
</ng-template>
|
||||
</ion-button>
|
||||
</div>
|
||||
<stapps-map-item
|
||||
*ngIf="items.length === 1"
|
||||
[item]="items[0]"
|
||||
(showDetails)="showItemModal($event)"
|
||||
></stapps-map-item>
|
||||
</div>
|
||||
<div class="map-buttons floating-buttons">
|
||||
<ion-button
|
||||
*ngIf="items.length > 1"
|
||||
color="light"
|
||||
shape="round"
|
||||
size="small"
|
||||
(click)="showListModal()"
|
||||
>
|
||||
<ion-icon name="list"></ion-icon> {{
|
||||
'map.page.buttons.SHOW_LIST' | translate
|
||||
}}
|
||||
</ion-button>
|
||||
<ion-button
|
||||
[disabled]="position === undefined"
|
||||
color="light"
|
||||
shape="round"
|
||||
size="small"
|
||||
(click)="onPositionClick()"
|
||||
>
|
||||
<ion-icon
|
||||
*ngIf="position !== null; else questionIcon"
|
||||
name="locate"
|
||||
></ion-icon>
|
||||
<ng-template #questionIcon>
|
||||
<ion-icon name="help-circle-outline"></ion-icon>
|
||||
</ng-template>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-content>
|
||||
119
src/app/modules/map/page/map-page.scss
Normal file
119
src/app/modules/map/page/map-page.scss
Normal file
@@ -0,0 +1,119 @@
|
||||
ion-header {
|
||||
ion-toolbar {
|
||||
--background: transparent;
|
||||
--ion-color-base: transparent;
|
||||
|
||||
ion-buttons {
|
||||
ion-back-button::part(native), ion-menu-button::part(native) {
|
||||
box-shadow: var(--map-box-shadow);
|
||||
//padding: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
ion-searchbar {
|
||||
--background: white;
|
||||
// important for iOS
|
||||
--box-shadow: var(--map-box-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
ion-content {
|
||||
// fixes the unexpected issue that the content is not fullscreen (behind the header)
|
||||
position: absolute;
|
||||
div.map-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
ion-back-button, ion-menu-button {
|
||||
--background: white;
|
||||
--background-hover: whitesmoke;
|
||||
--background-focused: whitesmoke;
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
.stapps-location {
|
||||
ion-icon {
|
||||
color: #fd435c;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.stapps-device-location {
|
||||
ion-icon {
|
||||
color: #4387fd;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
div.floating-content {
|
||||
display: grid;
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
padding: 0 20px;
|
||||
justify-content: center;
|
||||
|
||||
div.map-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
stapps-map-item {
|
||||
width: 550px;
|
||||
position: center;
|
||||
justify-self: center;
|
||||
margin: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.floating-buttons {
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
div.map-buttons {
|
||||
|
||||
ion-button {
|
||||
margin: 4px;
|
||||
// important for iOS
|
||||
--box-shadow: var(--map-box-shadow);
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
ion-button::part(native) {
|
||||
background: white;
|
||||
}
|
||||
|
||||
ion-button::part(native):hover, ion-button::part(native):focus {
|
||||
background: whitesmoke;
|
||||
}
|
||||
}
|
||||
|
||||
div.map-buttons.above {
|
||||
min-width: 70%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 667px) {
|
||||
div.map-buttons.above {
|
||||
display: flex;
|
||||
}
|
||||
div.floating-content {
|
||||
justify-content: normal;
|
||||
stapps-map-item {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
div.map-buttons.floating-buttons {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
60
src/app/modules/map/page/modals/map-list-modal.component.ts
Normal file
60
src/app/modules/map/page/modals/map-list-modal.component.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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, Input, OnInit} from '@angular/core';
|
||||
import {SCPlace, SCSearchFilter} from '@openstapps/core';
|
||||
import {MapProvider} from '../../map.provider';
|
||||
|
||||
/**
|
||||
* Modal showing a provided list of places
|
||||
*/
|
||||
@Component({
|
||||
selector: 'map-list-modal',
|
||||
templateUrl: 'map-list.html',
|
||||
styleUrls: ['map-list.scss'],
|
||||
})
|
||||
export class MapListModalComponent implements OnInit {
|
||||
/**
|
||||
* Action when close is pressed
|
||||
*/
|
||||
@Input() dismissAction: () => void;
|
||||
|
||||
/**
|
||||
* Used for creating the search for the shown list
|
||||
*/
|
||||
@Input() filterQuery?: SCSearchFilter;
|
||||
|
||||
/**
|
||||
* Places to show in the list
|
||||
*/
|
||||
items: SCPlace[];
|
||||
|
||||
/**
|
||||
* Used for creating the search for the shown list
|
||||
*/
|
||||
@Input() queryText?: string;
|
||||
|
||||
constructor(private mapProvider: MapProvider) {}
|
||||
|
||||
/**
|
||||
* Populate the list with the results from the search
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.mapProvider
|
||||
.searchPlaces(this.filterQuery, this.queryText)
|
||||
.then(result => {
|
||||
this.items = result.data as SCPlace[];
|
||||
});
|
||||
}
|
||||
}
|
||||
13
src/app/modules/map/page/modals/map-list.html
Normal file
13
src/app/modules/map/page/modals/map-list.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<ion-header translucent>
|
||||
<ion-toolbar>
|
||||
<ion-title>{{ 'map.modals.list.TITLE' | translate }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="dismissAction()">{{
|
||||
'app.ui.CLOSE' | translate
|
||||
}}</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<stapps-data-list [items]="items"></stapps-data-list>
|
||||
</ion-content>
|
||||
0
src/app/modules/map/page/modals/map-list.scss
Normal file
0
src/app/modules/map/page/modals/map-list.scss
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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, Input} from '@angular/core';
|
||||
import {SCPlace} from '@openstapps/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-map-single-modal',
|
||||
templateUrl: './map-single.html',
|
||||
styleUrls: ['./map-single.scss'],
|
||||
})
|
||||
export class MapSingleModalComponent {
|
||||
/**
|
||||
* Action when close is pressed
|
||||
*/
|
||||
@Input() dismissAction: () => void;
|
||||
|
||||
/**
|
||||
* The item to be shown
|
||||
*/
|
||||
@Input() item: SCPlace;
|
||||
}
|
||||
13
src/app/modules/map/page/modals/map-single.html
Normal file
13
src/app/modules/map/page/modals/map-single.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<ion-header translucent>
|
||||
<ion-toolbar>
|
||||
<ion-title>{{ 'map.modals.single.TITLE' | translate }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="dismissAction()">{{
|
||||
'app.ui.CLOSE' | translate
|
||||
}}</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<stapps-data-detail-content [item]="item"></stapps-data-detail-content>
|
||||
</ion-content>
|
||||
0
src/app/modules/map/page/modals/map-single.scss
Normal file
0
src/app/modules/map/page/modals/map-single.scss
Normal file
103
src/app/modules/map/position.service.spec.ts
Normal file
103
src/app/modules/map/position.service.spec.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (C) 2019-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 {TestBed} from '@angular/core/testing';
|
||||
import {MapModule} from './map.module';
|
||||
import {Geolocation, Geoposition} from '@ionic-native/geolocation/ngx';
|
||||
import {defer} from 'rxjs';
|
||||
import {HttpClientModule} from '@angular/common/http';
|
||||
import {StorageModule} from '../storage/storage.module';
|
||||
import {MapPosition, PositionService} from './position.service';
|
||||
import {Diagnostic} from '@ionic-native/diagnostic/ngx';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {
|
||||
LoggerConfig,
|
||||
LoggerModule,
|
||||
NGXLogger,
|
||||
NGXMapperService,
|
||||
} from 'ngx-logger';
|
||||
|
||||
/**
|
||||
* For faking a promise resolve
|
||||
*/
|
||||
function fakeAsyncResponse<T>(data: T) {
|
||||
return defer(() => Promise.resolve(data));
|
||||
}
|
||||
|
||||
describe('PositionService', () => {
|
||||
let geolocation: Geolocation;
|
||||
let positionService: PositionService;
|
||||
|
||||
const sampleMapPosition: MapPosition = {
|
||||
heading: 123,
|
||||
latitude: 34.12,
|
||||
longitude: 12.34,
|
||||
};
|
||||
const samplePosition: Geoposition = {
|
||||
coords: {
|
||||
...sampleMapPosition,
|
||||
accuracy: 1,
|
||||
altitude: 123,
|
||||
altitudeAccuracy: 1,
|
||||
speed: 1,
|
||||
},
|
||||
timestamp: 1_565_275_805_901,
|
||||
} as Geoposition;
|
||||
|
||||
beforeEach(async () => {
|
||||
const configProvider = {
|
||||
getValue: () => {
|
||||
Promise.resolve();
|
||||
},
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MapModule, HttpClientModule, StorageModule, LoggerModule],
|
||||
providers: [
|
||||
Geolocation,
|
||||
Diagnostic,
|
||||
LoggerConfig,
|
||||
NGXLogger,
|
||||
NGXMapperService,
|
||||
{
|
||||
provider: ConfigProvider,
|
||||
useValue: configProvider,
|
||||
},
|
||||
],
|
||||
});
|
||||
positionService = TestBed.inject(PositionService);
|
||||
geolocation = TestBed.inject(Geolocation);
|
||||
spyOn(geolocation, 'getCurrentPosition').and.returnValue(
|
||||
Promise.resolve(samplePosition),
|
||||
);
|
||||
|
||||
spyOn(geolocation, 'watchPosition').and.callFake(() => {
|
||||
return fakeAsyncResponse(samplePosition);
|
||||
});
|
||||
});
|
||||
|
||||
it('should provide the current location of the device', async () => {
|
||||
expect(await positionService.getCurrentLocation()).toEqual(
|
||||
sampleMapPosition,
|
||||
);
|
||||
});
|
||||
|
||||
it('should continuously provide (watch) location of the device', async () => {
|
||||
expect(
|
||||
positionService
|
||||
.watchCurrentLocation()
|
||||
.subscribe(result => expect(result).toEqual(sampleMapPosition)),
|
||||
);
|
||||
});
|
||||
});
|
||||
163
src/app/modules/map/position.service.ts
Normal file
163
src/app/modules/map/position.service.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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 {Injectable} from '@angular/core';
|
||||
import {Diagnostic} from '@ionic-native/diagnostic/ngx';
|
||||
import {
|
||||
Geolocation,
|
||||
GeolocationOptions,
|
||||
Geoposition,
|
||||
PositionError,
|
||||
} from '@ionic-native/geolocation/ngx';
|
||||
import {Point} from 'geojson';
|
||||
import {geoJSON, LatLng} from 'leaflet';
|
||||
import {Observable} from 'rxjs';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
* Check if provided position object is a position error
|
||||
*
|
||||
* @param position A position object to be checked
|
||||
*/
|
||||
function isPositionError(
|
||||
position: Geoposition | PositionError,
|
||||
): position is PositionError {
|
||||
return (
|
||||
typeof (position as PositionError).code !== 'undefined' &&
|
||||
typeof (position as PositionError).message !== 'undefined'
|
||||
);
|
||||
}
|
||||
|
||||
export interface Coordinates {
|
||||
/**
|
||||
* Geographic latitude from a device
|
||||
*/
|
||||
latitude: number;
|
||||
/**
|
||||
* Geographic longitude from a device
|
||||
*/
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface MapPosition extends Coordinates {
|
||||
/**
|
||||
* Where is the device pointed
|
||||
*/
|
||||
heading?: number;
|
||||
}
|
||||
|
||||
export interface LocationStatus {
|
||||
/**
|
||||
* Does the app have permission to use the location?
|
||||
*/
|
||||
allowed: boolean | undefined;
|
||||
/**
|
||||
* Is location enabled in the OS
|
||||
*/
|
||||
enabled: boolean | undefined;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PositionService {
|
||||
/**
|
||||
* Current location status
|
||||
*/
|
||||
locationStatus: LocationStatus;
|
||||
|
||||
/**
|
||||
* Current position
|
||||
*/
|
||||
position?: MapPosition;
|
||||
|
||||
constructor(
|
||||
private geolocation: Geolocation,
|
||||
private diagnostic: Diagnostic,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Gets current coordinates information of the device
|
||||
*
|
||||
* @param options Options which define which data should be provided (e.g. how accurate or how old)
|
||||
*/
|
||||
async getCurrentLocation(options?: GeolocationOptions): Promise<MapPosition> {
|
||||
const geoPosition = await this.geolocation.getCurrentPosition(options);
|
||||
|
||||
this.position = {
|
||||
heading:
|
||||
Number.isNaN(geoPosition.coords.heading) ||
|
||||
geoPosition.coords.heading == undefined
|
||||
? undefined
|
||||
: geoPosition.coords.heading,
|
||||
latitude: geoPosition.coords.latitude,
|
||||
longitude: geoPosition.coords.longitude,
|
||||
};
|
||||
|
||||
return this.position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides distance from users position
|
||||
*
|
||||
* @param point Point to which distance should be calculated
|
||||
*/
|
||||
getDistance(point: Point): number | undefined {
|
||||
if (typeof this.position === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new LatLng(
|
||||
this.position.latitude,
|
||||
this.position.longitude,
|
||||
).distanceTo(geoJSON(point).getBounds().getCenter());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the information about the availability of the location service (ONLY ON DEVICES / when cordova exists)
|
||||
*/
|
||||
async getLocationStatus(): Promise<LocationStatus> {
|
||||
const enabled = await this.diagnostic.isLocationEnabled();
|
||||
const allowed = await this.diagnostic.isLocationAuthorized();
|
||||
|
||||
return {enabled, allowed};
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches (continuously gets) current coordinates information of the device
|
||||
*
|
||||
* @param options Options which define which data should be provided (e.g. how accurate or how old)
|
||||
*/
|
||||
watchCurrentLocation(options?: GeolocationOptions): Observable<MapPosition> {
|
||||
return this.geolocation.watchPosition(options).pipe(
|
||||
map(geoPosition => {
|
||||
if (isPositionError(geoPosition)) {
|
||||
throw geoPosition;
|
||||
}
|
||||
|
||||
this.position = {
|
||||
heading:
|
||||
Number.isNaN(geoPosition.coords.heading) ||
|
||||
geoPosition.coords.heading == undefined
|
||||
? undefined
|
||||
: geoPosition.coords.heading,
|
||||
latitude: geoPosition.coords.latitude,
|
||||
longitude: geoPosition.coords.longitude,
|
||||
};
|
||||
|
||||
return this.position;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
81
src/app/modules/map/widget/map-widget.component.ts
Normal file
81
src/app/modules/map/widget/map-widget.component.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2020-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, ElementRef, Input, OnInit} from '@angular/core';
|
||||
import {SCPlace} from '@openstapps/core';
|
||||
import {geoJSON, Map, MapOptions, tileLayer} from 'leaflet';
|
||||
import {MapProvider} from '../map.provider';
|
||||
import Timeout = NodeJS.Timeout;
|
||||
|
||||
/**
|
||||
* The map widget (needs a container with explicit size)
|
||||
*/
|
||||
@Component({
|
||||
selector: 'stapps-map-widget',
|
||||
styleUrls: ['./map-widget.scss'],
|
||||
templateUrl: './map-widget.html',
|
||||
})
|
||||
export class MapWidgetComponent implements OnInit {
|
||||
/**
|
||||
* A leaflet map showed
|
||||
*/
|
||||
map: Map;
|
||||
|
||||
/**
|
||||
* Options of the leaflet map
|
||||
*/
|
||||
options: MapOptions;
|
||||
|
||||
/**
|
||||
* A place to show on the map
|
||||
*/
|
||||
@Input() place: SCPlace;
|
||||
|
||||
constructor(private element: ElementRef) {}
|
||||
|
||||
/**
|
||||
* Prepare the map
|
||||
*/
|
||||
ngOnInit() {
|
||||
const markerLayer = MapProvider.getPointMarker(this.place.geo.point);
|
||||
this.options = {
|
||||
center: geoJSON(this.place.geo.polygon || this.place.geo.point)
|
||||
.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: 18,
|
||||
},
|
||||
),
|
||||
markerLayer,
|
||||
],
|
||||
zoom: 16,
|
||||
zoomControl: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* What happens when the leaflet map is ready (note: doesn't mean that tiles are loaded)
|
||||
*/
|
||||
onMapReady(map: Map) {
|
||||
this.map = map;
|
||||
const interval: Timeout = setInterval(() =>
|
||||
MapProvider.invalidateWhenRendered(map, this.element, interval),
|
||||
);
|
||||
}
|
||||
}
|
||||
16
src/app/modules/map/widget/map-widget.html
Normal file
16
src/app/modules/map/widget/map-widget.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<div
|
||||
class="map-container"
|
||||
(leafletMapReady)="onMapReady($event)"
|
||||
leaflet
|
||||
[leafletOptions]="options"
|
||||
></div>
|
||||
<div class="map-buttons">
|
||||
<ion-button
|
||||
color="light"
|
||||
shape="round"
|
||||
size="small"
|
||||
[routerLink]="['/map', place.uid]"
|
||||
>
|
||||
<ion-icon name="expand"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
12
src/app/modules/map/widget/map-widget.scss
Normal file
12
src/app/modules/map/widget/map-widget.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
div.map-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
div.map-buttons {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 10000;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018, 2019, 2020 StApps
|
||||
* Copyright (C) 2018-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.
|
||||
@@ -12,7 +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/>.
|
||||
*/
|
||||
import {Component, OnDestroy} from '@angular/core';
|
||||
import {Component, Input, OnDestroy} from '@angular/core';
|
||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
||||
import {
|
||||
SCLanguage,
|
||||
@@ -38,6 +38,12 @@ import {FilterContext, SortContext, SortContextOption} from './context-type';
|
||||
templateUrl: 'context-menu.html',
|
||||
})
|
||||
export class ContextMenuComponent implements OnDestroy {
|
||||
/**
|
||||
* Id of the content the menu is used for
|
||||
*/
|
||||
@Input()
|
||||
contentId: string;
|
||||
|
||||
/**
|
||||
* Amount of filter options shown on compact view
|
||||
*/
|
||||
@@ -145,7 +151,6 @@ export class ContextMenuComponent implements OnDestroy {
|
||||
for (const filterBucket of filterFacet.buckets) {
|
||||
filterBucket.checked = false;
|
||||
}
|
||||
|
||||
this.contextMenuService.contextFilterChanged(this.filterOption);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<ion-menu type="overlay" menuId="context" contentId="data-list" side="end">
|
||||
<ion-menu
|
||||
type="overlay"
|
||||
menuId="context"
|
||||
contentId="{{ contentId }}"
|
||||
side="end"
|
||||
>
|
||||
<ion-list-header>
|
||||
<ion-toolbar>
|
||||
<h3>{{ 'menu.context.title' | translate | titlecase }}</h3>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
stapps-navigation {
|
||||
ion-radio {
|
||||
.radio-icon {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 StApps
|
||||
* Copyright (C) 2020-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.
|
||||
@@ -13,14 +13,92 @@
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Injectable} from '@angular/core';
|
||||
import {SCFacet, SCSearchFilter, SCSearchSort} from '@openstapps/core';
|
||||
import {Subject} from 'rxjs';
|
||||
import {
|
||||
FilterBucket,
|
||||
FilterContext,
|
||||
FilterFacet,
|
||||
SortContext,
|
||||
} from './context-type';
|
||||
SCFacet,
|
||||
SCFacetBucket,
|
||||
SCSearchFilter,
|
||||
SCSearchSort,
|
||||
} from '@openstapps/core';
|
||||
import {Subject} from 'rxjs';
|
||||
|
||||
export type ContextType = FilterContext | SortContext;
|
||||
|
||||
/**
|
||||
* A sort context
|
||||
*/
|
||||
interface SortContext {
|
||||
/**
|
||||
* Name of the context
|
||||
*/
|
||||
name: 'sort';
|
||||
|
||||
/**
|
||||
* Reverse option
|
||||
*/
|
||||
reversed: boolean;
|
||||
|
||||
/**
|
||||
* sort value
|
||||
*/
|
||||
value: string;
|
||||
|
||||
/**
|
||||
* Sort options
|
||||
*/
|
||||
values: SortContextOption[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A sort context option
|
||||
*/
|
||||
interface SortContextOption {
|
||||
/**
|
||||
* sort option is reversible
|
||||
*/
|
||||
reversible: boolean;
|
||||
|
||||
/**
|
||||
* sort option value
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A filter context
|
||||
*/
|
||||
interface FilterContext {
|
||||
/**
|
||||
* Compact view of the filter options
|
||||
*/
|
||||
compact?: boolean;
|
||||
/**
|
||||
* Name of the context
|
||||
*/
|
||||
name: 'filter';
|
||||
|
||||
/**
|
||||
* Filter values
|
||||
*/
|
||||
options: FilterFacet[];
|
||||
}
|
||||
|
||||
interface FilterFacet extends SCFacet {
|
||||
/**
|
||||
* FilterBuckets of a FilterFacet
|
||||
*/
|
||||
buckets: FilterBucket[];
|
||||
/**
|
||||
* Compact view of the option buckets
|
||||
*/
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
interface FilterBucket extends SCFacetBucket {
|
||||
/**
|
||||
* Sets the Filter active
|
||||
*/
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* ContextMenuService provides bidirectional communication of context menu options and search queries
|
||||
@@ -35,13 +113,13 @@ export class ContextMenuService {
|
||||
/**
|
||||
* Container for the filter context
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/member-ordering
|
||||
// tslint:disable-next-line:member-ordering
|
||||
filterOptions = new Subject<FilterContext>();
|
||||
|
||||
/**
|
||||
* Observable filterContext streams
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/member-ordering
|
||||
// tslint:disable-next-line:member-ordering
|
||||
filterContextChanged$ = this.filterOptions.asObservable();
|
||||
|
||||
/**
|
||||
@@ -52,19 +130,19 @@ export class ContextMenuService {
|
||||
/**
|
||||
* Observable filterContext streams
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/member-ordering
|
||||
// tslint:disable-next-line:member-ordering
|
||||
filterQueryChanged$ = this.filterQuery.asObservable();
|
||||
|
||||
/**
|
||||
* Container for the sort context
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/member-ordering
|
||||
// tslint:disable-next-line:member-ordering
|
||||
sortOptions = new Subject<SortContext>();
|
||||
|
||||
/**
|
||||
* Observable SortContext streams
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/member-ordering
|
||||
// tslint:disable-next-line:member-ordering
|
||||
sortContextChanged$ = this.sortOptions.asObservable();
|
||||
|
||||
/**
|
||||
@@ -75,7 +153,7 @@ export class ContextMenuService {
|
||||
/**
|
||||
* Observable SortContext streams
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/member-ordering
|
||||
// tslint:disable-next-line:member-ordering
|
||||
sortQueryChanged$ = this.sortQuery.asObservable();
|
||||
|
||||
/**
|
||||
|
||||
56
src/assets/custom-ionicons/navigate-straight.svg
Normal file
56
src/assets/custom-ionicons/navigate-straight.svg
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
class="ionicon"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
sodipodi:docname="navigate.svg"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1361"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
showguides="true"
|
||||
inkscape:zoom="0.4609375"
|
||||
inkscape:cx="200.67797"
|
||||
inkscape:cy="256"
|
||||
inkscape:window-x="423"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg6" />
|
||||
<title
|
||||
id="title2">Navigate</title>
|
||||
<path
|
||||
d="m 414.33738,465.28459 a 16,16 0 0 1 -22.9244,-0.29698 L 261.69424,335.26887 a 8,8 0 0 0 -11.31371,0 L 120.56279,465.0866 a 16.31,16.31 0 0 1 -18.48377,3.4224 16,16 0 0 1 -8.103433,-19.99698 L 240.94772,52.426166 a 16,16 0 0 1 29.9884,-0.0212 L 418.01433,448.38475 a 16,16 0 0 1 -3.67695,16.89985 z"
|
||||
id="path4"
|
||||
inkscape:connector-curvature="0" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"app": {
|
||||
"ui": {
|
||||
"CLOSE": "Schließen"
|
||||
},
|
||||
"errors": {
|
||||
"UNKNOWN": "Unbekannter Fehler"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"REFRESH_ACTION": "Aktualisieren",
|
||||
"REFRESHING": "Aktualisierung läuft...",
|
||||
@@ -60,8 +68,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"news": {
|
||||
"title": "Aktuelles"
|
||||
"map": {
|
||||
"page": {
|
||||
"TITLE": "Karte",
|
||||
"search": {
|
||||
"PLACEHOLDER": "Finde Gebäude, Points of Interest, Mensen und Cafés ..."
|
||||
},
|
||||
"buttons": {
|
||||
"SHOW_LIST": "Liste ansehen",
|
||||
"MORE": "Mehr"
|
||||
},
|
||||
"geolocation": {
|
||||
"TITLE": "Standort",
|
||||
"SUBTITLE": "Standort nicht erreichbar",
|
||||
"NOT_ENABLED": "Standortermittlung auf Deinem Gerät ist nicht aktiviert",
|
||||
"NOT_ALLOWED": "Zugriff auf den Standort für die App nicht zugelassen"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"single": {
|
||||
"TITLE": "Angezeigter Ort"
|
||||
},
|
||||
"list": {
|
||||
"TITLE": "Angezeigte Orte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"context": {
|
||||
@@ -81,6 +112,9 @@
|
||||
"settings": "Einstellungen"
|
||||
}
|
||||
},
|
||||
"news": {
|
||||
"title": "Aktuelles"
|
||||
},
|
||||
"search": {
|
||||
"nothing_found": "Keine Ergebnisse"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"app": {
|
||||
"ui": {
|
||||
"CLOSE": "Close"
|
||||
},
|
||||
"errors": {
|
||||
"UNKNOWN": "Unknown problem"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"REFRESH_ACTION": "Refresh",
|
||||
"REFRESHING": "Refreshing...",
|
||||
@@ -60,8 +68,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"news": {
|
||||
"title": "News"
|
||||
"map": {
|
||||
"page": {
|
||||
"TITLE": "Map",
|
||||
"search": {
|
||||
"PLACEHOLDER": "Find buildings, points of interests, canteens and cafes ..."
|
||||
},
|
||||
"buttons": {
|
||||
"SHOW_LIST": "Show list",
|
||||
"MORE": "More"
|
||||
},
|
||||
"geolocation": {
|
||||
"TITLE": "Location",
|
||||
"SUBTITLE": "Location not available",
|
||||
"NOT_ENABLED": "Location service is not enabled on your device",
|
||||
"NOT_ALLOWED": "The app is not allowed to access your location"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"single": {
|
||||
"TITLE": "Place shown"
|
||||
},
|
||||
"list": {
|
||||
"TITLE": "Places shown"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"context": {
|
||||
@@ -81,6 +112,9 @@
|
||||
"settings": "settings"
|
||||
}
|
||||
},
|
||||
"news": {
|
||||
"title": "News"
|
||||
},
|
||||
"search": {
|
||||
"nothing_found": "No results"
|
||||
},
|
||||
|
||||
@@ -78,4 +78,5 @@
|
||||
/** StApps **/
|
||||
--placeholder-gray: #F1F0ED;
|
||||
/** Change the colors of the toolbar and the toolbar text here **/
|
||||
--map-box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user