mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-04-17 03:39:04 +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",
|
"input": "node_modules/ionicons/dist/ionicons/svg",
|
||||||
"output": "./svg"
|
"output": "./svg"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"glob": "**/*.svg",
|
||||||
|
"input": "src/assets/custom-ionicons",
|
||||||
|
"output": "./svg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "./node_modules/leaflet/dist/images",
|
||||||
|
"output": "assets/"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"glob": "**/*.svg",
|
"glob": "**/*.svg",
|
||||||
"input": "src/assets/custom-ion-icons",
|
"input": "src/assets/custom-ion-icons",
|
||||||
@@ -43,7 +53,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": "src/global.scss"
|
"input": "src/global.scss"
|
||||||
}
|
},
|
||||||
|
"./node_modules/leaflet/dist/leaflet.css",
|
||||||
|
"./node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
},
|
},
|
||||||
@@ -158,6 +170,11 @@
|
|||||||
"glob": "**/*",
|
"glob": "**/*",
|
||||||
"input": "src/assets",
|
"input": "src/assets",
|
||||||
"output": "/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="ios" spec="6.2.0" />
|
||||||
<engine name="browser" spec="6.0.0" />
|
<engine name="browser" spec="6.0.0" />
|
||||||
<engine name="android" spec="9.0.0" />
|
<engine name="android" spec="9.0.0" />
|
||||||
|
<plugin name="cordova.plugins.diagnostic" spec="6.0.3" />
|
||||||
</widget>
|
</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",
|
"resolved": "https://registry.npmjs.org/@angular/router/-/router-9.1.12.tgz",
|
||||||
"integrity": "sha512-+qCaXa9y0nsRhzjAYBqmGoQ2YkrdXgftZwuFDf6t4qEi30EXa0oS97KrlFq0M5GKdLIDGrbUm9PcdHSTOI+ZhA=="
|
"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": {
|
"@babel/code-frame": {
|
||||||
"version": "7.14.5",
|
"version": "7.14.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz",
|
||||||
@@ -2266,6 +2276,21 @@
|
|||||||
"@types/cordova": "^0.0.34"
|
"@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": {
|
"@ionic-native/geolocation": {
|
||||||
"version": "5.29.0",
|
"version": "5.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ionic-native/geolocation/-/geolocation-5.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ionic-native/geolocation/-/geolocation-5.29.0.tgz",
|
||||||
@@ -3207,6 +3232,24 @@
|
|||||||
"@types/node": "*"
|
"@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": {
|
"@types/lodash": {
|
||||||
"version": "4.14.170",
|
"version": "4.14.170",
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.170.tgz",
|
"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": {
|
"core-js": {
|
||||||
"version": "2.6.5",
|
"version": "2.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz",
|
||||||
@@ -10007,6 +10067,11 @@
|
|||||||
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
||||||
"dev": true
|
"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": {
|
"get-assigned-identifiers": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz",
|
"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==",
|
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
||||||
"dev": true
|
"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": {
|
"less": {
|
||||||
"version": "3.11.3",
|
"version": "3.11.3",
|
||||||
"resolved": "https://registry.npmjs.org/less/-/less-3.11.3.tgz",
|
"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\"",
|
"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",
|
"documentation": "compodoc -p tsconfig.json -d docs",
|
||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"lint:fix": "ng lint:fix",
|
"lint-fix": "eslint --fix -c .eslintrc.json --ignore-path .eslintignore --ext .ts,.html src/",
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"postversion": "npm run changelog",
|
"postversion": "npm run changelog",
|
||||||
"pree2e": "webdriver-manager clean && webdriver-manager update --gecko false --versions.chrome $(google-chrome --product-version)",
|
"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": "9.1.12",
|
||||||
"@angular/platform-browser-dynamic": "9.1.12",
|
"@angular/platform-browser-dynamic": "9.1.12",
|
||||||
"@angular/router": "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",
|
"@capacitor/core": "2.4.6",
|
||||||
"@ionic-native/core": "5.29.0",
|
"@ionic-native/core": "5.29.0",
|
||||||
|
"@ionic-native/diagnostic": "5.32.0",
|
||||||
"@ionic-native/geolocation": "5.29.0",
|
"@ionic-native/geolocation": "5.29.0",
|
||||||
"@ionic-native/network": "5.31.1",
|
"@ionic-native/network": "5.31.1",
|
||||||
"@ionic-native/splash-screen": "5.29.0",
|
"@ionic-native/splash-screen": "5.29.0",
|
||||||
@@ -65,6 +68,7 @@
|
|||||||
"cordova-ios": "6.2.0",
|
"cordova-ios": "6.2.0",
|
||||||
"cordova-plugin-androidx-adapter": "1.1.3",
|
"cordova-plugin-androidx-adapter": "1.1.3",
|
||||||
"cordova-plugin-device": "2.0.3",
|
"cordova-plugin-device": "2.0.3",
|
||||||
|
"cordova.plugins.diagnostic": "6.0.3",
|
||||||
"cordova-plugin-geolocation": "4.1.0",
|
"cordova-plugin-geolocation": "4.1.0",
|
||||||
"cordova-plugin-ionic-keyboard": "2.2.0",
|
"cordova-plugin-ionic-keyboard": "2.2.0",
|
||||||
"cordova-plugin-ionic-webview": "5.0.0",
|
"cordova-plugin-ionic-webview": "5.0.0",
|
||||||
@@ -74,6 +78,9 @@
|
|||||||
"core-js": "2.6.5",
|
"core-js": "2.6.5",
|
||||||
"deepmerge": "3.3.0",
|
"deepmerge": "3.3.0",
|
||||||
"form-data": "2.5.0",
|
"form-data": "2.5.0",
|
||||||
|
"geojson": "0.5.0",
|
||||||
|
"leaflet": "1.4.0",
|
||||||
|
"leaflet.markercluster": "1.4.1",
|
||||||
"lodash-es": "4.17.21",
|
"lodash-es": "4.17.21",
|
||||||
"moment": "2.29.1",
|
"moment": "2.29.1",
|
||||||
"ngx-logger": "4.1.9",
|
"ngx-logger": "4.1.9",
|
||||||
@@ -103,6 +110,8 @@
|
|||||||
"@types/form-data": "2.5.0",
|
"@types/form-data": "2.5.0",
|
||||||
"@types/jasmine": "3.3.12",
|
"@types/jasmine": "3.3.12",
|
||||||
"@types/jasminewd2": "2.0.6",
|
"@types/jasminewd2": "2.0.6",
|
||||||
|
"@types/leaflet": "1.4.4",
|
||||||
|
"@types/leaflet.markercluster": "1.0.3",
|
||||||
"@types/lodash-es": "4.17.4",
|
"@types/lodash-es": "4.17.4",
|
||||||
"@types/node": "14.14.37",
|
"@types/node": "14.14.37",
|
||||||
"@typescript-eslint/eslint-plugin": "4.3.0",
|
"@typescript-eslint/eslint-plugin": "4.3.0",
|
||||||
@@ -134,11 +143,16 @@
|
|||||||
"cordova-plugin-whitelist": {},
|
"cordova-plugin-whitelist": {},
|
||||||
"cordova-plugin-device": {},
|
"cordova-plugin-device": {},
|
||||||
"cordova-plugin-splashscreen": {},
|
"cordova-plugin-splashscreen": {},
|
||||||
"cordova-plugin-ionic-webview": {},
|
"cordova-plugin-ionic-webview": {
|
||||||
|
"ANDROID_SUPPORT_ANNOTATIONS_VERSION": "27.+"
|
||||||
|
},
|
||||||
"cordova-plugin-ionic-keyboard": {},
|
"cordova-plugin-ionic-keyboard": {},
|
||||||
"cordova-plugin-geolocation": {
|
"cordova-plugin-geolocation": {
|
||||||
"GEOLOCATION_USAGE_DESCRIPTION": "The app will use your location to provide features for navigation or distances information.",
|
"GEOLOCATION_USAGE_DESCRIPTION": "The app will use your location to provide features for navigation or distances information.",
|
||||||
"GPS_REQUIRED": "true"
|
"GPS_REQUIRED": "true"
|
||||||
|
},
|
||||||
|
"cordova.plugins.diagnostic": {
|
||||||
|
"ANDROIDX_VERSION": "1.+"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"platforms": [
|
"platforms": [
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import localeDe from '@angular/common/locales/de';
|
|||||||
import {APP_INITIALIZER, NgModule, Provider} from '@angular/core';
|
import {APP_INITIALIZER, NgModule, Provider} from '@angular/core';
|
||||||
import {BrowserModule} from '@angular/platform-browser';
|
import {BrowserModule} from '@angular/platform-browser';
|
||||||
import {RouteReuseStrategy} from '@angular/router';
|
import {RouteReuseStrategy} from '@angular/router';
|
||||||
|
import {Diagnostic} from '@ionic-native/diagnostic/ngx';
|
||||||
import {SplashScreen} from '@ionic-native/splash-screen/ngx';
|
import {SplashScreen} from '@ionic-native/splash-screen/ngx';
|
||||||
import {StatusBar} from '@ionic-native/status-bar/ngx';
|
import {StatusBar} from '@ionic-native/status-bar/ngx';
|
||||||
import {IonicModule, IonicRouteStrategy} from '@ionic/angular';
|
import {IonicModule, IonicRouteStrategy} from '@ionic/angular';
|
||||||
@@ -41,6 +42,7 @@ import {AppComponent} from './app.component';
|
|||||||
import {ConfigModule} from './modules/config/config.module';
|
import {ConfigModule} from './modules/config/config.module';
|
||||||
import {ConfigProvider} from './modules/config/config.provider';
|
import {ConfigProvider} from './modules/config/config.provider';
|
||||||
import {DataModule} from './modules/data/data.module';
|
import {DataModule} from './modules/data/data.module';
|
||||||
|
import {MapModule} from './modules/map/map.module';
|
||||||
import {MenuModule} from './modules/menu/menu.module';
|
import {MenuModule} from './modules/menu/menu.module';
|
||||||
import {NewsModule} from './modules/news/news.module';
|
import {NewsModule} from './modules/news/news.module';
|
||||||
import {SettingsModule} from './modules/settings/settings.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 {StorageModule} from './modules/storage/storage.module';
|
||||||
import {ThingTranslateModule} from './translation/thing-translate.module';
|
import {ThingTranslateModule} from './translation/thing-translate.module';
|
||||||
import {fakeBackendProvider} from './_helpers/fake-backend.interceptor';
|
import {fakeBackendProvider} from './_helpers/fake-backend.interceptor';
|
||||||
|
|
||||||
import {initLogger} from './_helpers/ts-logger';
|
import {initLogger} from './_helpers/ts-logger';
|
||||||
|
|
||||||
registerLocaleData(localeDe);
|
registerLocaleData(localeDe);
|
||||||
@@ -105,6 +106,7 @@ export function createTranslateLoader(http: HttpClient) {
|
|||||||
const providers: Provider[] = [
|
const providers: Provider[] = [
|
||||||
StatusBar,
|
StatusBar,
|
||||||
SplashScreen,
|
SplashScreen,
|
||||||
|
Diagnostic,
|
||||||
{
|
{
|
||||||
provide: RouteReuseStrategy,
|
provide: RouteReuseStrategy,
|
||||||
useClass: IonicRouteStrategy,
|
useClass: IonicRouteStrategy,
|
||||||
@@ -134,6 +136,7 @@ const providers: Provider[] = [
|
|||||||
ConfigModule,
|
ConfigModule,
|
||||||
DataModule,
|
DataModule,
|
||||||
IonicModule.forRoot(),
|
IonicModule.forRoot(),
|
||||||
|
MapModule,
|
||||||
MenuModule,
|
MenuModule,
|
||||||
NewsModule,
|
NewsModule,
|
||||||
SettingsModule,
|
SettingsModule,
|
||||||
|
|||||||
@@ -36,44 +36,46 @@ import {DataProvider} from './data.provider';
|
|||||||
import {DataDetailContentComponent} from './detail/data-detail-content.component';
|
import {DataDetailContentComponent} from './detail/data-detail-content.component';
|
||||||
import {DataDetailComponent} from './detail/data-detail.component';
|
import {DataDetailComponent} from './detail/data-detail.component';
|
||||||
import {AddressDetailComponent} from './elements/address-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 {OffersDetailComponent} from './elements/offers-detail.component';
|
||||||
import {OffersInListComponent} from './elements/offers-in-list.component';
|
import {OffersInListComponent} from './elements/offers-in-list.component';
|
||||||
import {OriginDetailComponent} from './elements/origin-detail.component';
|
import {OriginDetailComponent} from './elements/origin-detail.component';
|
||||||
import {OriginInListComponent} from './elements/origin-in-list.component';
|
import {OriginInListComponent} from './elements/origin-in-list.component';
|
||||||
import {SimpleCardComponent} from './elements/simple-card.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 {DataListComponent} from './list/data-list.component';
|
||||||
import {FoodDataListComponent} from './list/food-data-list.component';
|
import {FoodDataListComponent} from './list/food-data-list.component';
|
||||||
import {SearchPageComponent} from './list/search-page.component';
|
import {SearchPageComponent} from './list/search-page.component';
|
||||||
import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
|
import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
|
||||||
import {ArticleDetailContentComponent} from './types/article/article-detail-content.component';
|
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 {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 {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 {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 {EventDetailContentComponent} from './types/event/event-detail-content.component';
|
||||||
import {EventListItemComponent} from './types/event/event-list-item.component';
|
import {EventListItemComponent} from './types/event/event-list-item.component';
|
||||||
import {FavoriteDetailContentComponent} from './types/favorite/favorite-detail-content.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 {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 {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 {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 {PlaceDetailContentComponent} from './types/place/place-detail-content.component';
|
||||||
import {PlaceListItemComponent} from './types/place/place-list-item.component';
|
import {PlaceListItemComponent} from './types/place/place-list-item.component';
|
||||||
import {PlaceMensaDetailComponent} from './types/place/special/mensa/place-mensa-detail.component';
|
import {PlaceMensaDetailComponent} from './types/place/special/mensa/place-mensa-detail.component';
|
||||||
import {SemesterDetailContentComponent} from './types/semester/semester-detail-content.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 {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';
|
import {VideoListItemComponent} from './types/video/video-list-item.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,17 +83,19 @@ import {VideoListItemComponent} from './types/video/video-list-item.component';
|
|||||||
*/
|
*/
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
ActionChipListComponent,
|
|
||||||
AddEventActionChipComponent,
|
|
||||||
AddEventPopoverComponent,
|
AddEventPopoverComponent,
|
||||||
|
OffersDetailComponent,
|
||||||
|
OffersInListComponent,
|
||||||
AddressDetailComponent,
|
AddressDetailComponent,
|
||||||
ArticleDetailContentComponent,
|
ArticleDetailContentComponent,
|
||||||
ArticleListItemComponent,
|
ArticleListItemComponent,
|
||||||
|
SimpleCardComponent,
|
||||||
|
SkeletonSimpleCardComponent,
|
||||||
CatalogDetailContentComponent,
|
CatalogDetailContentComponent,
|
||||||
CatalogListItemComponent,
|
CatalogListItemComponent,
|
||||||
DataDetailComponent,
|
DataDetailComponent,
|
||||||
DataDetailContentComponent,
|
DataDetailContentComponent,
|
||||||
DataIconPipe,
|
FoodDataListComponent,
|
||||||
DataListComponent,
|
DataListComponent,
|
||||||
DataListItemComponent,
|
DataListItemComponent,
|
||||||
DateSeriesDetailContentComponent,
|
DateSeriesDetailContentComponent,
|
||||||
@@ -102,13 +106,10 @@ import {VideoListItemComponent} from './types/video/video-list-item.component';
|
|||||||
EventListItemComponent,
|
EventListItemComponent,
|
||||||
FavoriteDetailContentComponent,
|
FavoriteDetailContentComponent,
|
||||||
FavoriteListItemComponent,
|
FavoriteListItemComponent,
|
||||||
FoodDataListComponent,
|
|
||||||
LocateActionChipComponent,
|
|
||||||
LongInlineTextComponent,
|
LongInlineTextComponent,
|
||||||
|
MapWidgetComponent,
|
||||||
MessageDetailContentComponent,
|
MessageDetailContentComponent,
|
||||||
MessageListItemComponent,
|
MessageListItemComponent,
|
||||||
OffersDetailComponent,
|
|
||||||
OffersInListComponent,
|
|
||||||
OrganizationDetailContentComponent,
|
OrganizationDetailContentComponent,
|
||||||
OrganizationListItemComponent,
|
OrganizationListItemComponent,
|
||||||
OriginDetailComponent,
|
OriginDetailComponent,
|
||||||
@@ -121,20 +122,22 @@ import {VideoListItemComponent} from './types/video/video-list-item.component';
|
|||||||
SearchPageComponent,
|
SearchPageComponent,
|
||||||
SemesterDetailContentComponent,
|
SemesterDetailContentComponent,
|
||||||
SemesterListItemComponent,
|
SemesterListItemComponent,
|
||||||
SimpleCardComponent,
|
|
||||||
SkeletonListItemComponent,
|
SkeletonListItemComponent,
|
||||||
SkeletonSegmentComponent,
|
SkeletonSegmentComponent,
|
||||||
SkeletonSimpleCardComponent,
|
|
||||||
VideoDetailContentComponent,
|
VideoDetailContentComponent,
|
||||||
VideoListItemComponent,
|
VideoListItemComponent,
|
||||||
|
DataIconPipe,
|
||||||
|
ActionChipListComponent,
|
||||||
|
AddEventActionChipComponent,
|
||||||
|
LocateActionChipComponent,
|
||||||
],
|
],
|
||||||
entryComponents: [DataListComponent],
|
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
|
||||||
DataRoutingModule,
|
|
||||||
FormsModule,
|
|
||||||
HttpClientModule,
|
|
||||||
IonicModule.forRoot(),
|
IonicModule.forRoot(),
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
DataRoutingModule,
|
||||||
|
HttpClientModule,
|
||||||
|
LeafletModule,
|
||||||
MarkdownModule.forRoot(),
|
MarkdownModule.forRoot(),
|
||||||
MenuModule,
|
MenuModule,
|
||||||
MomentModule.forRoot({
|
MomentModule.forRoot({
|
||||||
@@ -148,5 +151,15 @@ import {VideoListItemComponent} from './types/video/video-list-item.component';
|
|||||||
ThingTranslateModule.forChild(),
|
ThingTranslateModule.forChild(),
|
||||||
],
|
],
|
||||||
providers: [DataProvider, DataFacetsProvider, Network, StAppsWebHttpClient],
|
providers: [DataProvider, DataFacetsProvider, Network, StAppsWebHttpClient],
|
||||||
|
exports: [
|
||||||
|
DataListComponent,
|
||||||
|
DataListItemComponent,
|
||||||
|
DataDetailComponent,
|
||||||
|
SkeletonSimpleCardComponent,
|
||||||
|
SkeletonListItemComponent,
|
||||||
|
DataIconPipe,
|
||||||
|
PlaceListItemComponent,
|
||||||
|
DataDetailContentComponent,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class DataModule {}
|
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
|
<stapps-simple-card
|
||||||
*ngIf="item.description"
|
*ngIf="item.description"
|
||||||
[title]="'Description'"
|
[title]="'Description'"
|
||||||
|
|||||||
@@ -38,24 +38,6 @@
|
|||||||
<stapps-skeleton-simple-card></stapps-skeleton-simple-card>
|
<stapps-skeleton-simple-card></stapps-skeleton-simple-card>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngSwitchDefault>
|
<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>
|
<stapps-data-detail-content [item]="item"></stapps-data-detail-content>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,4 +15,18 @@
|
|||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: 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 {DataListComponent} from './data-list.component';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||||
|
import {ConfigProvider} from '../../config/config.provider';
|
||||||
|
|
||||||
describe('DataListComponent', () => {
|
describe('DataListComponent', () => {
|
||||||
let component: DataListComponent;
|
let component: DataListComponent;
|
||||||
let fixture: ComponentFixture<DataListComponent>;
|
let fixture: ComponentFixture<DataListComponent>;
|
||||||
|
let configProviderMock: jasmine.SpyObj<ConfigProvider>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
|
configProviderMock = jasmine.createSpyObj('ConfigProvider', {
|
||||||
|
getValue: () => Promise.resolve({lat: 123, lng: 123}),
|
||||||
|
});
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [DataListComponent],
|
declarations: [DataListComponent],
|
||||||
imports: [TranslateModule.forRoot()],
|
imports: [TranslateModule.forRoot()],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
|
providers: [{provide: ConfigProvider, useValue: configProviderMock}],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<stapps-context></stapps-context>
|
<stapps-context contentId="data-list"></stapps-context>
|
||||||
<ion-header>
|
<ion-header>
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ion-buttons slot="start">
|
<ion-buttons slot="start">
|
||||||
@@ -13,7 +13,9 @@
|
|||||||
<ion-searchbar
|
<ion-searchbar
|
||||||
(ngModelChange)="searchStringChanged($event)"
|
(ngModelChange)="searchStringChanged($event)"
|
||||||
[(ngModel)]="queryText"
|
[(ngModel)]="queryText"
|
||||||
></ion-searchbar>
|
showClearButton="always"
|
||||||
|
>
|
||||||
|
</ion-searchbar>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {DataProvider} from '../../data.provider';
|
|||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
providers: [DataProvider],
|
providers: [DataProvider],
|
||||||
|
styleUrls: ['place-detail-content.scss'],
|
||||||
selector: 'stapps-place-detail-content',
|
selector: 'stapps-place-detail-content',
|
||||||
templateUrl: 'place-detail-content.html',
|
templateUrl: 'place-detail-content.html',
|
||||||
})
|
})
|
||||||
@@ -41,7 +42,9 @@ export class PlaceDetailContentComponent {
|
|||||||
*
|
*
|
||||||
* @param item TODO
|
* @param item TODO
|
||||||
*/
|
*/
|
||||||
|
// tslint:disable-next-line:completed-docs prefer-function-over-method
|
||||||
hasCategories(item: SCThings): item is SCThings & {categories: string[]} {
|
hasCategories(item: SCThings): item is SCThings & {categories: string[]} {
|
||||||
|
// tslint:disable-next-line:completed-docs
|
||||||
return typeof (item as {categories: string[]}).categories !== 'undefined';
|
return typeof (item as {categories: string[]}).categories !== 'undefined';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<stapps-place-mensa-detail-content
|
<stapps-place-mensa-detail-content
|
||||||
[item]="item"
|
[item]="item"
|
||||||
[language]="language"
|
|
||||||
*ngIf="isMensaThing(item)"
|
*ngIf="isMensaThing(item)"
|
||||||
></stapps-place-mensa-detail-content>
|
></stapps-place-mensa-detail-content>
|
||||||
<ng-container *ngIf="item.type !== 'floor'">
|
<ng-container *ngIf="item.type !== 'floor'">
|
||||||
@@ -26,8 +25,9 @@
|
|||||||
}}</a>
|
}}</a>
|
||||||
</ion-card-content>
|
</ion-card-content>
|
||||||
</ion-card>
|
</ion-card>
|
||||||
<stapps-address-detail
|
</ng-container>
|
||||||
*ngIf="item.inPlace && item.inPlace.address"
|
<ng-container *ngIf="item.type !== 'floor'">
|
||||||
[address]="item.inPlace.address"
|
<ion-card *ngIf="item.geo" class="map-widget">
|
||||||
></stapps-address-detail>
|
<stapps-map-widget [place]="item" expandable="true"></stapps-map-widget>
|
||||||
|
</ion-card>
|
||||||
</ng-container>
|
</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
|
* 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
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
* Software Foundation, version 3.
|
* Software Foundation, version 3.
|
||||||
@@ -14,18 +14,55 @@
|
|||||||
*/
|
*/
|
||||||
import {Component, Input} from '@angular/core';
|
import {Component, Input} from '@angular/core';
|
||||||
import {SCBuilding, SCFloor, SCPointOfInterest, SCRoom} from '@openstapps/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({
|
@Component({
|
||||||
selector: 'stapps-place-list-item',
|
selector: 'stapps-place-list-item',
|
||||||
templateUrl: 'place-list-item.html',
|
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">
|
<p *ngIf="item.description">
|
||||||
{{ 'description' | thingTranslate: item }}
|
{{ 'description' | thingTranslate: item }}
|
||||||
</p>
|
</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>
|
</div>
|
||||||
</ion-col>
|
</ion-col>
|
||||||
<ion-col width-20 text-right *ngIf="item.type !== 'building'">
|
<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
|
* 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
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
* Software Foundation, version 3.
|
* Software Foundation, version 3.
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* 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 {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
SCLanguage,
|
SCLanguage,
|
||||||
@@ -38,6 +38,12 @@ import {FilterContext, SortContext, SortContextOption} from './context-type';
|
|||||||
templateUrl: 'context-menu.html',
|
templateUrl: 'context-menu.html',
|
||||||
})
|
})
|
||||||
export class ContextMenuComponent implements OnDestroy {
|
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
|
* Amount of filter options shown on compact view
|
||||||
*/
|
*/
|
||||||
@@ -145,7 +151,6 @@ export class ContextMenuComponent implements OnDestroy {
|
|||||||
for (const filterBucket of filterFacet.buckets) {
|
for (const filterBucket of filterFacet.buckets) {
|
||||||
filterBucket.checked = false;
|
filterBucket.checked = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.contextMenuService.contextFilterChanged(this.filterOption);
|
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-list-header>
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<h3>{{ 'menu.context.title' | translate | titlecase }}</h3>
|
<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
|
* 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
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
* Software Foundation, version 3.
|
* Software Foundation, version 3.
|
||||||
@@ -13,14 +13,92 @@
|
|||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {SCFacet, SCSearchFilter, SCSearchSort} from '@openstapps/core';
|
|
||||||
import {Subject} from 'rxjs';
|
|
||||||
import {
|
import {
|
||||||
FilterBucket,
|
SCFacet,
|
||||||
FilterContext,
|
SCFacetBucket,
|
||||||
FilterFacet,
|
SCSearchFilter,
|
||||||
SortContext,
|
SCSearchSort,
|
||||||
} from './context-type';
|
} 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
|
* ContextMenuService provides bidirectional communication of context menu options and search queries
|
||||||
@@ -35,13 +113,13 @@ export class ContextMenuService {
|
|||||||
/**
|
/**
|
||||||
* Container for the filter context
|
* Container for the filter context
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/member-ordering
|
// tslint:disable-next-line:member-ordering
|
||||||
filterOptions = new Subject<FilterContext>();
|
filterOptions = new Subject<FilterContext>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable filterContext streams
|
* Observable filterContext streams
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/member-ordering
|
// tslint:disable-next-line:member-ordering
|
||||||
filterContextChanged$ = this.filterOptions.asObservable();
|
filterContextChanged$ = this.filterOptions.asObservable();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,19 +130,19 @@ export class ContextMenuService {
|
|||||||
/**
|
/**
|
||||||
* Observable filterContext streams
|
* Observable filterContext streams
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/member-ordering
|
// tslint:disable-next-line:member-ordering
|
||||||
filterQueryChanged$ = this.filterQuery.asObservable();
|
filterQueryChanged$ = this.filterQuery.asObservable();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Container for the sort context
|
* Container for the sort context
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/member-ordering
|
// tslint:disable-next-line:member-ordering
|
||||||
sortOptions = new Subject<SortContext>();
|
sortOptions = new Subject<SortContext>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable SortContext streams
|
* Observable SortContext streams
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/member-ordering
|
// tslint:disable-next-line:member-ordering
|
||||||
sortContextChanged$ = this.sortOptions.asObservable();
|
sortContextChanged$ = this.sortOptions.asObservable();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,7 +153,7 @@ export class ContextMenuService {
|
|||||||
/**
|
/**
|
||||||
* Observable SortContext streams
|
* Observable SortContext streams
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/member-ordering
|
// tslint:disable-next-line:member-ordering
|
||||||
sortQueryChanged$ = this.sortQuery.asObservable();
|
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": {
|
"data": {
|
||||||
"REFRESH_ACTION": "Aktualisieren",
|
"REFRESH_ACTION": "Aktualisieren",
|
||||||
"REFRESHING": "Aktualisierung läuft...",
|
"REFRESHING": "Aktualisierung läuft...",
|
||||||
@@ -60,8 +68,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"news": {
|
"map": {
|
||||||
"title": "Aktuelles"
|
"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": {
|
"menu": {
|
||||||
"context": {
|
"context": {
|
||||||
@@ -81,6 +112,9 @@
|
|||||||
"settings": "Einstellungen"
|
"settings": "Einstellungen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"news": {
|
||||||
|
"title": "Aktuelles"
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"nothing_found": "Keine Ergebnisse"
|
"nothing_found": "Keine Ergebnisse"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"app": {
|
||||||
|
"ui": {
|
||||||
|
"CLOSE": "Close"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"UNKNOWN": "Unknown problem"
|
||||||
|
}
|
||||||
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"REFRESH_ACTION": "Refresh",
|
"REFRESH_ACTION": "Refresh",
|
||||||
"REFRESHING": "Refreshing...",
|
"REFRESHING": "Refreshing...",
|
||||||
@@ -60,8 +68,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"news": {
|
"map": {
|
||||||
"title": "News"
|
"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": {
|
"menu": {
|
||||||
"context": {
|
"context": {
|
||||||
@@ -81,6 +112,9 @@
|
|||||||
"settings": "settings"
|
"settings": "settings"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"news": {
|
||||||
|
"title": "News"
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"nothing_found": "No results"
|
"nothing_found": "No results"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -78,4 +78,5 @@
|
|||||||
/** StApps **/
|
/** StApps **/
|
||||||
--placeholder-gray: #F1F0ED;
|
--placeholder-gray: #F1F0ED;
|
||||||
/** Change the colors of the toolbar and the toolbar text here **/
|
/** 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