src/app/features/location/map-popup/map-popup.component.ts

Description

A dialog to display an OpenStreetMap map with markers and optionally allow the user to select a location.

Example

Metadata

Relationships

Used by

No results matching.

Depends on

Index

Properties
Methods

Constructor

constructor()

Methods

Async mapClicked
mapClicked(newCoordinates: Coordinates)
Parameters :
Name Type Optional
newCoordinates Coordinates No
Returns : any
onSave
onSave()
Returns : void
updateLocation
updateLocation(event: GeoLocation | undefined)
Parameters :
Name Type Optional
event GeoLocation | undefined No
Returns : void

Properties

data
Type : unknown
Default value : inject<MapPopupConfig>(MAT_DIALOG_DATA)
helpText
Type : string
Default value : $localize`Search an address or click on the map directly to select a different location`
markedLocations
Type : BehaviorSubject<OpenStreetMapsSearchResult[]>
selectedLocation
Type : GeoLocation
import { Component, inject, ChangeDetectionStrategy } from "@angular/core";
import {
  MAT_DIALOG_DATA,
  MatDialogModule,
  MatDialogRef,
} from "@angular/material/dialog";
import { Coordinates } from "../coordinates";
import { Entity } from "../../../core/entity/model/entity";
import { BehaviorSubject, firstValueFrom, Observable, Subject } from "rxjs";
import { MapComponent } from "../map/map.component";
import { AsyncPipe } from "@angular/common";
import { MatButtonModule } from "@angular/material/button";
import { OpenStreetMapsSearchResult, GeoService } from "../geo.service";
import { AddressEditComponent } from "../address-edit/address-edit.component";
import { ConfirmationDialogService } from "../../../core/common-components/confirmation-dialog/confirmation-dialog.service";
import { LocationProperties } from "../map/map-properties-popup/map-properties-popup.component";
import { GeoLocation } from "../geo-location";

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

  /**
   * Display a custom help text in the dialog to explain possible actions.
   * (Otherwise the default help is shown)
   */
  helpText?: string;

  /**
   * A single location that is selected and editable.
   */
  selectedLocation?: GeoLocation;

  /**
   * If true, show a minimal map view with only pins and a close button for selection-only use cases.
   * Hides the address input, search, and save/cancel buttons.
   */
  showMapOnly?: boolean;
}

/**
 * A dialog to display an OpenStreetMap map with markers and optionally allow the user to select a location.
 */
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "app-map-popup",
  templateUrl: "./map-popup.component.html",
  styleUrls: ["./map-popup.component.scss"],
  imports: [
    MatDialogModule,
    MapComponent,
    MatButtonModule,
    AsyncPipe,
    AddressEditComponent,
  ],
})
export class MapPopupComponent {
  data = inject<MapPopupConfig>(MAT_DIALOG_DATA);
  private dialogRef = inject<MatDialogRef<MapPopupComponent>>(MatDialogRef);
  private geoService = inject(GeoService);
  private confirmationDialog = inject(ConfirmationDialogService);

  markedLocations: BehaviorSubject<OpenStreetMapsSearchResult[]>;
  helpText: string = $localize`Search an address or click on the map directly to select a different location`;

  selectedLocation: GeoLocation;

  constructor() {
    const data = this.data;

    this.markedLocations = new BehaviorSubject<OpenStreetMapsSearchResult[]>(
      (data.marked as OpenStreetMapsSearchResult[]) ?? [],
    );
    this.selectedLocation = data.selectedLocation;
    this.ensureGeoLookupInMarkedLocations();
    this.setDialogCloseBehavior(data);
    this.setHelpText(data);
  }

  private ensureGeoLookupInMarkedLocations() {
    if (
      this.selectedLocation &&
      this.selectedLocation.geoLookup &&
      !this.markedLocations.value
        .filter((x) => !!x)
        .includes(this.selectedLocation.geoLookup)
    ) {
      this.markedLocations.next([
        ...this.markedLocations.value,
        this.selectedLocation.geoLookup,
      ]);
    }
  }

