Skip to content

Commit

Permalink
feat(journey-maps): deselect pois (#1910)
Browse files Browse the repository at this point in the history
Refs: ROKAS-1438

---------

Co-authored-by: Chris Dickinson <“christopher.dickinson@sbb.ch”>
  • Loading branch information
besartmemeti and Chris Dickinson authored Jun 1, 2023
1 parent 0e5b113 commit b6f10f3
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,18 @@
<button sbb-ghost-button (click)="client.moveSouth()">South</button>
<button sbb-ghost-button (click)="client.moveWest()">West</button>
</li>

<li>
<span class="sbb-label">Teaser / Popup actions</span>
<button sbb-ghost-button (click)="client.unselectAll(['MARKER', 'POI'])">
Close all Teasers / Popups
</button>
<button sbb-ghost-button (click)="client.unselectAll(['POI'])">Close POI Teasers</button>
<button sbb-ghost-button (click)="client.unselectAll(['MARKER'])">
Close marker Teasers / Popups
</button>
</li>

<li>
<div formGroupName="listenerOptions">
<span class="sbb-label">Map interaction configuration</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
SimpleChanges,
} from '@angular/core';
Expand All @@ -14,6 +15,7 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import {
SbbDeselectableFeatureDataType,
SbbFeatureData,
SbbFeatureDataType,
SbbFeaturesClickEventData,
Expand Down Expand Up @@ -42,10 +44,11 @@ import { SBB_ZONE_LAYER } from '../../services/map/map-zone-service';
providers: [SbbMapSelectionEvent],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SbbFeatureEventListener implements OnChanges, OnDestroy {
export class SbbFeatureEventListener implements OnChanges, OnDestroy, OnInit {
@Input() listenerOptions: SbbListenerOptions;
@Input() map: MapLibreMap | null;
@Input() poiOptions?: SbbPointsOfInterestOptions;
@Input() onFeaturesUnselect: Subject<SbbDeselectableFeatureDataType[]>;

@Output() featureSelectionsChange: EventEmitter<SbbFeaturesSelectEventData> =
new EventEmitter<SbbFeaturesSelectEventData>();
Expand Down Expand Up @@ -81,6 +84,12 @@ export class SbbFeatureEventListener implements OnChanges, OnDestroy {
readonly mapSelectionEventService: SbbMapSelectionEvent
) {}

ngOnInit(): void {
this.onFeaturesUnselect.pipe(takeUntil(this._destroyed)).subscribe((types) => {
this.unselectFeaturesOfType(types);
});
}

ngOnDestroy(): void {
this._destroyed.next();
this._destroyed.complete();
Expand Down Expand Up @@ -135,7 +144,7 @@ export class SbbFeatureEventListener implements OnChanges, OnDestroy {
);
this._featuresClickEvent
.pipe(takeUntil(this._destroyed))
.subscribe((data) => this._featureClicked(data));
.subscribe((clickedFeatures) => this._featureClicked(clickedFeatures));
}

if (!this._featuresHoverEvent) {
Expand Down Expand Up @@ -175,14 +184,20 @@ export class SbbFeatureEventListener implements OnChanges, OnDestroy {
return selectionModes;
}

private _featureClicked(data: SbbFeaturesClickEventData) {
this.mapSelectionEventService.toggleSelection(data.features);
private _featureClicked(clickEventData: SbbFeaturesClickEventData) {
this.mapSelectionEventService.toggleSelection(clickEventData.features);
const selectedFeatures = this.mapSelectionEventService.findSelectedFeatures();
this.featureSelectionsChange.next(selectedFeatures);
this.featuresClick.next(data);
this._updateOverlay(data.features, 'click', data.clickLngLat, selectedFeatures);
this.featuresClick.next(clickEventData);
this._updateOverlay(
clickEventData.features,
'click',
clickEventData.clickLngLat,
selectedFeatures
);
}

// return either all features or ordered by route (in case of multiple routes)
private _filterOverlayFeatures(
features: SbbFeatureData[],
type: SbbFeatureDataType
Expand Down Expand Up @@ -216,37 +231,40 @@ export class SbbFeatureEventListener implements OnChanges, OnDestroy {
}

private _updateOverlay(
features: SbbFeatureData[],
clickedFeatures: SbbFeatureData[],
event: 'click' | 'hover',
pos: { lng: number; lat: number },
selectedFeatures?: SbbFeaturesSelectEventData
) {
const topMostFeature = features[0];
const featureDataType = topMostFeature.featureDataType;
const listenerTypeOptions: SbbListenerTypeOptions = this.listenerOptions[featureDataType]!;
const topMostClickedFeature = clickedFeatures[0];
const featureDataTypeOfTopMostClickedFeature = topMostClickedFeature.featureDataType;
const listenerTypeOptions: SbbListenerTypeOptions =
this.listenerOptions[featureDataTypeOfTopMostClickedFeature]!;
const isClick = event === 'click';
const template = isClick
? listenerTypeOptions?.clickTemplate
: listenerTypeOptions?.hoverTemplate;

const selectedIds = (selectedFeatures?.features ?? [])
.filter((f) => f.featureDataType === featureDataType)
.map((f) => f.id);

// If we have a click event then we have to decide if we want to show or hide the overlay.
const showOverlay =
!isClick || features.map((f) => f.id).some((id) => selectedIds.includes(id));
!isClick ||
this._anySelectedClickedFeatureHasSameTypeAsTopClickedFeature(
clickedFeatures,
featureDataTypeOfTopMostClickedFeature,
selectedFeatures
);

if (template && showOverlay) {
clearTimeout(this.overlayTimeoutId);
this.overlayVisible = true;
this.overlayEventType = event;
this.overlayTemplate = template;
this.overlayFeatures = this._filterOverlayFeatures(features, featureDataType);
this.overlayFeatures = this._filterOverlayFeatures(
clickedFeatures,
featureDataTypeOfTopMostClickedFeature
);
this.overlayIsPopup = listenerTypeOptions.popup!;

if (topMostFeature.geometry.type === 'Point') {
this.overlayPosition = topMostFeature.geometry.coordinates as LngLatLike;
if (topMostClickedFeature.geometry.type === 'Point') {
this.overlayPosition = topMostClickedFeature.geometry.coordinates as LngLatLike;
} else {
this.overlayPosition = pos;
}
Expand All @@ -255,11 +273,66 @@ export class SbbFeatureEventListener implements OnChanges, OnDestroy {
}
}

// this method seems to return true if any of the clicked features is selected and has the same
// SbbFeatureDataType as the top-most clicked feature.
private _anySelectedClickedFeatureHasSameTypeAsTopClickedFeature(
clickedFeatures: SbbFeatureData[],
topClickedFeatureType: SbbFeatureDataType,
selectedFeatures?: { features: SbbFeatureData[] | undefined }
): boolean {
const selectedIdsOfSameFeatureTypeAsTopClicked = this._idsOfFeatureWithSameFeatureDataType(
selectedFeatures?.features,
topClickedFeatureType
);
return this._anyFeaturesWithSameId(clickedFeatures, selectedIdsOfSameFeatureTypeAsTopClicked);
}

private _idsOfFeatureWithSameFeatureDataType(
features: SbbFeatureData[] | undefined,
featureDataType: SbbFeatureDataType
): (string | number | undefined)[] {
return this._featuresWithSameFeatureDataType(features, featureDataType).map(
(feature) => feature.id
);
}
private _featuresWithSameFeatureDataType(
features: SbbFeatureData[] | undefined,
featureDataType: SbbFeatureDataType
) {
return (features ?? []).filter((feature) => feature.featureDataType === featureDataType);
}

private _anyFeaturesWithSameId(features: SbbFeatureData[], ids: (string | number | undefined)[]) {
return features.map((feature) => feature.id).some((id) => ids.includes(id));
}

onOverlayClosed() {
this.overlayVisible = false;
if (this.overlayEventType === 'click') {
// if a popup or teaser overlay was closed,
this.mapSelectionEventService.toggleSelection(this.overlayFeatures);
this.overlayFeatures = [];
this.featureSelectionsChange.next(this.mapSelectionEventService.findSelectedFeatures());
}
}

// only used for POI-features at the moment
unselectFeaturesOfType(types: SbbDeselectableFeatureDataType[]) {
const selectedFeaturesOfTypes = this.overlayFeatures.filter((feature) =>
types.some((type) => feature.featureDataType === type)
);

if (selectedFeaturesOfTypes.length > 0) {
// unselect given features
this.overlayVisible = false;
this.mapSelectionEventService.toggleSelection(selectedFeaturesOfTypes);
this.featureSelectionsChange.next({ features: [] });

// remove unselected features from list
this.overlayFeatures = this.overlayFeatures.filter((feature) =>
types.some((type) => feature.featureDataType !== type)
);
this._cd.detectChanges();
}
}
}
1 change: 1 addition & 0 deletions src/journey-maps/angular/journey-maps.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
(featuresHoverChange)="featuresHoverChange.next($event)"
(featuresClick)="featuresClick.next($event); handleMarkerOrClusterClick($event.features)"
(featureSelectionsChange)="selectedFeaturesChange.next($event)"
[onFeaturesUnselect]="onFeaturesUnselectEvent"
></sbb-feature-event-listener>
<sbb-marker-details
[selectedMarker]="selectedMarker"
Expand Down
2 changes: 2 additions & 0 deletions src/journey-maps/angular/journey-maps.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ export interface SbbJourneyMetaInformation {

export type SbbFeatureDataType = 'MARKER' | 'ROUTE' | 'STATION' | 'ZONE' | 'POI';

export type SbbDeselectableFeatureDataType = Extract<SbbFeatureDataType, 'MARKER' | 'POI'>;

/** Angular TemplateRef or an id of a HTML <template>. */
export type SbbTemplateType = TemplateRef<any> | string;

Expand Down
18 changes: 18 additions & 0 deletions src/journey-maps/angular/journey-maps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { SbbFeatureEventListener } from './components/feature-event-listener/fea
import { SbbLevelSwitcher } from './components/level-switch/services/level-switcher';
import { SbbMapLayerFilter } from './components/level-switch/services/map-layer-filter';
import {
SbbDeselectableFeatureDataType,
SbbFeatureData,
SbbFeaturesClickEventData,
SbbFeaturesHoverChangeEventData,
Expand Down Expand Up @@ -394,6 +395,23 @@ export class SbbJourneyMaps implements OnInit, AfterViewInit, OnDestroy, OnChang
this.touchEventCollector.next(event);
}

onFeaturesUnselectEvent: Subject<SbbDeselectableFeatureDataType[]> = new Subject();

/**
* Unselects all elements on the map that are of one of the `SbbFeatureDataType`s passed in as a parameter.
* Currently, we only support 'MARKER' and 'POI'.
*/
unselectAll(types: SbbDeselectableFeatureDataType[]): void {
// unselect markers
if (types.includes('MARKER')) {
this.onMarkerUnselected();
}
// unselect pois
if (types.includes('POI')) {
this.onFeaturesUnselectEvent.next(['POI']);
}
}

/**
* Move the map North as if pressing the up arrow key on the keyboard
*/
Expand Down
10 changes: 7 additions & 3 deletions src/journey-maps/angular/services/map/events/map-event-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,14 @@ export class SbbMapEventUtils {
- Finally set the new state in map source.
*/
if (!mapFeature.source) {
throw new Error('Missing source id in feature: ' + mapFeature);
throw new Error(`Missing source id in feature: ${mapFeature}`);
}
mapFeature.state = mapInstance.getFeatureState(mapFeature);
mapFeature.state = Object.assign(mapFeature.state, state);

mapFeature.state = {
...mapInstance.getFeatureState(mapFeature),
...state,
};

mapInstance.setFeatureState(mapFeature, mapFeature.state);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,19 @@ export class SbbMapSelectionEvent {
};
}

// this method sets a SbbFeatureData as selected/unselected
private _setFeatureSelection(data: SbbFeatureData, selected: boolean) {
if (this._selectionModes.get(data.featureDataType) === 'single') {
// if this SbbFeatureDataType has selectionMode 'single',
// remove the 'featureState' for all MapGeoJSONFeature features containing this 'source' and 'sourceLayer'
const sourceInfo = { source: data.source, sourceLayer: data.sourceLayer };
this._mapInstance.removeFeatureState(sourceInfo);
}

// also, update the state of this MapGeoJSONFeature inside the map instance
this._mapEventUtils.setFeatureState(data, this._mapInstance, { selected });

// also, do the same state update for any 'related' route features (related because part of the same journey ??)
this._routeUtilsService.setRelatedRouteFeaturesSelection(this._mapInstance, data, selected);
}

Expand Down
3 changes: 1 addition & 2 deletions src/journey-maps/angular/services/map/map-journey-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,12 @@ export class SbbMapJourneyService {
mapSelectionEventService,
toFeatureCollection(routeFeatures)
);

this._mapTransferService.updateTransfer(map, toFeatureCollection(transferFeatures));
} else {
// handle transfer and routes together, otherwise they can overwrite each other's transfer or route data
this._mapRouteService.updateRoute(
map,
mapSelectionEventService,
// handle transfer and routes together, otherwise they can overwrite each other's transfer or route data
toFeatureCollection(routeFeatures.concat(transferFeatures)),
stopoverFeatures,
selectedLegId
Expand Down

0 comments on commit b6f10f3

Please sign in to comment.