Skip to content

Commit

Permalink
feat: multiple locations can be displayed in the map (#1662)
Browse files Browse the repository at this point in the history
closes #1601

This functionality has been developed for the project “QualitätsMENTOR”.
QualitätsMENTOR is developed under the projects “Landungsbrücken – Patenschaften in Hamburg stärken” and “openTransfer Patenschaften”. It is funded through the program “Menschen stärken Menschen” by the German Federal Ministry of Family Affairs, Senior Citizens, Women and Youth.
More information at https://github.com/qualitaetsmentor

“Landungsbrücken – Patenschaften in Hamburg stärken” is a project of BürgerStiftung Hamburg in cooperation with the Mentor.Ring Hamburg. With a mix of networking opportunities, capacity building and financial support the project strengthens Hamburg’s scene of mentoring projects since its founding in 2016.

The “Stiftung Bürgermut” foundation since 2007 supports the digital and real exchange of experiences and connections of active citizens. Within the federal program “Menschen stärken Menschen” the foundation as part of its program “openTransfer Patenschaften” offers support services for connecting, spreading and upskilling mentoring organisations across Germany. 

Diese Funktion wurde entwickelt für das Projekt QualitätsMENTOR.
Der QualitätsMENTOR wird entwickelt im Rahmen der Projekte Landungsbrücken – Patenschaften in Hamburg stärken und openTransfer Patenschaften. Er ist gefördert durch das Bundesprogramm Menschen stärken Menschen des Bundesministeriums für Familie, Senioren, Frauen und Jugend.
Mehr Informationen unter https://github.com/qualitaetsmentor

“Landungsbrücken – Patenschaften in Hamburg stärken” ist ein Projekt der BürgerStiftung Hamburg in Kooperation mit dem Mentor.Ring Hamburg. Mit einer Mischung aus Vernetzungsangeboten, Qualifizierungsmaßnahmen und finanzieller Förderung stärkt das Projekt die Hamburger Szene der Patenschaftsprojekte seit der Gründung im Jahr 2016.

Die Stiftung Bürgermut fördert seit 2007 den digitalen und realen Erfahrungsaustausch und die Vernetzung von engagierten Bürger:innen. Innerhalb des Bundesprogramms „Menschen stärken Menschen” bietet die Stiftung im Rahmen ihres Programms openTransfer Patenschaften Unterstützungsleistungen zur Vernetzung, Verbreitung und Qualifizierung von Patenschafts- und Mentoringorganisationen bundesweit.

Co-authored-by: QualitaetsMENTOR <117934638+QualitaetsMENTOR@users.noreply.github.com>
Co-authored-by: Sebastian <sebastian.leidig@gmail.com>
  • Loading branch information
3 people authored Jan 27, 2023
1 parent b6b8ad1 commit 6d1367c
Show file tree
Hide file tree
Showing 21 changed files with 731 additions and 181 deletions.
3 changes: 1 addition & 2 deletions src/app/core/config/config-fix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,7 @@ export const defaultJsonConfig = {
"config": {
"rightSide": {
"entityType": School.ENTITY_TYPE,
"availableFilters": [{ "id": "language" }]
"availableFilters": [{ "id": "language" }],
},
}
}
Expand Down Expand Up @@ -1120,7 +1120,6 @@ export const defaultJsonConfig = {
["address", "address"],
["distance", "privateSchool"],
],
"showMap": ["address", "address"],
"onMatch": {
"newEntityType": ChildSchoolRelation.ENTITY_TYPE,
"newEntityMatchPropertyLeft": "childId",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<app-map
height="70vh"
[marked]="data.marked | async"
[(displayedProperties)]="data.displayedProperties"
[entities]="data.entities | async"
[highlightedEntities]="data.highlightedEntities | async"
(mapClick)="mapClicked($event)"
Expand Down
10 changes: 6 additions & 4 deletions src/app/features/location/map-popup/map-popup.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";

import { MapPopupComponent } from "./map-popup.component";
import { EntitySchemaService } from "../../../core/entity/schema/entity-schema.service";
import { MAT_DIALOG_DATA } from "@angular/material/dialog";
import { ConfigService } from "../../../core/config/config.service";
import { Subject } from "rxjs";
import { Coordinates } from "../coordinates";
import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing";

describe("MapPopupComponent", () => {
let component: MapPopupComponent;
Expand All @@ -14,10 +14,12 @@ describe("MapPopupComponent", () => {

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MapPopupComponent],
imports: [MapPopupComponent, FontAwesomeTestingModule],
providers: [
EntitySchemaService,
{ provide: MAT_DIALOG_DATA, useValue: { mapClick } },
{
provide: MAT_DIALOG_DATA,
useValue: { mapClick, displayedProperties: {} },
},
{ provide: ConfigService, useValue: { getConfig: () => undefined } },
],
}).compileComponents();
Expand Down
18 changes: 7 additions & 11 deletions src/app/features/location/map-popup/map-popup.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,28 @@ import { MAT_DIALOG_DATA, MatDialogModule } from "@angular/material/dialog";
import { Coordinates } from "../coordinates";
import { Entity } from "../../../core/entity/model/entity";
import { Observable, Subject } from "rxjs";
import { LocationEntity, MapComponent } from "../map/map.component";
import { MapComponent } from "../map/map.component";
import { AsyncPipe, NgIf } from "@angular/common";
import { MatButtonModule } from "@angular/material/button";
import { LocationProperties } from "../map/map-properties-popup/map-properties-popup.component";

export interface MapPopupConfig {
marked?: Observable<Coordinates[]>;
entities?: Observable<LocationEntity[]>;
highlightedEntities?: Observable<LocationEntity[]>;
entities?: Observable<Entity[]>;
highlightedEntities?: Observable<Entity[]>;
mapClick?: Subject<Coordinates>;
entityClick?: Subject<Entity>;
disabled?: boolean;
helpText?: string;
displayedProperties?: LocationProperties;
}

@Component({
selector: "app-map-popup",
templateUrl: "./map-popup.component.html",
styleUrls: ["./map-popup.component.scss"],
imports: [
MatDialogModule,
MapComponent,
NgIf,
MatButtonModule,
AsyncPipe
],
standalone: true
imports: [MatDialogModule, MapComponent, NgIf, MatButtonModule, AsyncPipe],
standalone: true,
})
export class MapPopupComponent {
constructor(
Expand Down
15 changes: 14 additions & 1 deletion src/app/features/location/map-utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Entity } from "../../core/entity/model/entity";
import { Entity, EntityConstructor } from "../../core/entity/model/entity";
import * as L from "leaflet";
import { Coordinates } from "./coordinates";
import { getHue } from "../../utils/style-utils";
import { locationEntitySchemaDataType } from "./location-data-type";

const iconRetinaUrl = "assets/marker-icon-2x.png";
const iconUrl = "assets/marker-icon.png";
Expand Down Expand Up @@ -62,3 +63,15 @@ export function getKmDistance(x: Coordinates, y: Coordinates) {
const d = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) * R;
return d / 1000;
}

/**
* Get all properties of an entity that represent a geographic location
* @param entity
*/
export function getLocationProperties(entity: EntityConstructor) {
return [...entity.schema.entries()]
.filter(
([_, schema]) => schema.dataType === locationEntitySchemaDataType.name
)
.map(([name]) => name);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<h1 matDialogTitle i18n="Title of popup to select locations that are displayed in the map">
Select displayed locations
</h1>
<app-dialog-close matDialogClose></app-dialog-close>
<mat-dialog-content>
<mat-form-field *ngFor="let row of entityProperties" style="width: 100%">
<mat-label>{{row.entity.label}}</mat-label>
<mat-select
[(value)]="row.selected"
[disabled]="row.properties.length < 2"
multiple
>
<mat-option *ngFor="let prop of row.properties" [value]="prop.name">{{ prop.label }}</mat-option>
</mat-select>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-stroked-button (click)="closeDialog()" i18n="Button for closing popup and applying changes">Apply</button>
</mat-dialog-actions>
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";

import {
LocationProperties,
MapPropertiesPopupComponent,
} from "./map-properties-popup.component";
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing";
import {
entityRegistry,
EntityRegistry,
} from "../../../../core/entity/database-entity.decorator";
import { Child } from "../../../../child-dev-project/children/model/child";
import { School } from "../../../../child-dev-project/schools/model/school";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";

describe("MapPropertiesPopupComponent", () => {
let component: MapPropertiesPopupComponent;
let fixture: ComponentFixture<MapPropertiesPopupComponent>;
let properties: LocationProperties = {};
let mockDialogRef: jasmine.SpyObj<MatDialogRef<MapPropertiesPopupComponent>>;

beforeEach(async () => {
Child.schema.set("address", { label: "Address", dataType: "location" });
Child.schema.set("otherAddress", {
label: "Other address",
dataType: "location",
});
properties[Child.ENTITY_TYPE] = ["address"];
School.schema.set("address", {
label: "School address",
dataType: "location",
});
properties[School.ENTITY_TYPE] = ["address"];
mockDialogRef = jasmine.createSpyObj(["close"]);
await TestBed.configureTestingModule({
imports: [
MapPropertiesPopupComponent,
FontAwesomeTestingModule,
NoopAnimationsModule,
],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: properties },
{ provide: EntityRegistry, useValue: entityRegistry },
{ provide: MatDialogRef, useValue: mockDialogRef },
],
}).compileComponents();

fixture = TestBed.createComponent(MapPropertiesPopupComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

afterEach(() => {
Child.schema.delete("address");
Child.schema.delete("otherAddress");
School.schema.delete("address");
});

it("should create", () => {
expect(component).toBeTruthy();
});

it("should display all available properties with their labels", () => {
expect(component.entityProperties).toEqual([
{
entity: Child,
properties: [
{ name: "address", label: "Address" },
{ name: "otherAddress", label: "Other address" },
],
selected: ["address"],
},
{
entity: School,
properties: [{ name: "address", label: "School address" }],
selected: ["address"],
},
]);
});

it("should emit the selected properties", () => {
component.entityProperties.find(({ entity }) => entity === Child).selected =
["otherAddress"];

component.closeDialog();

expect(mockDialogRef.close).toHaveBeenCalledWith({
[Child.ENTITY_TYPE]: ["otherAddress"],
[School.ENTITY_TYPE]: ["address"],
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Component, Inject } from "@angular/core";
import {
MAT_DIALOG_DATA,
MatDialogModule,
MatDialogRef,
} from "@angular/material/dialog";
import { DialogCloseComponent } from "../../../../core/common-components/dialog-close/dialog-close.component";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatSelectModule } from "@angular/material/select";
import { NgForOf, NgIf } from "@angular/common";
import { EntityConstructor } from "../../../../core/entity/model/entity";
import { EntityRegistry } from "../../../../core/entity/database-entity.decorator";
import { getLocationProperties } from "../../map-utils";
import { MatButtonModule } from "@angular/material/button";

/**
* A map of entity types and the (selected) location properties of this type
*/
export type LocationProperties = { [key: string]: string[] };

@Component({
selector: "app-map-properties-popup",
templateUrl: "./map-properties-popup.component.html",
styles: [],
imports: [
MatDialogModule,
DialogCloseComponent,
MatFormFieldModule,
MatSelectModule,
NgForOf,
NgIf,
MatButtonModule,
],
standalone: true,
})
export class MapPropertiesPopupComponent {
entityProperties: {
entity: EntityConstructor;
properties: { name: string; label: string }[];
selected: string[];
}[];

constructor(
@Inject(MAT_DIALOG_DATA) mapProperties: LocationProperties,
entities: EntityRegistry,
private dialogRef: MatDialogRef<MapPropertiesPopupComponent>
) {
this.entityProperties = Object.entries(mapProperties).map(
([entityType, selected]) => {
const entity = entities.get(entityType);
const mapProperties = getLocationProperties(entity);
const properties = mapProperties.map((name) => ({
name,
label: entity.schema.get(name).label,
}));
return { entity, properties, selected };
}
);
}

closeDialog() {
const result: LocationProperties = {};
this.entityProperties.forEach(
({ entity, selected }) => (result[entity.ENTITY_TYPE] = selected)
);
this.dialogRef.close(result);
}
}
11 changes: 8 additions & 3 deletions src/app/features/location/map/map.component.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
<div class="map-frame" [style.height]="height">
<div id="map" #map>
<button *ngIf="expandable" mat-mini-fab (click)="showPopup()">
<fa-icon icon="expand" style="color: black;"></fa-icon>
</button>
<div class="action-elements">
<button *ngIf="expandable" mat-mini-fab (click)="openMapInPopup()">
<fa-icon icon="expand" style="color: black;"></fa-icon>
</button>
<button *ngIf="showPropertySelection" mat-mini-fab (click)="openMapPropertiesPopup()">
<fa-icon icon="location-dot" style="color: black;"></fa-icon>
</button>
</div>
</div>
</div>
8 changes: 6 additions & 2 deletions src/app/features/location/map/map.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@
height: 100%;
}

button {
.action-elements {
position: absolute;
bottom: 3px;
left: 3px;
left: 0;
z-index: 1000;
}

button {
background-color: white !important;
margin-left: 3px;
}
Loading

0 comments on commit 6d1367c

Please sign in to comment.