  private setDialogCloseBehavior(data: MapPopupConfig) {
    if (!data.disabled) {
      this.dialogRef.disableClose = true;
    }
  }

  private setHelpText(data: MapPopupConfig) {
    if (data.hasOwnProperty("helpText")) {
      this.helpText = data.helpText;
    }
  }

  async mapClicked(newCoordinates: Coordinates) {
    if (this.data.disabled || !newCoordinates) {
      return;
    }
    const geoResult: OpenStreetMapsSearchResult = await firstValueFrom(
      this.geoService.reverseLookup(newCoordinates),
    );

    // A map click always moves the pin. If there is already an address text or
    // structured details, ask whether to overwrite them with the new location;
    // otherwise just adopt the new location.
    const hasExistingAddress =
      !!this.selectedLocation?.locationString ||
      !!this.geoService.composeAddressFromParts(this.selectedLocation);

    if (hasExistingAddress) {
      const result = await this.confirmationDialog.getConfirmation(
        $localize`Update address to this location?`,
        $localize`You selected a new location on the map. Do you want to update the address text and details to this new spot?`,
        [
          {
            text: $localize`Keep current text & details`,
            dialogResult: "keep",
            click: () => {},
          },
          {
            text: $localize`Update to new location`,
            dialogResult: "update",
            click: () => {},
          },
        ],
      );

      if (result !== "update") {
        // Move the pin only; keep the existing text and structured parts exactly
        // as they are (do NOT enrich, which would fill empty parts from the new
        // lookup — the user declined to adopt the new location's details).
        this.selectedLocation = {
          ...this.selectedLocation,
          geoLookup: geoResult,
        };
        this.markedLocations.next([geoResult]);
        return;
      }
    }

    // Adopt the new location fully: text and parts are re-derived from the
    // fresh lookup (old parts are intentionally dropped).
    this.updateLocation({ geoLookup: geoResult });
  }

  onSave() {
    this.closeDialog();
  }

  private closeDialog() {
    this.dialogRef.close([this.selectedLocation]);
  }

  updateLocation(event: GeoLocation | undefined) {
    let updatedLocation = this.geoService.enrichGeoLocation(event);

    const displayName = updatedLocation?.geoLookup?.display_name;
    const hasManualAddress =
      updatedLocation?.locationString !== undefined &&
      updatedLocation?.locationString !== null &&
      updatedLocation?.locationString !== "";

    if (displayName && !hasManualAddress) {
      updatedLocation = {
        ...(updatedLocation ?? {}),
        locationString: displayName,
      };
    }

    this.selectedLocation = updatedLocation;
    this.markedLocations.next(
      updatedLocation?.geoLookup ? [updatedLocation.geoLookup] : [],
    );
  }
}
<mat-dialog-content style="max-height: 90vh">
  @if (!data.showMapOnly) {
    <app-address-edit
      [disabled]="data.disabled"
      [selectedLocation]="selectedLocation"
      (selectedLocationChange)="updateLocation($event)"
    ></app-address-edit>
  }

  <app-map
    height="65vh"
    [marked]="markedLocations | async"
    [(displayedProperties)]="data.displayedProperties"
    [entities]="data.entities | async"
    [highlightedEntities]="data.highlightedEntities | async"
    (mapClick)="mapClicked($event)"
    (entityClick)="data.entityClick?.next($event)"
    [showMapOnly]="data.showMapOnly"
  ></app-map>
</mat-dialog-content>

<mat-dialog-actions>
  @if (data.showMapOnly || data.disabled) {
    <button mat-raised-button mat-dialog-close i18n>Close</button>
  } @else {
    @if (helpText) {
      <p class="help-text">{{ helpText }}</p>
    }

    <button mat-stroked-button mat-dialog-close i18n>Cancel</button>
    <button mat-raised-button (click)="onSave()" i18n>Save</button>
  }
</mat-dialog-actions>

./map-popup.component.scss

mat-dialog-actions {
  justify-content: end;
}

.help-text {
  margin: auto 20px;
}

app-address-edit {
  width: 100%;
  display: block;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""