From a37ef3fcaedf3a69076be52b95924b2fd3fb84bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98ystein=20Myhre?= <71138449+gruble@users.noreply.github.com> Date: Fri, 7 Nov 2025 11:18:11 +0100 Subject: [PATCH 01/15] RO-3016 og RO-3015 Lagt til NVE Designsystem og ny fane for planer (#856) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jørgen Loe Kvalberg --- package-lock.json | 184 +++++++++++++++++- package.json | 1 + src/app/pages/plans/plans.page.css | 12 ++ src/app/pages/plans/plans.page.html | 21 ++ src/app/pages/plans/plans.page.ts | 28 +++ src/app/pages/tabs/tabs.page.html | 89 +++++---- src/app/pages/tabs/tabs.page.ts | 12 +- src/app/pages/tabs/tabs.routes.ts | 7 + src/app/pages/tabs/tabs.service.ts | 3 + .../source-sans-3-latin-400-italic.woff2 | Bin 0 -> 15808 bytes .../source-sans-3-latin-400-normal.woff2 | Bin 0 -> 15696 bytes .../source-sans-3-latin-600-italic.woff2 | Bin 0 -> 15736 bytes .../source-sans-3-latin-600-normal.woff2 | Bin 0 -> 15668 bytes src/assets/i18n/en.json | 9 +- src/assets/i18n/nb.json | 9 +- src/assets/icon/input.svg | 1 + src/global.scss | 38 ++++ src/main.ts | 15 +- 18 files changed, 379 insertions(+), 50 deletions(-) create mode 100644 src/app/pages/plans/plans.page.css create mode 100644 src/app/pages/plans/plans.page.html create mode 100644 src/app/pages/plans/plans.page.ts create mode 100644 src/assets/fonts/source-sans-3-latin-400-italic.woff2 create mode 100644 src/assets/fonts/source-sans-3-latin-400-normal.woff2 create mode 100644 src/assets/fonts/source-sans-3-latin-600-italic.woff2 create mode 100644 src/assets/fonts/source-sans-3-latin-600-normal.woff2 create mode 100644 src/assets/icon/input.svg diff --git a/package-lock.json b/package-lock.json index e549411b3..ac377e226 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,6 +77,7 @@ "ngx-file-drop": "^16.0.0", "ngx-logger": "^4.2.1", "ngx-markdown": "^20.0.0", + "nve-designsystem": "^2.16.0", "observable-webworker": "^3.4.0", "pouchdb-adapter-idb": "^7.2.2", "rxjs": "^7.8.1", @@ -4526,6 +4527,15 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.6.3", "dev": true, @@ -5090,6 +5100,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@geoman-io/leaflet-geoman-free": { "version": "2.14.2", "license": "MIT", @@ -6196,6 +6231,30 @@ "dev": true, "license": "MIT" }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.4.0.tgz", + "integrity": "sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/react": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@lit/react/-/react-1.0.8.tgz", + "integrity": "sha512-p2+YcF+JE67SRX3mMlJ1TKCSTsgyOVdAwd/nxp3NuV1+Cb6MWALbN6nT7Ld4tpmYofcE5kcaSY1YBB9erY+6fw==", + "license": "BSD-3-Clause", + "peerDependencies": { + "@types/react": "17 || 18 || 19" + } + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.1.tgz", + "integrity": "sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.4.0" + } + }, "node_modules/@lmdb/lmdb-darwin-arm64": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.4.2.tgz", @@ -8430,6 +8489,45 @@ "node": ">=10" } }, + "node_modules/@shoelace-style/animations": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@shoelace-style/animations/-/animations-1.2.0.tgz", + "integrity": "sha512-avvo1xxkLbv2dgtabdewBbqcJfV0e0zCwFqkPMnHFGbJbBHorRFfMAHh1NG9ymmXn0jW95ibUVH03E1NYXD6Gw==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/claviska" + } + }, + "node_modules/@shoelace-style/localize": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@shoelace-style/localize/-/localize-3.2.1.tgz", + "integrity": "sha512-r4C9C/5kSfMBIr0D9imvpRdCNXtUNgyYThc4YlS6K5Hchv1UyxNQ9mxwj+BTRH2i1Neits260sR3OjKMnplsFA==", + "license": "MIT" + }, + "node_modules/@shoelace-style/shoelace": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/@shoelace-style/shoelace/-/shoelace-2.20.1.tgz", + "integrity": "sha512-FSghU95jZPGbwr/mybVvk66qRZYpx5FkXL+vLNpy1Vp8UsdwSxXjIHE3fsvMbKWTKi9UFfewHTkc5e7jAqRYoQ==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.1.0", + "@floating-ui/dom": "^1.6.12", + "@lit/react": "^1.0.6", + "@shoelace-style/animations": "^1.2.0", + "@shoelace-style/localize": "^3.2.1", + "composed-offset-position": "^0.0.6", + "lit": "^3.2.1", + "qr-creator": "^1.0.0" + }, + "engines": { + "node": ">=14.17.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/claviska" + } + }, "node_modules/@sigstore/bundle": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.1.0.tgz", @@ -14785,6 +14883,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/react": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.0.2" + } + }, "node_modules/@types/responselike": { "version": "1.0.0", "dev": true, @@ -14871,8 +14979,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/@types/websql": { "version": "0.0.27", @@ -16572,6 +16679,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/composed-offset-position": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/composed-offset-position/-/composed-offset-position-0.0.6.tgz", + "integrity": "sha512-Q7dLompI6lUwd7LWyIcP66r4WcS9u7AL2h8HaeipiRfCRPLMWqRx8fYsjb4OHi6UQFifO7XtNC2IlEJ1ozIFxw==", + "license": "MIT", + "peerDependencies": { + "@floating-ui/utils": "^0.2.5" + } + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -17162,6 +17278,13 @@ "node": ">=4" } }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT", + "peer": true + }, "node_modules/custom-event": { "version": "1.0.1", "dev": true, @@ -19794,6 +19917,12 @@ } } }, + "node_modules/fontfaceobserver": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz", + "integrity": "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==", + "license": "BSD-2-Clause" + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -22217,6 +22346,37 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/lit": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.1.tgz", + "integrity": "sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.1.tgz", + "integrity": "sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.4.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.1.tgz", + "integrity": "sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, "node_modules/lmdb": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.4.2.tgz", @@ -24269,6 +24429,20 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nve-designsystem": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/nve-designsystem/-/nve-designsystem-2.16.0.tgz", + "integrity": "sha512-5GYYHtfBBQOLHZjN+r6jX9Fj76IKysuN312THUPc7U5nkzRi+Bl99DZIMejALUjj3xNCmOZBUn7erowxummfVQ==", + "license": "MIT", + "dependencies": { + "@shoelace-style/shoelace": "^2.20.1", + "fontfaceobserver": "^2.3.0", + "lit": "^3.3.0" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "^4.44.2" + } + }, "node_modules/object-assign": { "version": "4.1.1", "dev": true, @@ -25605,6 +25779,12 @@ "node": ">=0.9" } }, + "node_modules/qr-creator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/qr-creator/-/qr-creator-1.0.0.tgz", + "integrity": "sha512-C0cqfbS1P5hfqN4NhsYsUXePlk9BO+a45bAQ3xLYjBL3bOIFzoVEjs79Fado9u9BPBD3buHi3+vY+C8tHh4qMQ==", + "license": "MIT" + }, "node_modules/qs": { "version": "6.13.0", "dev": true, diff --git a/package.json b/package.json index c72d3d8d1..1c7d23403 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "ngx-file-drop": "^16.0.0", "ngx-logger": "^4.2.1", "ngx-markdown": "^20.0.0", + "nve-designsystem": "^2.16.0", "observable-webworker": "^3.4.0", "pouchdb-adapter-idb": "^7.2.2", "rxjs": "^7.8.1", diff --git a/src/app/pages/plans/plans.page.css b/src/app/pages/plans/plans.page.css new file mode 100644 index 000000000..08c0ccf97 --- /dev/null +++ b/src/app/pages/plans/plans.page.css @@ -0,0 +1,12 @@ +:host { + justify-content: unset; +} +.content { + display: flex; + flex-direction: column; + padding: var(--spacing-small); + gap: var(--spacing-small); +} +svg { + fill: currentColor; +} diff --git a/src/app/pages/plans/plans.page.html b/src/app/pages/plans/plans.page.html new file mode 100644 index 000000000..cee6d7c6e --- /dev/null +++ b/src/app/pages/plans/plans.page.html @@ -0,0 +1,21 @@ + + + + + + + + {{ "PLANS.TITLE" | translate }} + +
+

TODO: Planer

+ + {{ "PLANS.IMPORT_BUTTON.CAPTION" | translate }} + + + + @if (showImportmessage()) { + + } +
diff --git a/src/app/pages/plans/plans.page.ts b/src/app/pages/plans/plans.page.ts new file mode 100644 index 000000000..3f0543acd --- /dev/null +++ b/src/app/pages/plans/plans.page.ts @@ -0,0 +1,28 @@ +import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core'; +import { IonButtons, IonMenuButton, IonTitle } from '@ionic/angular/standalone'; +import { TranslatePipe } from '@ngx-translate/core'; +import { HeaderComponent } from 'src/app/modules/shared/components/header/header.component'; +import 'nve-designsystem/components/nve-button/nve-button.component.js'; +import 'nve-designsystem/components/nve-icon/nve-icon.component.js'; +import 'nve-designsystem/components/nve-message-card/nve-message-card.component.js'; + +@Component({ + selector: 'app-plans', + templateUrl: './plans.page.html', + styleUrl: './plans.page.css', + schemas: [CUSTOM_ELEMENTS_SCHEMA], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [HeaderComponent, IonButtons, IonMenuButton, IonTitle, TranslatePipe, TranslatePipe], +}) +/** Side som viser planer og sporfiler */ +export class PlansPage { + showImportmessage = signal(false); + + importGpxFiles = () => { + //TODO: Velg og importer GPX-filer + this.showImportmessage.set(true); + setTimeout(() => { + this.showImportmessage.set(false); + }, 3000); + }; +} diff --git a/src/app/pages/tabs/tabs.page.html b/src/app/pages/tabs/tabs.page.html index b9185f817..78eaf3378 100644 --- a/src/app/pages/tabs/tabs.page.html +++ b/src/app/pages/tabs/tabs.page.html @@ -1,47 +1,60 @@ - - - - - - {{ "TABS.MAP" | translate }} - - - - {{ "TABS.LIST" | translate }} - - @if (isNative) { + @if (!isFullscreen()) { + + @if (selectedTab()) { + - - {{ "TABS.WARNINGS" | translate }} - {{ badgeText() }} + + {{ "TABS.MAP" | translate }} - } @else { - - {{ "TABS.WARNINGS" | translate }} + + {{ "TABS.LIST" | translate }} + + @if (isNative) { + + + {{ "TABS.WARNINGS" | translate }} + @if (showBadge()) { + {{ badgeText() }} + } + + } @else { + + + {{ "TABS.WARNINGS" | translate }} + + } + + + {{ "TABS.PLANS" | translate }} } - - + + } diff --git a/src/app/pages/tabs/tabs.page.ts b/src/app/pages/tabs/tabs.page.ts index 10ffc6b72..1c659e41d 100644 --- a/src/app/pages/tabs/tabs.page.ts +++ b/src/app/pages/tabs/tabs.page.ts @@ -7,10 +7,9 @@ import { GeoHazard, LangKey } from '../../modules/common-core/models'; import { SearchCriteriaService } from '../../core/services/search-criteria/search-criteria.service'; import { WarningService } from '../../core/services/warning/warning.service'; import { TABS, TabsService } from './tabs.service'; -import { NgIf, AsyncPipe } from '@angular/common'; import { TranslatePipe } from '@ngx-translate/core'; import { addIcons } from 'ionicons'; -import { mapOutline, list, warning, openOutline } from 'ionicons/icons'; +import { mapOutline, list, warning, openOutline, analyticsOutline } from 'ionicons/icons'; import { toObservable, toSignal } from '@angular/core/rxjs-interop'; import { BreakpointService } from 'src/app/core/services/breakpoint.service'; import { Capacitor } from '@capacitor/core'; @@ -20,7 +19,7 @@ import { settings } from 'src/settings'; selector: 'app-tabs', templateUrl: 'tabs.page.html', styleUrls: ['tabs.page.scss'], - imports: [AsyncPipe, IonBadge, IonIcon, IonLabel, IonTabBar, IonTabButton, IonTabs, NgIf, TranslatePipe], + imports: [IonBadge, IonIcon, IonLabel, IonTabBar, IonTabButton, IonTabs, TranslatePipe], }) export class TabsPage { private fullscreenService = inject(FullscreenService); @@ -34,14 +33,13 @@ export class TabsPage { private currentGeoHazardSubscription = toSignal(this.userSettingService.currentGeoHazard$, { initialValue: [GeoHazard.NotSpecified], }); - readonly selectedTab$: Observable; + selectedTab = toSignal(this.tabsService.selectedTab$, { initialValue: null }); isFullscreen = toSignal(this.fullscreenService.isFullscreen$, { initialValue: false }); isDesktop = inject(BreakpointService).isDesktop; isNative = Capacitor.isNativePlatform(); constructor() { - this.selectedTab$ = this.tabsService.selectedTab$; - addIcons({ mapOutline, list, warning, openOutline }); + addIcons({ mapOutline, list, warning, openOutline, analyticsOutline }); combineLatest([ this.searchCriteriaService.searchCriteria$, toObservable(this.searchCriteriaService.isExtentCriteriaActive), @@ -102,6 +100,8 @@ export class TabsPage { } }); + iconLayout = computed(() => (this.isDesktop() ? 'icon-start' : 'icon-top')); + private async applyCurrentQueryParams(path: TABS | null) { if (path == TABS.HOME || path == TABS.WARNING_LIST) { await this.searchCriteriaService.applyQueryParams(); diff --git a/src/app/pages/tabs/tabs.routes.ts b/src/app/pages/tabs/tabs.routes.ts index f7cc41d04..9f745fa5f 100644 --- a/src/app/pages/tabs/tabs.routes.ts +++ b/src/app/pages/tabs/tabs.routes.ts @@ -3,6 +3,9 @@ import { TabsPage } from './tabs.page'; import { canActivateStartWizard } from '../../core/guards/start-wizard.guard'; import { desktopBlockGuard } from 'src/app/core/guards/desktop-block.guard'; +/** + * Ruter for navigasjon mellom faner nederst på skjermen + */ export const routes: Routes = [ { path: '', @@ -50,6 +53,10 @@ export const routes: Routes = [ loadComponent: () => import('../warning-list/warning-list.page').then((m) => m.WarningListPage), canActivate: [desktopBlockGuard], }, + { + path: 'plans', + loadComponent: () => import('../plans/plans.page').then((m) => m.PlansPage), + }, // Redirect from old regobs.no route { path: 'observation/search', diff --git a/src/app/pages/tabs/tabs.service.ts b/src/app/pages/tabs/tabs.service.ts index 2f57cacb1..c95f729fb 100644 --- a/src/app/pages/tabs/tabs.service.ts +++ b/src/app/pages/tabs/tabs.service.ts @@ -7,6 +7,7 @@ export enum TABS { HOME = 'home', OBSERVATION_LIST = 'search', WARNING_LIST = 'warning-list', + PLANS = 'plans', } /** @@ -37,6 +38,8 @@ export class TabsService { return TABS.OBSERVATION_LIST; } else if (cleanPath.indexOf(TABS.WARNING_LIST) > -1) { return TABS.WARNING_LIST; + } else if (cleanPath.indexOf(TABS.PLANS) > -1) { + return TABS.PLANS; } else if (cleanPath === '') { return TABS.HOME; } diff --git a/src/assets/fonts/source-sans-3-latin-400-italic.woff2 b/src/assets/fonts/source-sans-3-latin-400-italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..515b6b332a5a1cd1423a077fa39884bc28c66764 GIT binary patch literal 15808 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 CZCF5Xa|i~ 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 aqzmRLuoH$SetW;ZrJeO{{e>r+0RRA?c26?^ literal 0 HcmV?d00001 diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 5895613bd..feac04cb2 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -351,6 +351,12 @@ }, "UNZIP_ERROR_MESSAGE_GENERIC": "Could not save map package to disk. Please try again. On iPhone/iPad you must have the Varsom app active while downloading and unzipping." }, + "PLANS": { + "TITLE": "Plans and tracks", + "IMPORT_BUTTON": { + "CAPTION": "Import GPX files" + } + }, "POPUP_DISCLAMER": { "ABOUT_OBSERVATIONS": { "HEADER": "About observations", @@ -1015,7 +1021,8 @@ "LIST": "List", "MAP": "Map", "TRIPS": "Trips", - "WARNINGS": "Warnings" + "WARNINGS": "Warnings", + "PLANS": "Plans" }, "TRIP": { "COMMENT_CAPTION": "Description (required)", diff --git a/src/assets/i18n/nb.json b/src/assets/i18n/nb.json index 1f9026372..dec43533a 100644 --- a/src/assets/i18n/nb.json +++ b/src/assets/i18n/nb.json @@ -350,6 +350,12 @@ }, "UNZIP_ERROR_MESSAGE_GENERIC": "Klarte ikke lagre kartpakke på disk. Vennligst prøv igjen. På iPhone/iPad må du ha Varsom-appen aktiv inntil kartpakka er lasted ned og pakket ut." }, + "PLANS": { + "TITLE": "Planer og spor", + "IMPORT_BUTTON": { + "CAPTION": "Importer GPX-filer" + } + }, "POPUP_DISCLAMER": { "ABOUT_OBSERVATIONS": { "HEADER": "Om observasjoner", @@ -1014,7 +1020,8 @@ "LIST": "Liste", "MAP": "Kart", "TRIPS": "Turer", - "WARNINGS": "Varsler" + "WARNINGS": "Varsler", + "PLANS": "Planer" }, "TRIP": { "COMMENT_CAPTION": "Beskrivelse (obligatorisk)", diff --git a/src/assets/icon/input.svg b/src/assets/icon/input.svg new file mode 100644 index 000000000..ed46ae92a --- /dev/null +++ b/src/assets/icon/input.svg @@ -0,0 +1 @@ + diff --git a/src/global.scss b/src/global.scss index 2a5333470..435f42c63 100644 --- a/src/global.scss +++ b/src/global.scss @@ -25,6 +25,10 @@ @import '@ionic/angular/css/flex-utils.css'; @import './app/core/helpers/leaflet/user-marker/user-marker.css'; @import '@ionic/angular/css/text-transformation.css'; + +/* Varsom-tema for NVE designsystem */ +@import 'nve-designsystem/css/varsom.css'; + /** * Ionic Dark Mode * ----------------------------------------------------- @@ -55,6 +59,40 @@ a { unicode-range: U+2190-2199, U+2B08, U+2B09, U+2B0A, U+2B0B; } +/* Source Sans Pro brukes av NVE designsystem. TODO: Er ikke helt sikker på om vi trenger italic-variantene */ +@font-face { + font-family: 'Source Sans Pro'; + src: + local('Source Sans Pro'), + url('/fonts/source-sans-3-latin-400-normal.woff2') format('woff2'); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: 'Source Sans Pro'; + src: + local('Source Sans Pro'), + url('/fonts/source-sans-3-latin-400-italic.woff2') format('woff2'); + font-weight: 400; + font-style: italic; +} +@font-face { + font-family: 'Source Sans Pro'; + src: + local('Source Sans Pro'), + url('/fonts/source-sans-3-latin-600-normal.woff2') format('woff2'); + font-weight: 600; + font-style: normal; +} +@font-face { + font-family: 'Source Sans Pro'; + src: + local('Source Sans Pro'), + url('/fonts/source-sans-3-latin-600-italic.woff2') format('woff2'); + font-weight: 600; + font-style: italic; +} + :root { --ion-color-primary: #00425f; --ion-color-primary-rgb: 0, 66, 95; diff --git a/src/main.ts b/src/main.ts index a3c930389..676b85759 100644 --- a/src/main.ts +++ b/src/main.ts @@ -27,8 +27,9 @@ import { httpFactory } from './app/modules/auth/factories/http-factory'; import { AuthService, Browser, DefaultBrowser } from 'ionic-appauth'; import { CapacitorBrowser } from 'ionic-appauth/lib/capacitor'; import { authFactory } from './app/modules/auth/factories/auth-factory'; -import { register } from 'swiper/element/bundle'; +import { register as registerSwiperWebComponents } from 'swiper/element/bundle'; import { mapOldParamsToNew } from './app/core/services/search-criteria/url-params'; +import { icons, registerIconLibrary } from 'nve-designsystem/registerIcons/systemLibraryCustomization.js'; if (environment.production) { enableProdMode(); @@ -46,8 +47,18 @@ function replaceOldParamsWithNew() { } } +// fordi vi overstyrer Shoelace sine system-ikoner i NVE designsystem må vi registrere disse ikonene manuelt +function registerNveDesignsystemIcons() { + registerIconLibrary('system', { + resolver: (name) => { + return `data:image/svg+xml, ${encodeURIComponent(icons[name])}`; + }, + }); +} + function startApp() { - register(); + registerSwiperWebComponents(); + registerNveDesignsystemIcons(); replaceOldParamsWithNew(); console.log('starting app'); From c8073f9ad023b39b38451ba368efde02249f347b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Loe=20Kvalberg?= Date: Fri, 7 Nov 2025 15:05:18 +0100 Subject: [PATCH 02/15] RO-3017: Vise GeoJSON i kartet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Øystein Myhre <71138449+gruble@users.noreply.github.com> --- .../map/components/map/map.component.html | 14 +- .../map/components/map/map.component.ts | 229 ++++++++++++------ 2 files changed, 166 insertions(+), 77 deletions(-) diff --git a/src/app/modules/map/components/map/map.component.html b/src/app/modules/map/components/map/map.component.html index 6c76c5152..7363737d0 100644 --- a/src/app/modules/map/components/map/map.component.html +++ b/src/app/modules/map/components/map/map.component.html @@ -9,18 +9,14 @@ > } -@if (showObserverTrips()) { - diff --git a/src/app/pages/plans/plans.page.ts b/src/app/pages/plans/plans.page.ts index 3f0543acd..68e85e41c 100644 --- a/src/app/pages/plans/plans.page.ts +++ b/src/app/pages/plans/plans.page.ts @@ -5,6 +5,7 @@ import { HeaderComponent } from 'src/app/modules/shared/components/header/header import 'nve-designsystem/components/nve-button/nve-button.component.js'; import 'nve-designsystem/components/nve-icon/nve-icon.component.js'; import 'nve-designsystem/components/nve-message-card/nve-message-card.component.js'; +import { NgxFileDropEntry, NgxFileDropModule } from 'ngx-file-drop'; @Component({ selector: 'app-plans', @@ -12,7 +13,7 @@ import 'nve-designsystem/components/nve-message-card/nve-message-card.component. styleUrl: './plans.page.css', schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [HeaderComponent, IonButtons, IonMenuButton, IonTitle, TranslatePipe, TranslatePipe], + imports: [HeaderComponent, IonButtons, IonMenuButton, IonTitle, TranslatePipe, TranslatePipe, NgxFileDropModule], }) /** Side som viser planer og sporfiler */ export class PlansPage { @@ -25,4 +26,12 @@ export class PlansPage { this.showImportmessage.set(false); }, 3000); }; + /** + * Log GPX filenames when files are dropped in the dropzone + */ + onGpxDrop(gpxFiles: NgxFileDropEntry[]) { + for (const gpxFile of gpxFiles) { + console.log(gpxFile.relativePath); + } + } } From 1e22b8f64368f9cee50d5db90007338d25b6cd3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Loe=20Kvalberg?= Date: Fri, 7 Nov 2025 23:06:49 +0100 Subject: [PATCH 04/15] Konverter gpx til geojson og lagre i db --- package-lock.json | 10 +++ package.json | 1 + .../services/database/database.service.ts | 1 + .../services/geojson/geojson-item.model.ts | 6 ++ .../core/services/geojson/geojson.service.ts | 90 +++++++++++++++++++ src/app/pages/plans/gpx.ts | 19 ++++ src/app/pages/plans/plans.page.html | 6 +- src/app/pages/plans/plans.page.ts | 22 ++++- src/app/pages/plans/utils.ts | 27 ++++++ 9 files changed, 177 insertions(+), 5 deletions(-) create mode 100644 src/app/core/services/geojson/geojson-item.model.ts create mode 100644 src/app/core/services/geojson/geojson.service.ts create mode 100644 src/app/pages/plans/gpx.ts create mode 100644 src/app/pages/plans/utils.ts diff --git a/package-lock.json b/package-lock.json index ac377e226..1b2a0db7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "@ngx-translate/http-loader": "^16.0.1", "@openid/appauth": "^1.3.1", "@sentry/browser": "^7.37.2", + "@tmcw/togeojson": "^7.1.2", "@turf/turf": "^7.1.0", "@types/leaflet-draw": "^1.0.6", "angular-svg-icon": "^16.1.0", @@ -8762,6 +8763,15 @@ "node": ">=10" } }, + "node_modules/@tmcw/togeojson": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@tmcw/togeojson/-/togeojson-7.1.2.tgz", + "integrity": "sha512-QKnFs9DAuqqBVj4d6c69tV1Dj2TspSBTqffivoN0YoBCVdP/JY1+WaYCJbzU49RkoU5NOSOJ3jtFHCdEUVh21A==", + "license": "BSD-2-Clause", + "engines": { + "node": "*" + } + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", diff --git a/package.json b/package.json index 1c7d23403..a638d975e 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "@ngx-translate/http-loader": "^16.0.1", "@openid/appauth": "^1.3.1", "@sentry/browser": "^7.37.2", + "@tmcw/togeojson": "^7.1.2", "@turf/turf": "^7.1.0", "@types/leaflet-draw": "^1.0.6", "angular-svg-icon": "^16.1.0", diff --git a/src/app/core/services/database/database.service.ts b/src/app/core/services/database/database.service.ts index 2b53a0df0..b6f10ca3f 100644 --- a/src/app/core/services/database/database.service.ts +++ b/src/app/core/services/database/database.service.ts @@ -55,6 +55,7 @@ export class DatabaseService { * @returns Returns a promise with the value of the given key */ async get(key: string): Promise { + // TODO: Legg til null på type await firstValueFrom(this.ready$); return this.database.get(key); } diff --git a/src/app/core/services/geojson/geojson-item.model.ts b/src/app/core/services/geojson/geojson-item.model.ts new file mode 100644 index 000000000..d1bad1a89 --- /dev/null +++ b/src/app/core/services/geojson/geojson-item.model.ts @@ -0,0 +1,6 @@ +export interface GeoJSONItem { + id: string; + name: string; + date?: number; + on?: boolean; +} diff --git a/src/app/core/services/geojson/geojson.service.ts b/src/app/core/services/geojson/geojson.service.ts new file mode 100644 index 000000000..994ce1a5b --- /dev/null +++ b/src/app/core/services/geojson/geojson.service.ts @@ -0,0 +1,90 @@ +import { Injectable, effect, inject, signal } from '@angular/core'; +import { DatabaseService } from '../database/database.service'; +import { FeatureCollection } from 'geojson'; +import { GeoJSONItem } from './geojson-item.model'; +import { LoggingService } from 'src/app/modules/shared/services/logging/logging.service'; + +const DEBUG_TAG = 'GeoJSON'; + +@Injectable({ + providedIn: 'root', +}) +export class GeoJSONService { + private db = inject(DatabaseService); + private logger = inject(LoggingService); + + metadata = signal([]); + private initialized = false; + + constructor() { + this.init(); + + effect(() => { + const metadata = this.metadata(); + if (!this.initialized) return; + this.saveMetadata(metadata); + }); + } + + private async init() { + this.logger.debug('Init', DEBUG_TAG); + const items = await this.getMetadata(); + if (items && items.length > 0) { + this.metadata.set(items); + } + setTimeout(() => (this.initialized = true)); // For å unngå en første unødvendig lagring i effecten + } + + private async saveMetadata(items: GeoJSONItem[]) { + this.logger.debug('Saving metadata', DEBUG_TAG, { n: items.length }); + await this.db.set('geojson-metadata', items); + } + + private getMetadata() { + this.logger.debug('Reading metadata', DEBUG_TAG); + return this.db.get('geojson-metadata'); + } + + /** + * Save a geojson object with a given id + * @param id unique id for the geojson + * @param geojson the geojson object + */ + async save(metadata: GeoJSONItem, geojson: FeatureCollection): Promise { + this.logger.debug('Save', DEBUG_TAG, { metadata }); + try { + await this.db.set(`geojson:${metadata.id}`, geojson); + this.metadata.update((items) => [...items, metadata]); + } catch (error) { + this.logger.error(error, DEBUG_TAG, 'Could not save', { metadata, geojson }); + throw error; + } + } + + /** + * Get a geojson object by id + * @param id unique id for the geojson + */ + async get(id: GeoJSONItem['id']): Promise { + this.logger.debug('Get', DEBUG_TAG, { id }); + return this.db.get(`geojson:${id}`); + } + + /** + * Remove a geojson object by id + * @param id unique id for the geojson + */ + async remove(id: GeoJSONItem['id']): Promise { + this.logger.debug('Remove', DEBUG_TAG, { id }); + await this.db.remove(`geojson:${id}`); + this.metadata.update((items) => items.filter((item) => item.id !== id)); + } + + /** + * List all geojson ids + */ + // async listIds(): Promise { + // const keys = await this.db.keys(); + // return keys.filter((k) => k.startsWith('geojson:')).map((k) => k.replace('geojson:', '')); + // } +} diff --git a/src/app/pages/plans/gpx.ts b/src/app/pages/plans/gpx.ts new file mode 100644 index 000000000..3d102ba79 --- /dev/null +++ b/src/app/pages/plans/gpx.ts @@ -0,0 +1,19 @@ +import { FileSystemEntry } from 'ngx-file-drop'; +import { assertIsFileEntry, toText } from './utils'; +import { gpx } from '@tmcw/togeojson'; + +function assertIsGpxFile(fileEntry: FileSystemEntry): asserts fileEntry is FileSystemFileEntry { + assertIsFileEntry(fileEntry); + if (!fileEntry.name.toLowerCase().endsWith('.gpx')) { + throw new Error('fileEntry is not GPX file'); + } +} + +export async function toGeoJSON(fileEntry: FileSystemEntry) { + assertIsGpxFile(fileEntry); + const text = await toText(fileEntry); + const parser = new DOMParser(); + const xml = parser.parseFromString(text, 'application/xml'); + const geojson = gpx(xml); + return geojson; +} diff --git a/src/app/pages/plans/plans.page.html b/src/app/pages/plans/plans.page.html index dca6a877f..4083ef15e 100644 --- a/src/app/pages/plans/plans.page.html +++ b/src/app/pages/plans/plans.page.html @@ -19,12 +19,16 @@

TODO: Planer

} + @for (item of items(); track item.id) { +

{{ item.name }} - {{ item.id }} - {{ item.date }}

+ } + diff --git a/src/app/pages/plans/plans.page.ts b/src/app/pages/plans/plans.page.ts index 68e85e41c..4883eb2c1 100644 --- a/src/app/pages/plans/plans.page.ts +++ b/src/app/pages/plans/plans.page.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, inject, signal } from '@angular/core'; import { IonButtons, IonMenuButton, IonTitle } from '@ionic/angular/standalone'; import { TranslatePipe } from '@ngx-translate/core'; import { HeaderComponent } from 'src/app/modules/shared/components/header/header.component'; @@ -6,6 +6,10 @@ import 'nve-designsystem/components/nve-button/nve-button.component.js'; import 'nve-designsystem/components/nve-icon/nve-icon.component.js'; import 'nve-designsystem/components/nve-message-card/nve-message-card.component.js'; import { NgxFileDropEntry, NgxFileDropModule } from 'ngx-file-drop'; +import { toGeoJSON } from './gpx'; +import { GeoJSONService } from 'src/app/core/services/geojson/geojson.service'; +import { GeoJSONItem } from 'src/app/core/services/geojson/geojson-item.model'; +import { generateShortRandomId } from './utils'; @Component({ selector: 'app-plans', @@ -17,8 +21,11 @@ import { NgxFileDropEntry, NgxFileDropModule } from 'ngx-file-drop'; }) /** Side som viser planer og sporfiler */ export class PlansPage { + private geoJSON = inject(GeoJSONService); showImportmessage = signal(false); + items = this.geoJSON.metadata; + importGpxFiles = () => { //TODO: Velg og importer GPX-filer this.showImportmessage.set(true); @@ -26,12 +33,19 @@ export class PlansPage { this.showImportmessage.set(false); }, 3000); }; + /** * Log GPX filenames when files are dropped in the dropzone */ - onGpxDrop(gpxFiles: NgxFileDropEntry[]) { - for (const gpxFile of gpxFiles) { - console.log(gpxFile.relativePath); + async onFileDrop(files: NgxFileDropEntry[]) { + for (const { fileEntry, relativePath } of files) { + const geojson = await toGeoJSON(fileEntry); + const metadata: GeoJSONItem = { + id: generateShortRandomId(), + name: relativePath, + date: Date.now(), + }; + await this.geoJSON.save(metadata, geojson); } } } diff --git a/src/app/pages/plans/utils.ts b/src/app/pages/plans/utils.ts new file mode 100644 index 000000000..f409c2d8c --- /dev/null +++ b/src/app/pages/plans/utils.ts @@ -0,0 +1,27 @@ +import { FileSystemEntry } from 'ngx-file-drop'; + +export function assertIsFileEntry(fileEntry: FileSystemEntry): asserts fileEntry is FileSystemFileEntry { + if (!fileEntry.isFile) { + throw new Error('fileEntry is not a file'); + } +} + +export function toText(fileEntry: FileSystemFileEntry): Promise { + return new Promise((resolve, reject) => { + fileEntry.file((file: File) => { + file + .text() + .then((text) => resolve(text)) + .catch((reason) => reject(reason)); + }); + }); +} + +export function generateShortRandomId(length = 6) { + // Generate a random number and convert it to a base-36 string (0-9 and a-z). + // The substring(2) removes the "0." prefix from the generated string. + // We then take a substring of the desired length. + return Math.random() + .toString(36) + .substring(2, length + 2); +} From b121ebd2991c8da78757a8c1976715867b6e5525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Loe=20Kvalberg?= Date: Wed, 12 Nov 2025 16:16:11 +0100 Subject: [PATCH 05/15] Rydd opp import-kode --- src/app/pages/plans/plans.page.html | 22 +++++----------------- src/app/pages/plans/plans.page.ts | 9 --------- src/assets/i18n/en.json | 3 ++- src/assets/i18n/nb.json | 3 ++- 4 files changed, 9 insertions(+), 28 deletions(-) diff --git a/src/app/pages/plans/plans.page.html b/src/app/pages/plans/plans.page.html index 4083ef15e..9666cc315 100644 --- a/src/app/pages/plans/plans.page.html +++ b/src/app/pages/plans/plans.page.html @@ -10,28 +10,16 @@

TODO: Planer

- {{ "PLANS.IMPORT_BUTTON.CAPTION" | translate }} - - - - @if (showImportmessage()) { - - } - @for (item of items(); track item.id) {

{{ item.name }} - {{ item.id }} - {{ item.date }}

} - + - +
diff --git a/src/app/pages/plans/plans.page.ts b/src/app/pages/plans/plans.page.ts index 4883eb2c1..6c6a2684a 100644 --- a/src/app/pages/plans/plans.page.ts +++ b/src/app/pages/plans/plans.page.ts @@ -22,18 +22,9 @@ import { generateShortRandomId } from './utils'; /** Side som viser planer og sporfiler */ export class PlansPage { private geoJSON = inject(GeoJSONService); - showImportmessage = signal(false); items = this.geoJSON.metadata; - importGpxFiles = () => { - //TODO: Velg og importer GPX-filer - this.showImportmessage.set(true); - setTimeout(() => { - this.showImportmessage.set(false); - }, 3000); - }; - /** * Log GPX filenames when files are dropped in the dropzone */ diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index feac04cb2..b58813c3d 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -354,7 +354,8 @@ "PLANS": { "TITLE": "Plans and tracks", "IMPORT_BUTTON": { - "CAPTION": "Import GPX files" + "CAPTION": "Import GPX files", + "DROP_LABEL": "Drag and drop or click to add" } }, "POPUP_DISCLAMER": { diff --git a/src/assets/i18n/nb.json b/src/assets/i18n/nb.json index dec43533a..8e7d20695 100644 --- a/src/assets/i18n/nb.json +++ b/src/assets/i18n/nb.json @@ -353,7 +353,8 @@ "PLANS": { "TITLE": "Planer og spor", "IMPORT_BUTTON": { - "CAPTION": "Importer GPX-filer" + "CAPTION": "Importer GPX-filer", + "DROP_LABEL": "Dra og slipp eller klikk for å legge til" } }, "POPUP_DISCLAMER": { From 200fd017a7de47185a16bd13adc4df9d4d20dacd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Loe=20Kvalberg?= Date: Wed, 12 Nov 2025 16:27:16 +0100 Subject: [PATCH 06/15] =?UTF-8?q?Fiks=20visning=20p=C3=A5=20mobil?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/pages/plans/plans.page.html | 8 +++++++- src/app/pages/plans/plans.page.ts | 5 ++++- src/assets/i18n/en.json | 3 ++- src/assets/i18n/nb.json | 3 ++- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/app/pages/plans/plans.page.html b/src/app/pages/plans/plans.page.html index 9666cc315..c7339b26a 100644 --- a/src/app/pages/plans/plans.page.html +++ b/src/app/pages/plans/plans.page.html @@ -18,7 +18,13 @@

TODO: Planer

diff --git a/src/app/pages/plans/plans.page.ts b/src/app/pages/plans/plans.page.ts index 6c6a2684a..39b634a5b 100644 --- a/src/app/pages/plans/plans.page.ts +++ b/src/app/pages/plans/plans.page.ts @@ -1,4 +1,5 @@ -import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, inject, signal } from '@angular/core'; +import { Platform } from '@ionic/angular'; +import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, inject } from '@angular/core'; import { IonButtons, IonMenuButton, IonTitle } from '@ionic/angular/standalone'; import { TranslatePipe } from '@ngx-translate/core'; import { HeaderComponent } from 'src/app/modules/shared/components/header/header.component'; @@ -21,8 +22,10 @@ import { generateShortRandomId } from './utils'; }) /** Side som viser planer og sporfiler */ export class PlansPage { + private platform = inject(Platform); private geoJSON = inject(GeoJSONService); + isMobile = this.platform.is('mobile') || this.platform.is('android') || this.platform.is('ios'); items = this.geoJSON.metadata; /** diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index b58813c3d..66b27e8c4 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -355,7 +355,8 @@ "TITLE": "Plans and tracks", "IMPORT_BUTTON": { "CAPTION": "Import GPX files", - "DROP_LABEL": "Drag and drop or click to add" + "DROP_LABEL_DESKTOP": "Drag and drop or click to add", + "DROP_LABEL_MOBILE": "Tap to add files" } }, "POPUP_DISCLAMER": { diff --git a/src/assets/i18n/nb.json b/src/assets/i18n/nb.json index 8e7d20695..ee2428323 100644 --- a/src/assets/i18n/nb.json +++ b/src/assets/i18n/nb.json @@ -354,7 +354,8 @@ "TITLE": "Planer og spor", "IMPORT_BUTTON": { "CAPTION": "Importer GPX-filer", - "DROP_LABEL": "Dra og slipp eller klikk for å legge til" + "DROP_LABEL_DESKTOP": "Dra og slipp eller klikk for å legge til", + "DROP_LABEL_MOBILE": "Trykk for å legge til filer" } }, "POPUP_DISCLAMER": { From 0f2e2b173ba0fd42c464ca898e5dc2a2a565a7a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Loe=20Kvalberg?= Date: Fri, 14 Nov 2025 14:09:25 +0100 Subject: [PATCH 07/15] RO-3033: listevisning av turer (#869) Co-authored-by: marcin --- src/app/pages/plans/plans.page.css | 37 ++++++++++++++ src/app/pages/plans/plans.page.html | 30 +++++++++-- src/app/pages/plans/plans.page.spec.ts | 62 +++++++++++++++++++++++ src/app/pages/plans/plans.page.ts | 69 +++++++++++++++++++++++--- src/assets/i18n/en.json | 6 ++- src/assets/i18n/nb.json | 6 ++- src/assets/icon/tour.svg | 4 ++ tsconfig.json | 6 +-- 8 files changed, 204 insertions(+), 16 deletions(-) create mode 100644 src/app/pages/plans/plans.page.spec.ts create mode 100644 src/assets/icon/tour.svg diff --git a/src/app/pages/plans/plans.page.css b/src/app/pages/plans/plans.page.css index 08c0ccf97..834ab082b 100644 --- a/src/app/pages/plans/plans.page.css +++ b/src/app/pages/plans/plans.page.css @@ -10,3 +10,40 @@ svg { fill: currentColor; } + +.filter-panel { + display: flex; + justify-content: flex-end; + padding: var(--spacing-small); +} + +nve-select { + width: fit-content; +} + +.text-wrapper { + display: flex; + flex-direction: column; + gap: 4px; +} + +.label { + display: flex; + gap: 10px; + align-items: center; + font: var(--label-medium); +} + +nve-menu-item { + nve-icon { + --icon-size: 30px; + } +} + +nve-menu-item::part(base) { + height: fit-content; +} + +nve-badge::part(base) { + line-height: 1; +} diff --git a/src/app/pages/plans/plans.page.html b/src/app/pages/plans/plans.page.html index c7339b26a..f7dcbf232 100644 --- a/src/app/pages/plans/plans.page.html +++ b/src/app/pages/plans/plans.page.html @@ -10,9 +10,33 @@
+
+ + + + + +
+ + diff --git a/src/app/pages/plans/plans.page.ts b/src/app/pages/plans/plans.page.ts index 20aaee4b4..bd2df9d1e 100644 --- a/src/app/pages/plans/plans.page.ts +++ b/src/app/pages/plans/plans.page.ts @@ -1,6 +1,6 @@ import { Platform } from '@ionic/angular'; import { ChangeDetectionStrategy, Component, computed, CUSTOM_ELEMENTS_SCHEMA, inject, signal } from '@angular/core'; -import { IonButtons, IonMenuButton, IonTitle } from '@ionic/angular/standalone'; +import { IonButtons, IonMenuButton, IonRouterLinkWithHref, IonTitle, IonContent } from '@ionic/angular/standalone'; import { TranslatePipe } from '@ngx-translate/core'; import { HeaderComponent } from 'src/app/modules/shared/components/header/header.component'; import 'nve-designsystem/components/nve-button/nve-button.component.js'; @@ -18,6 +18,7 @@ import { GeoJSONService } from 'src/app/core/services/geojson/geojson.service'; import { GeoJSONItem } from 'src/app/core/services/geojson/geojson-item.model'; import { generateShortRandomId } from './utils'; import { DatePipe } from '@angular/common'; +import { Router, RouterLink } from '@angular/router'; @Component({ selector: 'app-plans', @@ -34,11 +35,15 @@ import { DatePipe } from '@angular/common'; TranslatePipe, TranslatePipe, NgxFileDropModule, + RouterLink, + IonRouterLinkWithHref, + IonContent, ], }) /** Side som viser planer og sporfiler */ export class PlansPage { private platform = inject(Platform); + private router = inject(Router); private geoJSON = inject(GeoJSONService); isMobile = this.platform.is('mobile') || this.platform.is('android') || this.platform.is('ios'); @@ -55,8 +60,13 @@ export class PlansPage { async onFileDrop(files: NgxFileDropEntry[]) { for (const { fileEntry, relativePath } of files) { const geojson = await toGeoJSON(fileEntry); - const metadata: GeoJSONItem = { id: generateShortRandomId(), name: relativePath, date: Date.now() }; + const id = generateShortRandomId(); + const metadata: GeoJSONItem = { id, name: relativePath, date: Date.now() }; await this.geoJSON.save(metadata, geojson); + // Åpne detaljsiden kun når en fil er lastet opp. + if (files.length === 1) { + this.router.navigate(['/plans', id]); + } } } diff --git a/src/app/pages/tabs/tabs.routes.ts b/src/app/pages/tabs/tabs.routes.ts index 9f745fa5f..db6c50ebe 100644 --- a/src/app/pages/tabs/tabs.routes.ts +++ b/src/app/pages/tabs/tabs.routes.ts @@ -57,6 +57,10 @@ export const routes: Routes = [ path: 'plans', loadComponent: () => import('../plans/plans.page').then((m) => m.PlansPage), }, + { + path: 'plans/:id', + loadComponent: () => import('../plans/plan/plan.page').then((m) => m.PlanPage), + }, // Redirect from old regobs.no route { path: 'observation/search', diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 3f06b9928..5a07e628a 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -358,9 +358,15 @@ "DROP_LABEL_DESKTOP": "Drag and drop or click to add", "DROP_LABEL_MOBILE": "Tap to add files" }, + "DELETE": "Delete", "SORT_TITLE": " Sort by", "SORT_BY_NAME": "Name", "SORT_BY_TIME": "Recently updated", + "DATE_EDITED": "Date edited", + "SHOW_ON_MAP": "Show on map", + "HIDE_ON_MAP": "Hide on map", + "FILE_NAME": "File name", + "COMMENT": "Comment", "VISIBLE_ON_MAP": "Visible on map" }, "POPUP_DISCLAMER": { diff --git a/src/assets/i18n/nb.json b/src/assets/i18n/nb.json index c7be34642..78918dcee 100644 --- a/src/assets/i18n/nb.json +++ b/src/assets/i18n/nb.json @@ -357,9 +357,15 @@ "DROP_LABEL_DESKTOP": "Dra og slipp eller klikk for å legge til", "DROP_LABEL_MOBILE": "Trykk for å legge til filer" }, + "DELETE": "Slett", "SORT_TITLE": "Sortering", "SORT_BY_NAME": "Navn", "SORT_BY_TIME": "Sist endret", + "DATE_EDITED": "Dato redigert", + "SHOW_ON_MAP": "Vis i kart", + "HIDE_ON_MAP": "Skjul i kart", + "FILE_NAME": "Filnavn", + "COMMENT": "Kommentar", "VISIBLE_ON_MAP": "Vises i kart" }, "POPUP_DISCLAMER": { diff --git a/src/assets/icon/altitude.svg b/src/assets/icon/altitude.svg new file mode 100644 index 000000000..3fa969218 --- /dev/null +++ b/src/assets/icon/altitude.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icon/arrow_range.svg b/src/assets/icon/arrow_range.svg new file mode 100644 index 000000000..ff649ba06 --- /dev/null +++ b/src/assets/icon/arrow_range.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icon/calendar_today.svg b/src/assets/icon/calendar_today.svg new file mode 100644 index 000000000..59a9af4bf --- /dev/null +++ b/src/assets/icon/calendar_today.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icon/delete_white.svg b/src/assets/icon/delete_white.svg new file mode 100644 index 000000000..892aca9e2 --- /dev/null +++ b/src/assets/icon/delete_white.svg @@ -0,0 +1 @@ + From ae0b85fa489d7372704fd655b246edcf8dfcdc24 Mon Sep 17 00:00:00 2001 From: Marcin Janecki <42612597+amish1188@users.noreply.github.com> Date: Tue, 16 Dec 2025 11:48:55 +0100 Subject: [PATCH 12/15] RO-3072: beregne og vise tur lengde (#894) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Øystein Myhre <71138449+gruble@users.noreply.github.com> Co-authored-by: Jørgen Loe Kvalberg --- .../services/geojson/geojson-item.model.ts | 1 + .../core/services/geojson/geojson.service.ts | 19 ++++++++++++++++++- src/app/pages/plans/plan/plan.page.css | 5 +++++ src/app/pages/plans/plan/plan.page.html | 2 +- src/app/pages/plans/plans.page.css | 4 ++++ src/app/pages/plans/plans.page.html | 7 ++++++- src/assets/i18n/en.json | 3 ++- src/assets/i18n/nb.json | 3 ++- 8 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/app/core/services/geojson/geojson-item.model.ts b/src/app/core/services/geojson/geojson-item.model.ts index e2f756b39..3144ac97e 100644 --- a/src/app/core/services/geojson/geojson-item.model.ts +++ b/src/app/core/services/geojson/geojson-item.model.ts @@ -4,4 +4,5 @@ export interface GeoJSONItem { date?: number; visibleOnMap?: boolean; comment?: string; + lengthKm?: number; } diff --git a/src/app/core/services/geojson/geojson.service.ts b/src/app/core/services/geojson/geojson.service.ts index 5a39e8889..89f619f69 100644 --- a/src/app/core/services/geojson/geojson.service.ts +++ b/src/app/core/services/geojson/geojson.service.ts @@ -1,10 +1,11 @@ import { Injectable, effect, inject, signal } from '@angular/core'; import { DatabaseService } from '../database/database.service'; -import { FeatureCollection } from 'geojson'; +import { Feature, FeatureCollection, GeoJsonProperties, Geometry } from 'geojson'; import { GeoJSONItem } from './geojson-item.model'; import { LoggingService } from 'src/app/modules/shared/services/logging/logging.service'; import { toObservable } from '@angular/core/rxjs-interop'; import { cleanFeatureCollection } from 'src/app/pages/plans/geojson'; +import { length } from '@turf/turf'; const DEBUG_TAG = 'GeoJSON'; @@ -48,6 +49,15 @@ export class GeoJSONService { return this.db.get('geojson-metadata'); } + /** + * Kalkulerer lengden av LineString features i et GeoJSON objekt + * @param geojson + * @returns lengde i kilometer + */ + private calculateLengthKm(geojson: Feature[]): number { + return geojson.reduce((sum, feature) => sum + length(feature), 0); + } + /** * Updates metadata for a given item * @param item the geojson item to update @@ -73,6 +83,13 @@ export class GeoJSONService { } try { + const lineFeatures = geojson.features.filter((f) => f.geometry.type === 'LineString'); + + if (lineFeatures.length) { + const lengthKm = this.calculateLengthKm(lineFeatures); + metadata.lengthKm = lengthKm; + } + await this.db.set(`geojson:${metadata.id}`, geojson); this.metadata.update((items) => [...items, metadata]); } catch (error) { diff --git a/src/app/pages/plans/plan/plan.page.css b/src/app/pages/plans/plan/plan.page.css index c15280a0f..c09303e79 100644 --- a/src/app/pages/plans/plan/plan.page.css +++ b/src/app/pages/plans/plan/plan.page.css @@ -13,6 +13,11 @@ nve-input::part(form-control-label) { color: black; } +nve-tag::part(extra), +nve-tag::part(text) { + line-height: 1; +} + .button-group { display: flex; gap: var(--spacing-small); diff --git a/src/app/pages/plans/plan/plan.page.html b/src/app/pages/plans/plan/plan.page.html index d4ad78b32..135cb60c0 100644 --- a/src/app/pages/plans/plan/plan.page.html +++ b/src/app/pages/plans/plan/plan.page.html @@ -22,7 +22,7 @@ - -- km + {{ info.lengthKm ? `${info.lengthKm.toFixed(2)} km` : '-- km' }}
diff --git a/src/app/pages/plans/plans.page.css b/src/app/pages/plans/plans.page.css index ac516078d..a4af78193 100644 --- a/src/app/pages/plans/plans.page.css +++ b/src/app/pages/plans/plans.page.css @@ -53,3 +53,7 @@ nve-badge::part(base) { .drop-zone-wrapper { height: 200px; } + +a { + text-decoration: none; +} diff --git a/src/app/pages/plans/plans.page.html b/src/app/pages/plans/plans.page.html index 132a4ae64..8dfe82540 100644 --- a/src/app/pages/plans/plans.page.html +++ b/src/app/pages/plans/plans.page.html @@ -30,7 +30,12 @@

TODO: Planer

{{ "PLANS.VISIBLE_ON_MAP" | translate }} } - {{ item.date | date }} + {{ item.date | date }} + @if (item.lengthKm !== undefined && item.lengthKm !== null) { + , {{ item.lengthKm.toFixed(2) }} km + } + diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 5a07e628a..37d2777d2 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -367,7 +367,8 @@ "HIDE_ON_MAP": "Hide on map", "FILE_NAME": "File name", "COMMENT": "Comment", - "VISIBLE_ON_MAP": "Visible on map" + "VISIBLE_ON_MAP": "Visible on map", + "CAPTION": "Import GPX files" }, "POPUP_DISCLAMER": { "ABOUT_OBSERVATIONS": { diff --git a/src/assets/i18n/nb.json b/src/assets/i18n/nb.json index 78918dcee..726744a34 100644 --- a/src/assets/i18n/nb.json +++ b/src/assets/i18n/nb.json @@ -366,7 +366,8 @@ "HIDE_ON_MAP": "Skjul i kart", "FILE_NAME": "Filnavn", "COMMENT": "Kommentar", - "VISIBLE_ON_MAP": "Vises i kart" + "VISIBLE_ON_MAP": "Vises i kart", + "CAPTION": "Importer GPX-filer" }, "POPUP_DISCLAMER": { "ABOUT_OBSERVATIONS": { From 97fe910e275d3406e6f954e7fd35ae17ede92ef8 Mon Sep 17 00:00:00 2001 From: Marcin Janecki <42612597+amish1188@users.noreply.github.com> Date: Tue, 16 Dec 2025 15:15:40 +0100 Subject: [PATCH 13/15] =?UTF-8?q?RO-3046:=20filter=20p=C3=A5=20tur=20plane?= =?UTF-8?q?r=20visning=20-alle=20eller=20kun=20synlige=20i=20kart=20(#897)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/pages/plans/plans.page.css | 12 ++++++++++++ src/app/pages/plans/plans.page.html | 9 ++++++++- src/app/pages/plans/plans.page.ts | 17 +++++++++++++++-- src/assets/i18n/en.json | 3 +++ src/assets/i18n/nb.json | 3 +++ src/assets/icon/check_black.svg | 1 + 6 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 src/assets/icon/check_black.svg diff --git a/src/app/pages/plans/plans.page.css b/src/app/pages/plans/plans.page.css index a4af78193..aba8a6c74 100644 --- a/src/app/pages/plans/plans.page.css +++ b/src/app/pages/plans/plans.page.css @@ -16,6 +16,8 @@ svg { .filter-panel { display: flex; justify-content: flex-end; + align-items: flex-end; + gap: var(--spacing-medium); padding: var(--spacing-small); } @@ -23,6 +25,16 @@ nve-select { width: fit-content; } +@media screen and (max-width: 650px) { + .filter-panel { + flex-direction: column; + align-items: stretch; + } + nve-select { + width: 100%; + } +} + .text-wrapper { display: flex; flex-direction: column; diff --git a/src/app/pages/plans/plans.page.html b/src/app/pages/plans/plans.page.html index 8dfe82540..70dac6856 100644 --- a/src/app/pages/plans/plans.page.html +++ b/src/app/pages/plans/plans.page.html @@ -10,8 +10,15 @@

TODO: Planer

-
+ + {{ "PLANS.SHOW_ALL" | translate }} + {{ "PLANS.SHOW_ONLY_VISIBLE_ON_MAP" | translate }} + {{ "PLANS.SORT_BY_NAME" | translate }} {{ "PLANS.SORT_BY_TIME" | translate }} diff --git a/src/app/pages/plans/plans.page.ts b/src/app/pages/plans/plans.page.ts index bd2df9d1e..4dc884a58 100644 --- a/src/app/pages/plans/plans.page.ts +++ b/src/app/pages/plans/plans.page.ts @@ -11,7 +11,6 @@ import 'nve-designsystem/components/nve-badge/nve-badge.component.js'; import 'nve-designsystem/components/nve-select/nve-select.component.js'; import 'nve-designsystem/components/nve-option/nve-option.component.js'; import 'nve-designsystem/components/nve-icon/nve-icon.component.js'; -import 'nve-designsystem/components/nve-message-card/nve-message-card.component.js'; import { NgxFileDropEntry, NgxFileDropModule } from 'ngx-file-drop'; import { toGeoJSON } from './utils'; import { GeoJSONService } from 'src/app/core/services/geojson/geojson.service'; @@ -49,9 +48,14 @@ export class PlansPage { isMobile = this.platform.is('mobile') || this.platform.is('android') || this.platform.is('ios'); sortValue = signal<'name' | 'date'>('date'); + visibilityFilter = signal<'all' | 'onlyVisibleOnMap'>('all'); items = computed(() => { const sorter = sortFunctions[this.sortValue()]; - return sorter(this.geoJSON.metadata()); + const sortedItems = sorter(this.geoJSON.metadata()); + if (this.visibilityFilter() === 'onlyVisibleOnMap') { + return sortedItems.filter((item) => item.visibleOnMap); + } + return sortedItems; }); /** @@ -78,6 +82,15 @@ export class PlansPage { const value = select.value as 'name' | 'date'; this.sortValue.set(value); } + + /** + * Hånderer visning av planer filter når bruker endrer valg i select + */ + onVisibilityFilterChange(event: Event) { + const select = event.target as HTMLSelectElement; + const value = select.value as 'all' | 'onlyVisibleOnMap'; + this.visibilityFilter.set(value); + } } /** diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 37d2777d2..76a7dc330 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -365,6 +365,9 @@ "DATE_EDITED": "Date edited", "SHOW_ON_MAP": "Show on map", "HIDE_ON_MAP": "Hide on map", + "SHOW": "Show", + "SHOW_ONLY_VISIBLE_ON_MAP": "Only visible on map", + "SHOW_ALL": "All", "FILE_NAME": "File name", "COMMENT": "Comment", "VISIBLE_ON_MAP": "Visible on map", diff --git a/src/assets/i18n/nb.json b/src/assets/i18n/nb.json index 726744a34..dd9f8419b 100644 --- a/src/assets/i18n/nb.json +++ b/src/assets/i18n/nb.json @@ -363,6 +363,9 @@ "SORT_BY_TIME": "Sist endret", "DATE_EDITED": "Dato redigert", "SHOW_ON_MAP": "Vis i kart", + "SHOW": "Vis", + "SHOW_ONLY_VISIBLE_ON_MAP": "Kun synlige i kart", + "SHOW_ALL": "Alle", "HIDE_ON_MAP": "Skjul i kart", "FILE_NAME": "Filnavn", "COMMENT": "Kommentar", diff --git a/src/assets/icon/check_black.svg b/src/assets/icon/check_black.svg new file mode 100644 index 000000000..d3831a2cb --- /dev/null +++ b/src/assets/icon/check_black.svg @@ -0,0 +1 @@ + From 46b3683fdc43671013aaff94ce9119e63897f1a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98ystein=20Myhre?= <71138449+gruble@users.noreply.github.com> Date: Tue, 16 Dec 2025 16:18:49 +0100 Subject: [PATCH 14/15] =?UTF-8?q?RO-3069:=20Fikset=20sm=C3=A5ting=20p?= =?UTF-8?q?=C3=A5=20planer=20(#898)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../map/components/map/map.component.ts | 106 ++---------------- src/app/pages/plans/plan/plan.page.css | 8 +- src/app/pages/plans/plan/plan.page.html | 16 +-- src/app/pages/plans/plan/plan.page.ts | 8 +- src/app/pages/plans/plans.page.css | 3 +- src/app/pages/plans/plans.page.html | 1 - src/app/pages/plans/plans.page.ts | 2 +- src/app/pages/tabs/tabs.page.html | 2 +- src/assets/i18n/en.json | 14 +-- src/assets/i18n/nb.json | 15 ++- src/assets/icon/tour.svg | 2 +- 11 files changed, 49 insertions(+), 128 deletions(-) diff --git a/src/app/modules/map/components/map/map.component.ts b/src/app/modules/map/components/map/map.component.ts index bf72832e5..f194b429a 100644 --- a/src/app/modules/map/components/map/map.component.ts +++ b/src/app/modules/map/components/map/map.component.ts @@ -45,98 +45,13 @@ import { LeafletModule } from '@bluehalo/ngx-leaflet'; import { MapControlsComponent } from '../map-controls/map-controls.component'; import type { FeatureCollection } from 'geojson'; import { GeoJSONService } from 'src/app/core/services/geojson/geojson.service'; +import { GeoJSONItem } from 'src/app/core/services/geojson/geojson-item.model'; +import { TranslateService } from '@ngx-translate/core'; const DEBUG_TAG = 'MapComponent'; -const noObserverTripDescription = 'Turen har ikke beskrivelse'; const observerTripsMinZoom = 10; -// TODO: Slett senere -const testGeojson1: FeatureCollection = { - type: 'FeatureCollection', - features: [ - { - type: 'Feature', - properties: {}, - geometry: { - coordinates: [ - [10.434983145022613, 59.85676353864457], - [10.438120130858607, 59.858023738491084], - [10.439270358998897, 59.859178879751596], - [10.440420587139272, 59.86080651072464], - [10.440943418111715, 59.862591562806955], - [10.441152550500334, 59.86448151354634], - [10.44136168288992, 59.8674737159337], - [10.439584057582863, 59.87030818567084], - [10.444707801115584, 59.872670059237464], - [10.452654831901441, 59.872827511510394], - [10.455059854376373, 59.87062311183959], - [10.451295471372532, 59.86736873093335], - [10.447635654562987, 59.86427152432276], - [10.445021499699465, 59.86316905914833], - ], - type: 'LineString', - }, - }, - { - type: 'Feature', - properties: {}, - geometry: { - coordinates: [10.444422542402833, 59.86307021223675], - type: 'Point', - }, - }, - { - type: 'Feature', - properties: {}, - geometry: { - coordinates: [10.43974311270091, 59.8597976822449], - type: 'Point', - }, - }, - { - type: 'Feature', - properties: {}, - geometry: { - coordinates: [10.447152209729126, 59.86894317654611], - type: 'Point', - }, - }, - ], -}; - -const testGeojson2: FeatureCollection = { - type: 'FeatureCollection', - features: [ - { - type: 'Feature', - properties: {}, - geometry: { - coordinates: [ - [10.407297796622316, 59.85167139553894], - [10.399870074640745, 59.85012867124155], - [10.391046164316293, 59.84920300233472], - [10.38713683695724, 59.84822120414702], - [10.385014630677546, 59.84771626808717], - [10.384679545475137, 59.84889444031086], - [10.385852343682615, 59.85015672141279], - [10.38713683695724, 59.85088601756061], - [10.38803039749638, 59.8514189546388], - ], - type: 'LineString', - }, - }, - { - type: 'Feature', - properties: {}, - geometry: { - coordinates: [10.384791240542256, 59.848249255925964], - type: 'Point', - }, - }, - ], -}; - export const isTopoMapLayer = (mapId: string) => (Object.values(TopoMapLayer)).includes(mapId); const redrawLayersInLayerGroup = (layerGroup: L.LayerGroup) => { layerGroup.eachLayer((layer) => { @@ -181,6 +96,7 @@ export class MapComponent implements OnInit, OnDestroy, AfterViewInit { private mapZoomService = inject(MapZoomService); private observerTripsService = inject(ObserverTripsService); private geoJSONService = inject(GeoJSONService); + private translateService = inject(TranslateService); readonly showControls = input(true); readonly showZoomButtons = input(true); @@ -344,8 +260,9 @@ export class MapComponent implements OnInit, OnDestroy, AfterViewInit { * @param map Leaflet map * @param id Unique id for the geojson layer * @param geojson FeatureCollection to add + * @param metadata Metadata associated with the geojson layer */ - private async addGeojsonLayer(map: L.Map, id: string, geojson: FeatureCollection) { + private async addGeojsonLayer(map: L.Map, id: string, geojson: FeatureCollection, metadata?: GeoJSONItem) { const pointIcon = L.icon({ iconUrl: '/assets/icon/map/prev-used-place.svg', iconSize: [25, 41], @@ -375,8 +292,12 @@ export class MapComponent implements OnInit, OnDestroy, AfterViewInit { // Click handler for this geojson const setMetadata = (e: L.LeafletMouseEvent) => { - this.metadataName.set(e.layer?.feature?.properties?.navn || 'Mangler navn'); - this.metadataDescription.set(e.layer?.feature?.properties?.beskrivelse || noObserverTripDescription); + const missingName = this.translateService.instant('PLANS.MISSING_NAME'); + const missingDescription = this.translateService.instant('PLANS.MISSING_COMMENT'); + const name = metadata?.name || e.layer?.feature?.properties?.navn || missingName; + const description = metadata?.comment || e.layer?.feature?.properties?.beskrivelse || missingDescription; + this.metadataName.set(name); + this.metadataDescription.set(description); }; // Zoom handler for this geojson @@ -566,7 +487,7 @@ export class MapComponent implements OnInit, OnDestroy, AfterViewInit { // Hent oppdatert geojson fra tjenesten const geojson = await this.geoJSONService.get(trackMetadata.id); if (geojson) { - this.addGeojsonLayer(map, trackMetadata.id, geojson); + this.addGeojsonLayer(map, trackMetadata.id, geojson, trackMetadata); } }); } @@ -587,9 +508,6 @@ export class MapComponent implements OnInit, OnDestroy, AfterViewInit { .subscribe(() => this.redrawMap()); this.mapReady.emit(map); - - this.addGeojsonLayer(map, 'test', testGeojson1); - this.addGeojsonLayer(map, 'test', testGeojson2); } private async initOfflineMaps() { diff --git a/src/app/pages/plans/plan/plan.page.css b/src/app/pages/plans/plan/plan.page.css index c09303e79..e3b73dcf9 100644 --- a/src/app/pages/plans/plan/plan.page.css +++ b/src/app/pages/plans/plan/plan.page.css @@ -34,5 +34,11 @@ nve-tag::part(text) { display: flex; flex-direction: column; gap: var(--spacing-medium); - padding: var(--spacing-medium) 0px; + padding: var(--spacing-small); +} + +form { + display: flex; + flex-direction: column; + gap: var(--spacing-medium); } diff --git a/src/app/pages/plans/plan/plan.page.html b/src/app/pages/plans/plan/plan.page.html index 135cb60c0..aab5972d0 100644 --- a/src/app/pages/plans/plan/plan.page.html +++ b/src/app/pages/plans/plan/plan.page.html @@ -14,7 +14,7 @@
- {{ "PLANS.DATE_EDITED" | translate }} {{ info.date | date }} + {{ "PLANS.DATE_EDITED" | translate }} {{ info.date | date: "short" }} @@ -25,15 +25,11 @@ {{ info.lengthKm ? `${info.lengthKm.toFixed(2)} km` : '-- km' }}
- - - {{ visibleOnMap() ? ("PLANS.HIDE_ON_MAP" | translate) : ("PLANS.SHOW_ON_MAP" | translate) }} - - + + + {{ "PLANS.VISIBLE_ON_MAP" | translate }} + +
-

TODO: Planer

- + {{ "TABS.PLANS" | translate }} } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 76a7dc330..1b01a7e24 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -354,24 +354,24 @@ "PLANS": { "TITLE": "Plans and tracks", "IMPORT_BUTTON": { - "CAPTION": "Import GPX files", + "CAPTION": "Import GPX/GeoJSON files", "DROP_LABEL_DESKTOP": "Drag and drop or click to add", "DROP_LABEL_MOBILE": "Tap to add files" }, "DELETE": "Delete", + "MISSING_COMMENT": "Description/comment missing", + "MISSING_NAME": "Track without name", + "NAME": "Name", "SORT_TITLE": " Sort by", "SORT_BY_NAME": "Name", - "SORT_BY_TIME": "Recently updated", - "DATE_EDITED": "Date edited", - "SHOW_ON_MAP": "Show on map", - "HIDE_ON_MAP": "Hide on map", + "SORT_BY_TIME": "Last saved", + "DATE_EDITED": "Last saved", "SHOW": "Show", "SHOW_ONLY_VISIBLE_ON_MAP": "Only visible on map", "SHOW_ALL": "All", "FILE_NAME": "File name", "COMMENT": "Comment", - "VISIBLE_ON_MAP": "Visible on map", - "CAPTION": "Import GPX files" + "VISIBLE_ON_MAP": "Visible on map" }, "POPUP_DISCLAMER": { "ABOUT_OBSERVATIONS": { diff --git a/src/assets/i18n/nb.json b/src/assets/i18n/nb.json index dd9f8419b..b73ca145c 100644 --- a/src/assets/i18n/nb.json +++ b/src/assets/i18n/nb.json @@ -353,24 +353,24 @@ "PLANS": { "TITLE": "Planer og spor", "IMPORT_BUTTON": { - "CAPTION": "Importer GPX-filer", + "CAPTION": "Importer GPX/GeoJSON-filer", "DROP_LABEL_DESKTOP": "Dra og slipp eller klikk for å legge til", "DROP_LABEL_MOBILE": "Trykk for å legge til filer" }, "DELETE": "Slett", + "MISSING_COMMENT": "Beskrivelse/kommentar mangler", + "MISSING_NAME": "Spor uten navn", + "NAME": "Navn", "SORT_TITLE": "Sortering", "SORT_BY_NAME": "Navn", - "SORT_BY_TIME": "Sist endret", - "DATE_EDITED": "Dato redigert", - "SHOW_ON_MAP": "Vis i kart", + "SORT_BY_TIME": "Sist lagret", + "DATE_EDITED": "Sist lagret", "SHOW": "Vis", "SHOW_ONLY_VISIBLE_ON_MAP": "Kun synlige i kart", "SHOW_ALL": "Alle", - "HIDE_ON_MAP": "Skjul i kart", "FILE_NAME": "Filnavn", "COMMENT": "Kommentar", - "VISIBLE_ON_MAP": "Vises i kart", - "CAPTION": "Importer GPX-filer" + "VISIBLE_ON_MAP": "Vises i kart" }, "POPUP_DISCLAMER": { "ABOUT_OBSERVATIONS": { @@ -1035,7 +1035,6 @@ "TABS": { "LIST": "Liste", "MAP": "Kart", - "TRIPS": "Turer", "WARNINGS": "Varsler", "PLANS": "Planer" }, diff --git a/src/assets/icon/tour.svg b/src/assets/icon/tour.svg index e5c393534..f5eca29b3 100644 --- a/src/assets/icon/tour.svg +++ b/src/assets/icon/tour.svg @@ -1,4 +1,4 @@ - + From 939e4dc67caf9d9645b44a346c7da38bdc937024 Mon Sep 17 00:00:00 2001 From: marcin Date: Tue, 16 Dec 2025 15:49:37 +0100 Subject: [PATCH 15/15] show map on plan detail add map to plan detail update asd --- .../modules/map-image/map-image.component.ts | 1 + src/app/pages/plans/plan/plan.page.css | 11 ++ src/app/pages/plans/plan/plan.page.html | 2 +- src/app/pages/plans/plan/plan.page.ts | 120 +++++++++++++++++- 4 files changed, 127 insertions(+), 7 deletions(-) diff --git a/src/app/modules/map-image/map-image.component.ts b/src/app/modules/map-image/map-image.component.ts index ef3775fc3..ec46766a4 100644 --- a/src/app/modules/map-image/map-image.component.ts +++ b/src/app/modules/map-image/map-image.component.ts @@ -50,6 +50,7 @@ export class MapImageComponent { ...x.layerConfig.options, }) ); + const mapSettings: L.MapOptions = { zoom: settings.map.tiles.zoomLevelObservationList, maxZoom: settings.map.tiles.maxZoom, diff --git a/src/app/pages/plans/plan/plan.page.css b/src/app/pages/plans/plan/plan.page.css index e3b73dcf9..0c3f103f4 100644 --- a/src/app/pages/plans/plan/plan.page.css +++ b/src/app/pages/plans/plan/plan.page.css @@ -42,3 +42,14 @@ form { flex-direction: column; gap: var(--spacing-medium); } + +.map { + height: 400px; + width: 100%; +} + +@media screen and (max-width: 650px) { + .map { + height: 200px; + } +} diff --git a/src/app/pages/plans/plan/plan.page.html b/src/app/pages/plans/plan/plan.page.html index aab5972d0..8a01da455 100644 --- a/src/app/pages/plans/plan/plan.page.html +++ b/src/app/pages/plans/plan/plan.page.html @@ -1,5 +1,4 @@ @if (itemMetadata(); as info) { - @@ -11,6 +10,7 @@
+
diff --git a/src/app/pages/plans/plan/plan.page.ts b/src/app/pages/plans/plan/plan.page.ts index 447b8c972..401d17b5e 100644 --- a/src/app/pages/plans/plan/plan.page.ts +++ b/src/app/pages/plans/plan/plan.page.ts @@ -1,5 +1,13 @@ import { Component, inject, input, CUSTOM_ELEMENTS_SCHEMA, computed, linkedSignal } from '@angular/core'; -import { IonToolbar, IonContent, IonBackButton, IonTitle, IonHeader, IonButtons } from '@ionic/angular/standalone'; +import { + IonToolbar, + IonContent, + IonBackButton, + IonTitle, + IonHeader, + IonButtons, + isPlatform, +} from '@ionic/angular/standalone'; import { GeoJSONService } from 'src/app/core/services/geojson/geojson.service'; import { DatePipe } from '@angular/common'; import 'nve-designsystem/components/nve-icon/nve-icon.component.js'; @@ -11,25 +19,125 @@ import 'nve-designsystem/components/nve-checkbox/nve-checkbox.component.js'; import 'nve-designsystem/components/nve-tag/nve-tag.component.js'; import { TranslatePipe } from '@ngx-translate/core'; import { Router } from '@angular/router'; +import L from 'leaflet'; +import { LineString, Point } from 'geojson'; +import { LeafletModule } from '@bluehalo/ngx-leaflet'; +import { settings } from 'src/settings'; +import { + MapLayersService, + OfflineCapableMapLayersService, +} from 'src/app/modules/static-map-image/static-tiles.service'; @Component({ selector: 'app-plan.page', templateUrl: './plan.page.html', schemas: [CUSTOM_ELEMENTS_SCHEMA], - imports: [DatePipe, IonContent, IonToolbar, IonBackButton, IonTitle, IonHeader, IonButtons, TranslatePipe], + imports: [ + DatePipe, + IonContent, + IonToolbar, + IonBackButton, + IonTitle, + IonHeader, + IonButtons, + TranslatePipe, + LeafletModule, + ], + providers: [ + { + provide: MapLayersService, + useClass: isPlatform('hybrid') ? OfflineCapableMapLayersService : MapLayersService, + }, + ], styleUrl: './plan.page.css', }) export class PlanPage { - geoJSON = inject(GeoJSONService); + geoJSONService = inject(GeoJSONService); + private mapLayersService = inject(MapLayersService); router = inject(Router); id = input.required(); + private map?: L.Map; - itemMetadata = computed(() => this.geoJSON.metadata().find((m) => m.id === this.id())); + private layerGroup = L.layerGroup(); + + itemMetadata = computed(() => this.geoJSONService.metadata().find((m) => m.id === this.id())); name = linkedSignal(() => this.itemMetadata()?.name || ''); comment = linkedSignal(() => this.itemMetadata()?.comment || ''); visibleOnMap = linkedSignal(() => this.itemMetadata()?.visibleOnMap || false); + mapOptions = computed(() => { + const mapConfig = this.mapLayersService.mapConfig(); + const layersConfig = mapConfig.map((map) => ({ + layerId: map.layer, + layerConfig: settings.map.tiles.topoMapLayers[map.layer], + })); + const layers = layersConfig.map((x) => + L.tileLayer(x.layerConfig.url, { + minZoom: settings.map.tiles.minZoom, + maxZoom: settings.map.tiles.maxZoom, + updateWhenIdle: settings.map.tiles.updateWhenIdle, + ...x.layerConfig.options, + }) + ); + const mapSettings: L.MapOptions = { + zoom: settings.map.tiles.zoomLevelObservationList, + maxZoom: settings.map.tiles.maxZoom, + minZoom: 8, + bounceAtZoomLimits: false, + attributionControl: false, + zoomControl: false, + layers, + }; + return mapSettings; + }); + + async calculateAndCenterMap() { + const geoJSON = await this.geoJSONService.get(this.id()); + + if (!geoJSON || !this.map) return; + + this.layerGroup.clearLayers(); + const geoJsonLayer = L.geoJSON(geoJSON); + this.layerGroup.addLayer(geoJsonLayer); + + if (!this.map.hasLayer(this.layerGroup)) { + this.layerGroup.addTo(this.map); + } + + const lineFeatures = geoJSON.features.map((f) => { + if (f.geometry && f.geometry.type === 'LineString') { + return f.geometry as LineString; + } + return null; + }); + const pointFeatures = geoJSON.features.map((f) => { + if (f.geometry && f.geometry.type === 'Point') { + return f.geometry as Point; + } + return null; + }); + + const lineCoords: [number, number][] = lineFeatures + .filter((f): f is LineString => f !== null) + .flatMap((f) => f.coordinates as [number, number][]); + + const pointCoords: [number, number][] = pointFeatures + .filter((f): f is Point => f !== null) + .map((f) => f.coordinates as [number, number]); + + const flatCoords: [number, number][] = [...lineCoords, ...pointCoords]; + + const latLngs = flatCoords.map(([lng, lat]) => L.latLng(lat, lng)); + + this.map.fitBounds(L.latLngBounds(latLngs), { padding: [5, 5] }); + } + + onMapReady(map: L.Map) { + this.map = map; + this.calculateAndCenterMap(); + } + onNameChange(event: Event) { const value = (event.target as HTMLInputElement).value; this.name.set(value); @@ -45,7 +153,7 @@ export class PlanPage { } async onRemove() { - await this.geoJSON.remove(this.id()); + await this.geoJSONService.remove(this.id()); await this.router.navigate(['/plans']); } @@ -58,7 +166,7 @@ export class PlanPage { lengthKm: this.itemMetadata()?.lengthKm, date: Date.now(), }; - await this.geoJSON.updateMetadata(itemToUpdate); + this.geoJSONService.updateMetadata(itemToUpdate); await this.router.navigate(['/plans']); } }

TODO: Planer

- @for (item of items(); track item.id) { -

{{ item.name }} - {{ item.id }} - {{ item.date }}

- } +
+ + {{ "PLANS.SORT_BY_NAME" | translate }} + {{ "PLANS.SORT_BY_TIME" | translate }} + +
+ + + @for (item of items(); track item.id; let last = $last) { + + +
+ {{ item.name }} + @if (item.on) { + {{ "PLANS.VISIBLE_ON_MAP" | translate }} + } + + {{ item.date | date }} +
+
+ @if (!last) { + + } + } +
diff --git a/src/app/pages/plans/plans.page.spec.ts b/src/app/pages/plans/plans.page.spec.ts new file mode 100644 index 000000000..56717500f --- /dev/null +++ b/src/app/pages/plans/plans.page.spec.ts @@ -0,0 +1,62 @@ +import { sortByName, sortByDate } from './plans.page'; +import { GeoJSONItem } from 'src/app/core/services/geojson/geojson-item.model'; + +describe('sortByName', () => { + it('sorts items alphabetically by name', () => { + const items: GeoJSONItem[] = [ + { id: '1', name: 'Charlie' }, + { id: '2', name: 'Alice' }, + { id: '3', name: 'Bob' }, + ]; + const sorted = sortByName(items); + expect(sorted.map((i) => i.name)).toEqual(['Alice', 'Bob', 'Charlie']); + }); + + it('does not mutate the original array', () => { + const items: GeoJSONItem[] = [ + { id: '1', name: 'Charlie' }, + { id: '2', name: 'Alice' }, + { id: '3', name: 'Bob' }, + ]; + const itemsCopy = [...items]; + const sorted = sortByName(items); + expect(sorted).not.toBe(items); + expect(items).toEqual(itemsCopy); + }); +}); + +describe('sortByDate', () => { + it('sorts items by date, newest first', () => { + const items: GeoJSONItem[] = [ + { id: '1', name: 'A', date: 100 }, + { id: '2', name: 'B', date: 300 }, + { id: '3', name: 'C', date: 200 }, + ]; + const sorted = sortByDate(items); + expect(sorted.map((i) => i.date)).toEqual([300, 200, 100]); + }); + + it('does not mutate the original array', () => { + const items: GeoJSONItem[] = [ + { id: '1', name: 'A', date: 100 }, + { id: '2', name: 'B', date: 300 }, + { id: '3', name: 'C', date: 200 }, + ]; + const itemsCopy = [...items]; + const sorted = sortByDate(items); + expect(sorted).not.toBe(items); + expect(items).toEqual(itemsCopy); + }); + + it('handles items with missing dates', () => { + const items: GeoJSONItem[] = [ + { id: '1', name: 'A', date: 100 }, + { id: '2', name: 'B' }, + { id: '3', name: 'C', date: 200 }, + ]; + const sorted = sortByDate(items); + expect(sorted[0].date).toBe(200); + expect(sorted[1].date).toBe(100); + expect(sorted[2].date).toBeUndefined(); + }); +}); diff --git a/src/app/pages/plans/plans.page.ts b/src/app/pages/plans/plans.page.ts index 39b634a5b..d67223211 100644 --- a/src/app/pages/plans/plans.page.ts +++ b/src/app/pages/plans/plans.page.ts @@ -1,9 +1,15 @@ import { Platform } from '@ionic/angular'; -import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, CUSTOM_ELEMENTS_SCHEMA, inject, signal } from '@angular/core'; import { IonButtons, IonMenuButton, IonTitle } from '@ionic/angular/standalone'; import { TranslatePipe } from '@ngx-translate/core'; import { HeaderComponent } from 'src/app/modules/shared/components/header/header.component'; import 'nve-designsystem/components/nve-button/nve-button.component.js'; +import 'nve-designsystem/components/nve-menu/nve-menu.component.js'; +import 'nve-designsystem/components/nve-menu-item/nve-menu-item.component.js'; +import 'nve-designsystem/components/nve-divider/nve-divider.component.js'; +import 'nve-designsystem/components/nve-badge/nve-badge.component.js'; +import 'nve-designsystem/components/nve-select/nve-select.component.js'; +import 'nve-designsystem/components/nve-option/nve-option.component.js'; import 'nve-designsystem/components/nve-icon/nve-icon.component.js'; import 'nve-designsystem/components/nve-message-card/nve-message-card.component.js'; import { NgxFileDropEntry, NgxFileDropModule } from 'ngx-file-drop'; @@ -11,6 +17,7 @@ import { toGeoJSON } from './gpx'; import { GeoJSONService } from 'src/app/core/services/geojson/geojson.service'; import { GeoJSONItem } from 'src/app/core/services/geojson/geojson-item.model'; import { generateShortRandomId } from './utils'; +import { DatePipe } from '@angular/common'; @Component({ selector: 'app-plans', @@ -18,7 +25,16 @@ import { generateShortRandomId } from './utils'; styleUrl: './plans.page.css', schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [HeaderComponent, IonButtons, IonMenuButton, IonTitle, TranslatePipe, TranslatePipe, NgxFileDropModule], + imports: [ + HeaderComponent, + IonButtons, + DatePipe, + IonMenuButton, + IonTitle, + TranslatePipe, + TranslatePipe, + NgxFileDropModule, + ], }) /** Side som viser planer og sporfiler */ export class PlansPage { @@ -26,7 +42,12 @@ export class PlansPage { private geoJSON = inject(GeoJSONService); isMobile = this.platform.is('mobile') || this.platform.is('android') || this.platform.is('ios'); - items = this.geoJSON.metadata; + + sortValue = signal<'name' | 'date'>('date'); + items = computed(() => { + const sorter = sortFunctions[this.sortValue()]; + return sorter(this.geoJSON.metadata()); + }); /** * Log GPX filenames when files are dropped in the dropzone @@ -34,12 +55,44 @@ export class PlansPage { async onFileDrop(files: NgxFileDropEntry[]) { for (const { fileEntry, relativePath } of files) { const geojson = await toGeoJSON(fileEntry); - const metadata: GeoJSONItem = { - id: generateShortRandomId(), - name: relativePath, - date: Date.now(), - }; + const metadata: GeoJSONItem = { id: generateShortRandomId(), name: relativePath, date: Date.now() }; await this.geoJSON.save(metadata, geojson); } } + + /** + * Hånderer sorting når bruker endrer valg i select + */ + onSortChange(event: Event) { + const select = event.target as HTMLSelectElement; + const value = select.value as 'name' | 'date'; + this.sortValue.set(value); + } +} + +/** + * Sorterer basert på navn, alfabetisk + */ +export function sortByName(items: GeoJSONItem[]) { + return items.toSorted((a, b) => a.name.localeCompare(b.name)); +} + +/** + * Sorterer basert på dato, nyeste først + */ +export function sortByDate(items: GeoJSONItem[]) { + return items.toSorted((a, b) => { + if (a.date && b.date) { + return b.date - a.date; + } + if (!a.date && !b.date) { + return 0; + } + return a.date ? -1 : 1; + }); } + +const sortFunctions = { + name: sortByName, + date: sortByDate, +} as const; diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 66b27e8c4..3f06b9928 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -357,7 +357,11 @@ "CAPTION": "Import GPX files", "DROP_LABEL_DESKTOP": "Drag and drop or click to add", "DROP_LABEL_MOBILE": "Tap to add files" - } + }, + "SORT_TITLE": " Sort by", + "SORT_BY_NAME": "Name", + "SORT_BY_TIME": "Recently updated", + "VISIBLE_ON_MAP": "Visible on map" }, "POPUP_DISCLAMER": { "ABOUT_OBSERVATIONS": { diff --git a/src/assets/i18n/nb.json b/src/assets/i18n/nb.json index ee2428323..c7be34642 100644 --- a/src/assets/i18n/nb.json +++ b/src/assets/i18n/nb.json @@ -356,7 +356,11 @@ "CAPTION": "Importer GPX-filer", "DROP_LABEL_DESKTOP": "Dra og slipp eller klikk for å legge til", "DROP_LABEL_MOBILE": "Trykk for å legge til filer" - } + }, + "SORT_TITLE": "Sortering", + "SORT_BY_NAME": "Navn", + "SORT_BY_TIME": "Sist endret", + "VISIBLE_ON_MAP": "Vises i kart" }, "POPUP_DISCLAMER": { "ABOUT_OBSERVATIONS": { diff --git a/src/assets/icon/tour.svg b/src/assets/icon/tour.svg new file mode 100644 index 000000000..e5c393534 --- /dev/null +++ b/src/assets/icon/tour.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tsconfig.json b/tsconfig.json index 006da19b7..d2053c32d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,8 +13,8 @@ "experimentalDecorators": true, "moduleResolution": "bundler", "importHelpers": true, - "target": "ES2022", - "module": "ES2022", + "target": "es2023", + "module": "esnext", // "sourceMap": true, // "inlineSources": true, // "sourceRoot": "/", @@ -51,4 +51,4 @@ "strictTemplates": true, "strictStandalone": true } -} \ No newline at end of file +} From 70267c21ed161c8ce9bebd1108aacd90bea292d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98ystein=20Myhre?= <71138449+gruble@users.noreply.github.com> Date: Mon, 17 Nov 2025 13:44:46 +0100 Subject: [PATCH 08/15] RO-3032: Vise importerte turer i kartet (#865) --- .../core/services/geojson/geojson.service.ts | 2 ++ .../map/components/map/map.component.ts | 31 ++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/app/core/services/geojson/geojson.service.ts b/src/app/core/services/geojson/geojson.service.ts index 994ce1a5b..9d53ddcd0 100644 --- a/src/app/core/services/geojson/geojson.service.ts +++ b/src/app/core/services/geojson/geojson.service.ts @@ -3,6 +3,7 @@ import { DatabaseService } from '../database/database.service'; import { FeatureCollection } from 'geojson'; import { GeoJSONItem } from './geojson-item.model'; import { LoggingService } from 'src/app/modules/shared/services/logging/logging.service'; +import { toObservable } from '@angular/core/rxjs-interop'; const DEBUG_TAG = 'GeoJSON'; @@ -14,6 +15,7 @@ export class GeoJSONService { private logger = inject(LoggingService); metadata = signal([]); + readonly metadata$ = toObservable(this.metadata); private initialized = false; constructor() { diff --git a/src/app/modules/map/components/map/map.component.ts b/src/app/modules/map/components/map/map.component.ts index 399f5b394..207f9bfa9 100644 --- a/src/app/modules/map/components/map/map.component.ts +++ b/src/app/modules/map/components/map/map.component.ts @@ -44,6 +44,7 @@ import { MapService } from '../../services/map/map.service'; import { LeafletModule } from '@bluehalo/ngx-leaflet'; import { MapControlsComponent } from '../map-controls/map-controls.component'; import type { FeatureCollection } from 'geojson'; +import { GeoJSONService } from 'src/app/core/services/geojson/geojson.service'; const DEBUG_TAG = 'MapComponent'; @@ -179,6 +180,7 @@ export class MapComponent implements OnInit, OnDestroy, AfterViewInit { private platform = inject(Platform); private mapZoomService = inject(MapZoomService); private observerTripsService = inject(ObserverTripsService); + private geoJSONService = inject(GeoJSONService); readonly showControls = input(true); readonly showZoomButtons = input(true); @@ -344,7 +346,19 @@ export class MapComponent implements OnInit, OnDestroy, AfterViewInit { * @param geojson FeatureCollection to add */ private async addGeojsonLayer(map: L.Map, id: string, geojson: FeatureCollection) { - const geojsonLayer = L.geoJSON(geojson, { style: { dashArray: '4', color: 'red', stroke: true } }); + const pointIcon = L.icon({ + iconUrl: '/assets/icon/map/prev-used-place.svg', + iconSize: [25, 41], + iconAnchor: [12, 41], + shadowUrl: 'leaflet/marker-shadow.png', + shadowSize: [41, 41], + }); + const geojsonLayer = L.geoJSON(geojson, { + style: { dashArray: '4', color: 'red', stroke: true }, + pointToLayer: (_, latlng) => { + return L.marker(latlng, { icon: pointIcon }); + }, + }); let extraTapRadiusLayer: L.Layer | undefined; if (isAndroidOrIos(this.platform)) { @@ -540,6 +554,21 @@ export class MapComponent implements OnInit, OnDestroy, AfterViewInit { } }); + // Lytt på endringer i evt. geojson-metadata og tegn geojson-lag (på nytt) + this.geoJSONService.metadata$.pipe(takeUntil(this.ngDestroy$)).subscribe(async (metadataForAllTracks) => { + if (metadataForAllTracks && map) { + metadataForAllTracks.forEach(async (trackMetadata) => { + // Fjern eksisterende geojson-lag for id + this.removeGeojsonLayer(map, trackMetadata.id); + // Hent oppdatert geojson fra tjenesten + const geojson = await this.geoJSONService.get(trackMetadata.id); + if (geojson) { + this.addGeojsonLayer(map, trackMetadata.id, geojson); + } + }); + } + }); + if (isAndroidOrIos(this.platform)) { this.initOfflineMaps(); } From 5263bef296490e10b30b99e80fbd3cc0e9135126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98ystein=20Myhre?= <71138449+gruble@users.noreply.github.com> Date: Mon, 17 Nov 2025 13:47:54 +0100 Subject: [PATCH 09/15] RO-3031: Import av GeoJSON-filer (#867) --- package-lock.json | 10 ++++++++++ package.json | 1 + src/app/pages/plans/gpx.ts | 3 ++- src/app/pages/plans/plans.page.html | 2 +- src/app/pages/plans/plans.page.ts | 2 +- src/app/pages/plans/utils.ts | 24 ++++++++++++++++++++++++ 6 files changed, 39 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1b2a0db7e..1da62fb96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,7 @@ "@ngx-translate/core": "^16.0.4", "@ngx-translate/http-loader": "^16.0.1", "@openid/appauth": "^1.3.1", + "@placemarkio/check-geojson": "^0.1.14", "@sentry/browser": "^7.37.2", "@tmcw/togeojson": "^7.1.2", "@turf/turf": "^7.1.0", @@ -7659,6 +7660,15 @@ "node": ">=14" } }, + "node_modules/@placemarkio/check-geojson": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/@placemarkio/check-geojson/-/check-geojson-0.1.14.tgz", + "integrity": "sha512-PZvNKzt6STytUw21TUkqU+TG6dbwTWb1ACosvInBYTBm37zsr8C74J6crBTQ3BWkyd6YeitYd4HibJzBEyk6Aw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.52.3", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz", diff --git a/package.json b/package.json index a638d975e..e1583558b 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "@ngx-translate/core": "^16.0.4", "@ngx-translate/http-loader": "^16.0.1", "@openid/appauth": "^1.3.1", + "@placemarkio/check-geojson": "^0.1.14", "@sentry/browser": "^7.37.2", "@tmcw/togeojson": "^7.1.2", "@turf/turf": "^7.1.0", diff --git a/src/app/pages/plans/gpx.ts b/src/app/pages/plans/gpx.ts index 3d102ba79..31d70cfbe 100644 --- a/src/app/pages/plans/gpx.ts +++ b/src/app/pages/plans/gpx.ts @@ -1,10 +1,11 @@ import { FileSystemEntry } from 'ngx-file-drop'; import { assertIsFileEntry, toText } from './utils'; import { gpx } from '@tmcw/togeojson'; +import { isGpxFile } from './utils'; function assertIsGpxFile(fileEntry: FileSystemEntry): asserts fileEntry is FileSystemFileEntry { assertIsFileEntry(fileEntry); - if (!fileEntry.name.toLowerCase().endsWith('.gpx')) { + if (!isGpxFile(fileEntry)) { throw new Error('fileEntry is not GPX file'); } } diff --git a/src/app/pages/plans/plans.page.html b/src/app/pages/plans/plans.page.html index f7dcbf232..04f58f44d 100644 --- a/src/app/pages/plans/plans.page.html +++ b/src/app/pages/plans/plans.page.html @@ -38,7 +38,7 @@

TODO: Planer

} - +
- - -