-
Notifications
You must be signed in to change notification settings - Fork 7
RO-3115:endre list med planer og vise plan i kart knappen #905
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
amish1188
wants to merge
19
commits into
feature/RO-13-turplaner
Choose a base branch
from
feat/ro-3115
base: feature/RO-13-turplaner
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
a37ef3f
RO-3016 og RO-3015 Lagt til NVE Designsystem og ny fane for planer (#…
gruble c8073f9
RO-3017: Vise GeoJSON i kartet
jorgkv 65e7b43
Legg til filimport med filedrop
jorgkv 1e22b8f
Konverter gpx til geojson og lagre i db
jorgkv b121ebd
Rydd opp import-kode
jorgkv 200fd01
Fiks visning på mobil
jorgkv 0f2e2b1
RO-3033: listevisning av turer (#869)
jorgkv 70267c2
RO-3032: Vise importerte turer i kartet (#865)
gruble 5263bef
RO-3031: Import av GeoJSON-filer (#867)
gruble dbbc4be
Legg til vasking av koordinater og egenskaper (#864)
jorgkv 63052cc
RO-3034: Detaljside (#870)
jorgkv ae0b85f
RO-3072: beregne og vise tur lengde (#894)
amish1188 97fe910
RO-3046: filter på tur planer visning -alle eller kun synlige i kart …
amish1188 46b3683
RO-3069: Fikset småting på planer (#898)
gruble e44d017
RO-3047: Mer effektiv tegning av spor (#902)
gruble 962e533
change list view and add buttons to show tour on the map
amish1188 d5da4c8
reset geojsonItemToShowOnMap
amish1188 2e0998d
fjerne komment
amish1188 a1241ec
css changes
amish1188 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| export interface GeoJSONItem { | ||
| id: string; | ||
| name: string; | ||
| date: number; | ||
| visibleOnMap?: boolean; | ||
| comment?: string; | ||
| lengthKm?: number; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| import { TestBed, fakeAsync, tick } from '@angular/core/testing'; | ||
| import { GeoJSONService } from './geojson.service'; | ||
| import { DatabaseService } from '../database/database.service'; | ||
| import { LoggingService } from 'src/app/modules/shared/services/logging/logging.service'; | ||
| import { FeatureCollection } from 'geojson'; | ||
| import { GeoJSONItem } from './geojson-item.model'; | ||
|
|
||
| describe('GeoJSONService', () => { | ||
| let service: GeoJSONService; | ||
| let databaseService: jasmine.SpyObj<DatabaseService>; | ||
| let loggingService: jasmine.SpyObj<LoggingService>; | ||
|
|
||
| const mockGeoJSON: FeatureCollection = { | ||
| type: 'FeatureCollection', | ||
| features: [ | ||
| { | ||
| type: 'Feature', | ||
| geometry: { | ||
| type: 'Point', | ||
| coordinates: [10.0, 60.0], | ||
| }, | ||
| properties: { | ||
| name: 'Test Point', | ||
| }, | ||
| }, | ||
| ], | ||
| }; | ||
|
|
||
| const mockMetadataItem: GeoJSONItem = { | ||
| id: 'test-id-1', | ||
| name: 'Test GeoJSON', | ||
| date: new Date().getTime(), | ||
| }; | ||
|
|
||
| beforeEach(() => { | ||
| const databaseServiceSpy = jasmine.createSpyObj('DatabaseService', ['get', 'set', 'remove']); | ||
| const loggingServiceSpy = jasmine.createSpyObj('LoggingService', ['debug', 'error']); | ||
|
|
||
| TestBed.configureTestingModule({ | ||
| providers: [ | ||
| GeoJSONService, | ||
| { provide: DatabaseService, useValue: databaseServiceSpy }, | ||
| { provide: LoggingService, useValue: loggingServiceSpy }, | ||
| ], | ||
| }); | ||
|
|
||
| databaseService = TestBed.inject(DatabaseService) as jasmine.SpyObj<DatabaseService>; | ||
| loggingService = TestBed.inject(LoggingService) as jasmine.SpyObj<LoggingService>; | ||
|
|
||
| // Default spy returns | ||
| databaseService.get.and.returnValue(Promise.resolve([])); | ||
| databaseService.set.and.returnValue(Promise.resolve()); | ||
| databaseService.remove.and.returnValue(Promise.resolve()); | ||
| }); | ||
|
|
||
| it('should be created', () => { | ||
| service = TestBed.inject(GeoJSONService); | ||
| expect(service).toBeTruthy(); | ||
| }); | ||
|
|
||
| describe('init', () => { | ||
| it('should load metadata from database on initialization', fakeAsync(() => { | ||
| const existingMetadata: GeoJSONItem[] = [mockMetadataItem]; | ||
| databaseService.get.and.returnValue(Promise.resolve(existingMetadata)); | ||
|
|
||
| service = TestBed.inject(GeoJSONService); | ||
| tick(); | ||
|
|
||
| expect(databaseService.get).toHaveBeenCalledWith('geojson-metadata'); | ||
| expect(service.metadata()).toEqual(existingMetadata); | ||
| })); | ||
|
|
||
| it('should initialize with empty metadata if none exists', fakeAsync(() => { | ||
| databaseService.get.and.returnValue(Promise.resolve(null)); | ||
|
|
||
| service = TestBed.inject(GeoJSONService); | ||
| tick(); | ||
|
|
||
| expect(service.metadata()).toEqual([]); | ||
| })); | ||
| }); | ||
|
|
||
| describe('save', () => { | ||
| beforeEach(fakeAsync(() => { | ||
| service = TestBed.inject(GeoJSONService); | ||
| tick(); // Complete initialization | ||
| tick(); // Allow effect to run | ||
| databaseService.set.calls.reset(); | ||
| })); | ||
|
|
||
| it('should save geojson and update metadata', fakeAsync(() => { | ||
| service.save(mockMetadataItem, mockGeoJSON); | ||
| tick(); | ||
|
|
||
| expect(databaseService.set).toHaveBeenCalledWith(`geojson:${mockMetadataItem.id}`, mockGeoJSON); | ||
| expect(service.metadata()).toContain(mockMetadataItem); | ||
| })); | ||
|
|
||
| it('should save metadata after adding item', fakeAsync(() => { | ||
| service.save(mockMetadataItem, mockGeoJSON); | ||
| tick(); | ||
| tick(); // Allow effect to trigger | ||
|
|
||
| expect(databaseService.set).toHaveBeenCalledWith('geojson-metadata', [mockMetadataItem]); | ||
| })); | ||
|
|
||
| it('should emit changed metadata item', fakeAsync(() => { | ||
| let emittedMetadata: GeoJSONItem | undefined; | ||
| service.changedMetadataItem$.subscribe((metadata) => (emittedMetadata = metadata)); | ||
|
|
||
| service.save(mockMetadataItem, mockGeoJSON); | ||
| tick(); | ||
|
|
||
| expect(emittedMetadata).toEqual(mockMetadataItem); | ||
| })); | ||
|
|
||
| it('should throw error if save fails', fakeAsync(() => { | ||
| const error = new Error('Save failed'); | ||
| databaseService.set.and.returnValue(Promise.reject(error)); | ||
|
|
||
| expectAsync(service.save(mockMetadataItem, mockGeoJSON)).toBeRejectedWith(error); | ||
| tick(); | ||
|
|
||
| expect(loggingService.error).toHaveBeenCalled(); | ||
| })); | ||
| }); | ||
|
|
||
| describe('get', () => { | ||
| beforeEach(fakeAsync(() => { | ||
| service = TestBed.inject(GeoJSONService); | ||
| tick(); | ||
| })); | ||
|
|
||
| it('should retrieve geojson by id', fakeAsync(() => { | ||
| databaseService.get.and.returnValue(Promise.resolve(mockGeoJSON)); | ||
|
|
||
| service.get(mockMetadataItem.id).then((result) => { | ||
| expect(result).toEqual(mockGeoJSON); | ||
| }); | ||
| tick(); | ||
|
|
||
| expect(databaseService.get).toHaveBeenCalledWith(`geojson:${mockMetadataItem.id}`); | ||
| })); | ||
| }); | ||
|
|
||
| describe('remove', () => { | ||
| beforeEach(fakeAsync(() => { | ||
| service = TestBed.inject(GeoJSONService); | ||
| tick(); | ||
| // Add an item first | ||
| service.save(mockMetadataItem, mockGeoJSON); | ||
| tick(); | ||
| databaseService.set.calls.reset(); | ||
| })); | ||
|
|
||
| it('should remove geojson and update metadata', fakeAsync(() => { | ||
| service.remove(mockMetadataItem.id); | ||
| tick(); | ||
|
|
||
| expect(databaseService.remove).toHaveBeenCalledWith(`geojson:${mockMetadataItem.id}`); | ||
| expect(service.metadata()).not.toContain(mockMetadataItem); | ||
| })); | ||
|
|
||
| it('should emit removed id', fakeAsync(() => { | ||
| let emittedId: string | undefined; | ||
| service.removedMetadataItemId$.subscribe((id) => (emittedId = id)); | ||
|
|
||
| service.remove(mockMetadataItem.id); | ||
| tick(); | ||
|
|
||
| expect(emittedId).toBe(mockMetadataItem.id); | ||
| })); | ||
|
|
||
| it('should save updated metadata after removal', fakeAsync(() => { | ||
| service.remove(mockMetadataItem.id); | ||
| tick(); | ||
| tick(); // Allow effect to trigger | ||
|
|
||
| expect(databaseService.set).toHaveBeenCalledWith('geojson-metadata', []); | ||
| })); | ||
| }); | ||
|
|
||
| describe('changedMetadataItem$', () => { | ||
| beforeEach(fakeAsync(() => { | ||
| service = TestBed.inject(GeoJSONService); | ||
| tick(); | ||
| })); | ||
|
|
||
| it('should emit metadata changes', fakeAsync(() => { | ||
| const emittedValues: GeoJSONItem[] = []; | ||
| service.changedMetadataItem$.subscribe((metadata) => emittedValues.push(metadata)); | ||
|
|
||
| service.updateMetadata(mockMetadataItem); | ||
| tick(); | ||
|
|
||
| expect(emittedValues[emittedValues.length - 1]).toEqual(mockMetadataItem); | ||
| })); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| import { Injectable, effect, inject, signal } from '@angular/core'; | ||
| import { DatabaseService } from '../database/database.service'; | ||
| 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 { cleanFeatureCollection } from 'src/app/pages/plans/geojson'; | ||
| import { length } from '@turf/turf'; | ||
| import { Subject } from 'rxjs'; | ||
| import { toObservable } from '@angular/core/rxjs-interop'; | ||
|
|
||
| const DEBUG_TAG = 'GeoJSON'; | ||
|
|
||
| @Injectable({ | ||
| providedIn: 'root', | ||
| }) | ||
| export class GeoJSONService { | ||
| private db = inject(DatabaseService); | ||
| private logger = inject(LoggingService); | ||
|
|
||
| // tur som skal vises på kartet når bruker klikker på vis i kart knappen i turlisten. | ||
| geojsonItemToShowOnMap = signal<L.LatLngBounds | null>(null); | ||
| readonly geojsonItemToShowOnMap$ = toObservable(this.geojsonItemToShowOnMap); | ||
| private initialized = false; | ||
| private metadata_ = signal<GeoJSONItem[]>([]); | ||
| private changedMetadataItem = new Subject<GeoJSONItem>(); | ||
| private removedMetadataItemId = new Subject<string>(); | ||
|
|
||
| /** Metadata for alle lagrede spor */ | ||
| metadata = this.metadata_.asReadonly(); | ||
|
|
||
| /** Lytt på denne for å få beskjed om nye eller endrede spor */ | ||
| changedMetadataItem$ = this.changedMetadataItem.asObservable(); | ||
|
|
||
| /** Lytt på denne for å få beskjed om slettede spor */ | ||
| removedMetadataItemId$ = this.removedMetadataItemId.asObservable(); | ||
|
|
||
| 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<GeoJSONItem[]>('geojson-metadata'); | ||
| } | ||
|
|
||
| /** | ||
| * Kalkulerer lengden av LineString features i et GeoJSON objekt | ||
| * @param geojson | ||
| * @returns lengde i kilometer | ||
| */ | ||
| private calculateLengthKm(geojson: Feature<Geometry, GeoJsonProperties>[]): number { | ||
| return geojson.reduce((sum, feature) => sum + length(feature), 0); | ||
| } | ||
|
|
||
| /** | ||
| * Updates metadata for a given item | ||
| * @param item the geojson item to update | ||
| */ | ||
| updateMetadata(item: GeoJSONItem) { | ||
| this.metadata_.update((items) => { | ||
| const other = items.filter((x) => x.id !== item.id); | ||
| return [...other, item]; | ||
| }); | ||
| this.changedMetadataItem.next(item); | ||
| } | ||
|
|
||
| /** | ||
| * Save a geojson object with a given id | ||
| * @param metadata metadata for the geojson | ||
| * @param geojson geojson object | ||
| */ | ||
| async save(metadata: GeoJSONItem, geojson: FeatureCollection): Promise<void> { | ||
| this.logger.debug('Save', DEBUG_TAG, { metadata }); | ||
| try { | ||
| cleanFeatureCollection(geojson); | ||
| } catch (error) { | ||
| this.logger.error(error, DEBUG_TAG, 'Error in cleaning process, but object may be mutated - half cleaned'); | ||
| } | ||
|
|
||
| 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]); | ||
| this.changedMetadataItem.next(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<FeatureCollection> { | ||
| this.logger.debug('Get', DEBUG_TAG, { id }); | ||
| return this.db.get<FeatureCollection>(`geojson:${id}`); | ||
| } | ||
|
|
||
| /** | ||
| * Remove a geojson object by id | ||
| * @param id unique id for the geojson | ||
| */ | ||
| async remove(id: GeoJSONItem['id']): Promise<void> { | ||
| this.logger.debug('Remove', DEBUG_TAG, { id }); | ||
| await this.db.remove(`geojson:${id}`); | ||
| this.metadata_.update((items) => items.filter((item) => item.id !== id)); | ||
| this.removedMetadataItemId.next(id); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.