diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index efd221f..a4c9559 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -10,6 +10,7 @@ const routes: Routes = [
{ path: 'info', component: InfoComponent },
{ path: 'map', component: MapComponent },
{ path: 'walk', component: WalkComponent },
+ { path: 'community', component: WalkComponent },
{ path: 'stack-fade', component: StackFadeComponent },
{ path: '', component: OverviewComponent, children: [] }
];
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 3b52c18..88de002 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -31,6 +31,7 @@ import { SingleImageComponent } from './components/single-image/single-image.com
import { InfoTextComponent } from './components/info-text/info-text.component';
import { AudioTextComponent } from './components/audio-text/audio-text.component';
import { DataHotspotComponent } from './components/data-hotspot/data-hotspot.component';
+import { CommunityHotspotComponent } from './components/community-hotspot/community-hotspot.component';
import { InstructionsComponent } from './components/instructions.component';
import { ToolsComponent } from './components/tools.component';
import { CopyrightComponent } from './components/copyright/copyright.component';
@@ -96,6 +97,7 @@ function initializeKeycloak(keycloak: KeycloakService) {
InfoTextComponent,
AudioTextComponent,
DataHotspotComponent,
+ CommunityHotspotComponent,
InstructionsComponent,
CopyrightComponent,
ToolsComponent,
diff --git a/src/app/components/choose-channel/choose-channel.component.ts b/src/app/components/choose-channel/choose-channel.component.ts
index 1d5e47f..dd46af9 100644
--- a/src/app/components/choose-channel/choose-channel.component.ts
+++ b/src/app/components/choose-channel/choose-channel.component.ts
@@ -18,7 +18,7 @@ export class ChooseChannelComponent implements OnInit {
'',
'walk',
'walk', // TODO: change to 'stopmotion' when ready (add to routes)
- 'walk', // TODO: change to 'community' when ready (add to routes)
+ 'community', // TODO: change to 'community' when ready (add to routes)
'map',
'walk', // TODO: change to 'audiowalk' when ready (add to routes)
]
diff --git a/src/app/components/community-hotspot/community-hotspot.component.css b/src/app/components/community-hotspot/community-hotspot.component.css
new file mode 100644
index 0000000..49037e8
--- /dev/null
+++ b/src/app/components/community-hotspot/community-hotspot.component.css
@@ -0,0 +1,93 @@
+
+.carousel-container {
+ position: fixed;
+ top: calc(48px);
+ left: 0;
+ right: 0;
+ bottom: 0;
+ margin: 0;
+ padding: 0;
+}
+
+.carousel-wrapper {
+ height: 100%;
+ width: 100%;
+
+ white-space: nowrap;
+
+ overflow-x: scroll;
+ overflow-y: hidden;
+ scroll-snap-type: x mandatory;
+}
+
+.carousel-image {
+ width: 100%;
+ height: 100%;
+ display: inline-block;
+ position: relative;
+ overflow: hidden;
+ scroll-snap-align: center;
+}
+
+.carousel-image img {
+ object-fit: cover;
+ object-position: center;
+ height: 100%;
+ width: 100%;
+}
+
+.carousel-arrow {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ color: white;
+ cursor: pointer;
+ background: rgba(0, 0, 0, 0.5);
+ padding: 8px;
+ border-radius: 50%;
+}
+
+.carousel-arrow.left {
+ left: 10px;
+}
+
+.carousel-arrow.right {
+ right: 10px;
+}
+
+#text {
+ display: grid;
+ grid-template-rows: 36px calc(100% - 36px);
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ max-height: 33%;
+ background-color: rgba(255, 255, 255, 0.7);
+ border-radius: 7px 7px 0 0;
+ padding: 8px;
+ box-shadow: 0px -3px 14px 2px rgba(0, 0, 0, 0.12);
+}
+
+#text p {
+ overflow-y: scroll;
+}
+
+#text .infotable {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ column-gap: 10px;
+ margin-bottom: 16px;
+}
+
+#text .mdc-button {
+ background-color: #ffffff88;
+ margin-bottom: 16px;
+}
+
+app-copyright {
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ z-index: 999;
+}
diff --git a/src/app/components/community-hotspot/community-hotspot.component.html b/src/app/components/community-hotspot/community-hotspot.component.html
new file mode 100644
index 0000000..fb5658a
--- /dev/null
+++ b/src/app/components/community-hotspot/community-hotspot.component.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+ keyboard_arrow_left
+
+
+ keyboard_arrow_right
+
+
+
+
+
{{ hotspot.species }}
+
+
diff --git a/src/app/components/community-hotspot/community-hotspot.component.ts b/src/app/components/community-hotspot/community-hotspot.component.ts
new file mode 100644
index 0000000..ec023e6
--- /dev/null
+++ b/src/app/components/community-hotspot/community-hotspot.component.ts
@@ -0,0 +1,56 @@
+import { Component, ElementRef, ViewChild } from '@angular/core';
+import { filter } from 'rxjs';
+import { HotspotCommunity, HotspotService } from 'src/app/services/hotspot.service';
+import { slideUpDownAnimation } from 'src/app/shared';
+
+@Component({
+ selector: 'app-community-hotspot',
+ templateUrl: './community-hotspot.component.html',
+ styleUrls: ['./community-hotspot.component.css'],
+ animations: [slideUpDownAnimation]
+})
+export class CommunityHotspotComponent {
+
+ hotspot?: HotspotCommunity;
+
+ @ViewChild('wrapper')
+ wrapper?: ElementRef;
+
+ constructor(private hotspotService: HotspotService) {
+ this.hotspotService.trigger
+ .pipe(filter(h => h !== false && h.type === 5))
+ .subscribe(hotspot => {
+ console.log(hotspot);
+ if (hotspot && hotspot.type === 5) this.hotspot = hotspot;
+ })
+ }
+
+ swipeTransition(direction: 'left'|'right') {
+ if (this.wrapper) {
+ const imgs = this.wrapper.nativeElement.querySelectorAll('.carousel-image');
+ let current: number|null = null;
+ imgs.forEach(img => {
+ if (this.isElementInViewport(img)) current = Number(img.id.substring(4));
+ });
+ if (current !== null) {
+ let next: number;
+ if (direction === 'right') next = (current+1)%imgs.length;
+ else next = (((current-1)%imgs.length)+imgs.length)%imgs.length;
+ imgs.forEach(img => {
+ if (img.id === 'img-'+next) img.scrollIntoView({ behavior: 'smooth'});
+ });
+ }
+ }
+ }
+
+ private isElementInViewport (el: any) {
+ const rect = el.getBoundingClientRect();
+ const condition =
+ rect.top >= 0 &&
+ rect.left >= 0 &&
+ rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /* or $(window).height() */
+ rect.right <= (window.innerWidth || document.documentElement.clientWidth); /* or $(window).width() */
+ return condition;
+ }
+
+}
diff --git a/src/app/components/map/map.component.ts b/src/app/components/map/map.component.ts
index 035d27e..4a364aa 100644
--- a/src/app/components/map/map.component.ts
+++ b/src/app/components/map/map.component.ts
@@ -6,7 +6,7 @@ import { Map, Marker, GeoJSONSource, Popup } from 'maplibre-gl';
import { Feature, Position } from 'geojson';
import { DataService, NoteService, OidcService, ParcoursService, TrackRecorderService } from 'src/app/services';
import { CoordinatePoint, Deployment, MAP_STYLE_CONFIG } from 'src/app/shared';
-import { map, of, Subject, switchMap, takeUntil } from 'rxjs';
+import { map as rxjsMap, of, Subject, switchMap, takeUntil } from 'rxjs';
@Component({
selector: 'app-map',
@@ -39,7 +39,7 @@ export class MapComponent implements AfterViewInit, OnDestroy {
// draw track
this.trackRecorder.track.pipe(
takeUntil(this.destroy),
- map(track => track.map(p => [p.coords.longitude, p.coords.latitude])))
+ rxjsMap(track => track.map(p => [p.coords.longitude, p.coords.latitude])))
.subscribe(track => {
const s = this.map?.getSource('route');
if (s) s.setData({
@@ -148,7 +148,13 @@ export class MapComponent implements AfterViewInit, OnDestroy {
}
private drawHotspots(map: Map) {
- this.dataService.getWalkHotspots(1).subscribe(hotspots => {
+ this.dataService.getWalkHotspots(1).pipe(
+ switchMap(hotspots => {
+ return this.dataService.getCommunityHotspots().pipe(
+ rxjsMap(communityHotspots => {
+ return hotspots.concat(communityHotspots);
+ }));
+ })).subscribe(hotspots => {
const radius = 20; // hotspot radius in meters
const metersToPixelsAtMaxZoom = (meters:number, latitude:number) => meters / 0.075 / Math.cos(latitude * Math.PI / 180);
const features = hotspots.map(h => {
diff --git a/src/app/components/walk/walk.component.html b/src/app/components/walk/walk.component.html
index 41c54df..ebfd3f8 100644
--- a/src/app/components/walk/walk.component.html
+++ b/src/app/components/walk/walk.component.html
@@ -8,6 +8,8 @@
+
+
diff --git a/src/app/components/walk/walk.component.ts b/src/app/components/walk/walk.component.ts
index 73c1941..6f8a6c6 100644
--- a/src/app/components/walk/walk.component.ts
+++ b/src/app/components/walk/walk.component.ts
@@ -1,4 +1,6 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { Subject, takeUntil } from 'rxjs';
import { HotspotService, HotspotType } from 'src/app/services/hotspot.service';
import { hotspotCrossfadeAnimation } from 'src/app/shared';
@@ -8,18 +10,35 @@ import { hotspotCrossfadeAnimation } from 'src/app/shared';
styleUrls: ['./walk.component.css'],
animations: [hotspotCrossfadeAnimation],
})
-export class WalkComponent implements OnInit {
+export class WalkComponent implements OnInit, OnDestroy {
+ private destroy = new Subject();
+ private mode: 'walk'|'community'|'audiowalk' = 'walk';
hotspot?: HotspotType|false;
constructor(
- public hotspotService: HotspotService
- ) { }
+ public hotspotService: HotspotService,
+ public route: ActivatedRoute,
+ ) {
+ const modes = ['walk', 'community', 'audiowalk'];
+ if (modes.includes(this.route.snapshot.url[0].path)) {
+ this.mode = this.route.snapshot.url[0].path as 'walk'|'community'|'audiowalk';
+ } else {
+ console.warn('Unknown mode, defaulting to "walk"');
+ }
+ }
ngOnInit(): void {
// "false" could be triggering a side effect to fade out audio, wait, then continue delivering "false"
- this.hotspotService.trigger.subscribe(hotspot => this.hotspot = hotspot);
- this.hotspotService.loadHotspots();
+ this.hotspotService.trigger
+ .pipe(takeUntil(this.destroy))
+ .subscribe(hotspot => this.hotspot = hotspot);
+ this.hotspotService.loadHotspots(this.mode);
+ }
+
+ ngOnDestroy(): void {
+ this.destroy.next(null);
+ this.destroy.complete();
}
}
diff --git a/src/app/services/data.service.ts b/src/app/services/data.service.ts
index 143ca4c..443c55f 100644
--- a/src/app/services/data.service.ts
+++ b/src/app/services/data.service.ts
@@ -2,7 +2,8 @@ import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, map, tap } from 'rxjs';
import { Deployment, Note, ImageStack, SectionText, StackImage, WalkPath } from '../shared';
-import { HotspotDataPayload, HotspotType } from './hotspot.service';
+import { HotspotCommunity, HotspotDataPayload, HotspotType, WildeNachbarnProperties } from './hotspot.service';
+import { Feature, FeatureCollection, Point } from 'geojson';
@Injectable({
providedIn: 'root'
@@ -100,6 +101,32 @@ export class DataService {
}))
}
+ public getCommunityHotspots(): Observable {
+ const url = `${this.apiUrl}/walk/community-hotspots`;
+ return this.http.get(url).pipe(map(fc => {
+ return fc.features.map((f: Feature) => {
+ const p = (f.properties as WildeNachbarnProperties);
+ const hotspot: HotspotCommunity = {
+ type: 5,
+ location: {
+ lat: (f.geometry as Point).coordinates[1],
+ lon: (f.geometry as Point).coordinates[0]
+ },
+ subject: `WN: ${p.Art}`,
+ id: p.ID,
+ species: p.Art,
+ date: p.Datum,
+ time_range: p.Zeitraum,
+ post_url: p.Meldung,
+ portrait_url: p.Artenportraet,
+ weight: p.weight,
+ media: p.media
+ }
+ return hotspot;
+ })
+ }));
+ }
+
public getImageResource(url: string) {
return this.http.get(`${this.apiUrl}/files/walk/${url}`, {responseType: 'blob'});
}
diff --git a/src/app/services/hotspot.service.ts b/src/app/services/hotspot.service.ts
index d48aef1..6c8cf5e 100644
--- a/src/app/services/hotspot.service.ts
+++ b/src/app/services/hotspot.service.ts
@@ -2,7 +2,7 @@ import { Component, Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { TriggerHotspotDialogComponent } from '../components/trigger-hotspot-dialog.component';
import { CoordinatePoint } from '../shared';
-import { BehaviorSubject } from 'rxjs';
+import { BehaviorSubject, combineLatest, map, switchMap } from 'rxjs';
import { ParcoursService } from './parcours.service';
import { DataService } from './data.service';
import { AudioService } from './audio.service';
@@ -43,17 +43,36 @@ export interface HotspotAudiotext extends Hotspot {
speakerFunction?: string;
contentSubject?: string;
}
+
+export interface WildeNachbarnProperties {
+ ID: number;
+ Art: string;
+ Datum: string;
+ Zeitraum: string;
+ Meldung: string;
+ Artenportraet?: string;
+ weight: number;
+ media: string[];
+}
export interface HotspotCommunity extends Hotspot {
type: 5;
- dataUrl: string;
+ id: number;
+ species: string;
+ date: string;
+ time_range: string;
+ post_url: string;
+ portrait_url?: string;
+ weight: number;
+ media: string[];
}
+
export interface HotspotData extends Hotspot {
type: 6;
endpoint: string;
title: string;
text: string;
}
-export type HotspotType = HotspotImageSingle|HotspotImageSequence|HotspotInfotext|HotspotAudiotext|HotspotData;
+export type HotspotType = HotspotImageSingle|HotspotImageSequence|HotspotInfotext|HotspotAudiotext|HotspotData|HotspotCommunity;
export interface HotspotBarchartData {
tag: string
@@ -107,6 +126,7 @@ export class HotspotService {
private hotspots: Array = [];
private currentHotspot: HotspotType | null = null;
+ private typeFilter = new BehaviorSubject([1,2,3,4,6]);
public trigger: BehaviorSubject;
public closeHotspots: BehaviorSubject>;
@@ -119,9 +139,13 @@ export class HotspotService {
) {
this.trigger = new BehaviorSubject(false);
this.closeHotspots = new BehaviorSubject>([]);
- this.parcoursService.location.subscribe(location => {
+
+ combineLatest([this.parcoursService.location, this.typeFilter])
+ .subscribe(([location, typeFilter]) => {
if (this.hotspots.length > 0) {
- const c = this.hotspots.map(hotspot => Object.assign(hotspot, { distance: distance(
+ const c = this.hotspots.filter(hotspot => {
+ return typeFilter.includes(hotspot.type);
+ }).map(hotspot => Object.assign(hotspot, { distance: distance(
[hotspot.location.lon, hotspot.location.lat],
[location.coords.longitude, location.coords.latitude],
{ units: 'meters' })
@@ -141,8 +165,30 @@ export class HotspotService {
})
}
- loadHotspots() {
- this.dataService.getWalkHotspots(1).subscribe(hotspots => this.hotspots = hotspots);
+ set typeFilterValue(value: number[]) {
+ this.typeFilter.next(value);
+ }
+
+ loadHotspots(mode: 'walk'|'community'|'audiowalk' = 'walk') {
+ if (mode === 'walk') {
+ this.dataService.getWalkHotspots(1).subscribe(hotspots => {
+ this.hotspots = hotspots;
+ this.typeFilter.next([1, 2, 3, 4, 6]);
+ });
+ /* // if we want to load community hotspots as well
+ this.dataService.getWalkHotspots(1).pipe(
+ switchMap(hotspots => {
+ return this.dataService.getCommunityHotspots().pipe(
+ map(communityHotspots => {
+ this.hotspots = hotspots.concat(communityHotspots);
+ }))
+ })).subscribe(); */
+ } else if (mode === 'community') {
+ this.dataService.getCommunityHotspots().subscribe(hotspots => {
+ this.hotspots = hotspots;
+ this.typeFilter.next([5]);
+ });
+ }
}
chooseHotspot() {
@@ -193,6 +239,23 @@ export class HotspotService {
contentSubject: 'Untersuchung der Auswirkungen von Klimawandel auf Biodiversität',
})
break;
+ case 5:
+ this.trigger.next({
+ id: 87, type,
+ location: { lat: 1, lon: 4},
+ species: 'Biber',
+ date: '03.12.2016',
+ time_range: '14.00 - 14.59',
+ post_url: 'https://beidebasel.wildenachbarn.ch/beobachtung/50451',
+ portrait_url: 'https://beidebasel.wildenachbarn.ch/artportraet/biber',
+ weight: 1,
+ media: [
+ 'https://beidebasel.wildenachbarn.ch/system/files/styles/beobachtungcolorbox_large/private/medien_zu_meldungen/7051/2018-09/DSCN1128%20-%20Arbeitskopie%202.jpg?itok=O5sTokIp',
+ 'https://beidebasel.wildenachbarn.ch/system/files/styles/beobachtungcolorbox_large/private/medien_zu_meldungen/7051/2018-09/DSCN1133.jpg?itok=kBf3rZ93',
+ 'https://beidebasel.wildenachbarn.ch/system/files/styles/beobachtungcolorbox_large/private/medien_zu_meldungen/7051/2018-09/DSCN1115.jpg?itok=dzRKC9tJ',
+ ]
+ })
+ break;
case 6:
this.trigger.next({
location: { lon: 0, lat: 0 },