@propmix/overlay-map
v0.1.0
Published
A standalone Angular component, `pmx-overlay-map`, that wraps a Google Map and adds the things apps usually need on top of it:
Keywords
Readme
@propmix/overlay-map
A standalone Angular component, pmx-overlay-map, that wraps a Google Map and adds the things
apps usually need on top of it:
- Draw polygons by clicking on the map, and read the drawn shapes back out.
- Edit and delete existing polygons (drag vertices, select, remove).
- Draw a radius circle around the center point.
- Plot typed markers (subject, comparable statuses, etc.) with colors and labels.
- Show a custom info window (your own Angular component) when a marker is clicked.
1. Prerequisites
This library does not load the Google Maps JavaScript API for you. Your application must load it
before the component is shown, and it must include the marker library (the component uses
AdvancedMarkerElement).
2. Installation
npm install @propmix/overlay-map3. Map ID setup (one time)
Google's AdvancedMarkerElement requires a Map ID. You can give the component a default Map ID
once at the application root, and every pmx-overlay-map will pick it up.
import { provideOverlayMap } from '@propmix/overlay-map';
bootstrapApplication(AppComponent, {
providers: [provideOverlayMap({ mapId: 'YOUR_MAP_ID' })],
});If you use NgModules, add the same provider to your root module's providers array. You can still
override the Map ID per instance with the [mapId] input (see below). If you need the value to come
from a service (for example a company setting), provide the GOOGLE_MAP_ID token directly instead
of using provideOverlayMap:
import { GOOGLE_MAP_ID } from '@propmix/overlay-map';
providers: [
{
provide: GOOGLE_MAP_ID,
useFactory: (settingsService: SettingsService) => settingsService.mapId,
deps: [SettingsService],
},
];4. Quick start
import { Component } from '@angular/core';
import { Coordinate, OverlayMapComponent } from '@propmix/overlay-map';
@Component({
selector: 'app-demo',
standalone: true,
imports: [OverlayMapComponent],
template: `
<div style="height: 400px">
<pmx-overlay-map [center]="center"></pmx-overlay-map>
</div>
`,
})
export class DemoComponent {
center: Coordinate = { lat: 40.7128, lng: -74.006 };
}IMPORTANT: The component fills its parent (
height: 100%). Give the wrapping element a height, otherwise the map collapses to zero pixels and nothing is visible.
5. Inputs
| Input | Type | Default | Description |
| --------------------- | ---------------------- | -------------- | ---------------------------------------------------------------------------------------- |
| center | Coordinate | required | Initial map center. A subject marker (home icon) is always placed here. |
| zoom | number | 14 | Initial zoom level. Ignored once the map fits bounds to a circle, polygons, or markers. |
| readOnly | boolean | false | When true, drawing, editing, and deletion are all disabled. The map is view only. |
| multiPolygon | MultiPolygon \| null | undefined | Polygons to render on load. Editable unless readOnly. The map fits its bounds to them. |
| radius | number \| null | undefined | Draws a circle of this radius in miles around center and fits bounds to it. |
| markerList | MarkerCoordinate[] | undefined | Markers to plot. The map fits bounds to them. |
| hideInfoWindow | boolean | false | When true, clicking a marker does nothing (no info window opens). |
| mapId | string | provided token | Overrides the app-level GOOGLE_MAP_ID for this one map. |
| mapOptions | OverlayMapOptions | see defaults | Zoom limits, pan restriction bounds, and polygon colors. |
| infoWindowComponent | Type<unknown> | undefined | The Angular component rendered inside a marker's info window. See section 8. |
All inputs except center are optional. multiPolygon, radius, and markerList are reactive:
assigning a new value re-renders that layer and re-fits the map.
Output
| Output | Type | Description |
| -------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| notify | EventEmitter<MapNotification> | Fires when the component wants the host to show a message (for example "Add at least 3 points to draw a polygon", or a map-load error). Surface it however you like (snackbar, toast, etc.). |
6. Data types
These are the interfaces used by the inputs and outputs. All are exported from @propmix/overlay-map.
Coordinate
A single latitude/longitude point. Values may be numbers or numeric strings.
interface Coordinate {
lat: string | number;
lng: string | number;
}Polygon and MultiPolygon
A Polygon is the ordered list of corner points of one shape. A MultiPolygon is a list of those
shapes, so a single map can hold several independent polygons.
type Polygon = Coordinate[]; // one shape: its vertices in order
type MultiPolygon = Polygon[]; // many shapes on the same mapMarkerType
The visual style of a marker. Each type maps to a built-in color.
type MarkerType = 'subject' | 'active' | 'pending' | 'foreclosure' | 'sold' | 'disabled';| Type | Appearance |
| ------------- | ----------------------------------- |
| subject | Purple pin with house icon. Default |
| active | Green |
| pending | Amber |
| foreclosure | Magenta |
| sold | Red |
| disabled | Gray |
MarkerCoordinate
A marker is a coordinate plus its presentation and an optional payload.
interface MarkerCoordinate extends Coordinate {
type?: MarkerType; // which built-in style to use
label: string; // text shown inside the marker (ignored for `subject`, which shows the house icon)
isSelected?: boolean; // highlights the marker and raises its z-index
id?: string; // stable identity; used to match/update markers and to find the clicked one
color?: string; // overrides the type color (any CSS color). When selected, becomes the border color
data?: unknown; // arbitrary payload handed to your info-window component (see section 8)
}Tip: always set a unique
id. The component matches markers bylat,lng, andidwhenmarkerListchanges, so a stable id lets it update an existing marker instead of dropping and recreating it, and it is how the clicked marker is mapped back to its data.
OverlayMapOptions
Optional map-level tuning. Every field is optional; sensible defaults apply.
interface OverlayMapOptions {
minZoom?: number; // default 3
maxZoom?: number; // default 19
gestureHandling?: string; // default 'greedy' (see Google Maps gestureHandling)
restrictionBounds?: google.maps.LatLngBoundsLiteral; // limits how far the map can pan
polygonStrokeColor?: string; // default '#000000' - normal polygon outline/fill
polygonSelectedColor?: string; // default '#dc3545' - color of a polygon selected for deletion
}MapNotification
The payload emitted by (notify).
type MapNotificationType = 'success' | 'info' | 'warning' | 'error';
interface MapNotification {
type: MapNotificationType;
message: string;
}MapInfoWindowContext
What your info-window component receives. Covered in section 8.
interface MapInfoWindowContext<T = unknown> {
data: T; // the clicked marker's `data` payload
marker: MarkerCoordinate; // the full clicked marker
close: () => void; // call to close the info window
}7. Drawing polygons and reading them back
Drawing (interactive)
When the map is not readOnly, drawing is always armed:
- Click on the map to drop the first vertex, then keep clicking to add more.
- Finish the shape by either double-clicking, or clicking the first vertex again.
- A polygon needs at least 3 points. If the user tries to close it with fewer, the component
emits a
notifywarning instead of closing.
You can draw several polygons in a row; each finished shape stays on the map and a new one starts on the next click.
Editing and deleting
- Finished polygons are editable: drag a vertex to move it, or drag a midpoint to add one.
- Click a polygon to select it. It turns red (
polygonSelectedColor) and a round trash badge appears at its center. - Click the trash badge to delete that polygon. Click the polygon again to deselect without deleting.
Reading the drawn shapes: getPolygons()
This is the key method for capturing user input. Get a reference to the component with @ViewChild,
then call getPolygons() whenever you want the current shapes (for example when the user clicks
Save). It returns the live vertices of every polygon on the map, or null if there are none.
import { Component, ViewChild } from '@angular/core';
import { MultiPolygon, OverlayMapComponent } from '@propmix/overlay-map';
@Component({
standalone: true,
imports: [OverlayMapComponent],
template: `
<div style="height: 400px">
<pmx-overlay-map [center]="center" [multiPolygon]="initialShapes"></pmx-overlay-map>
</div>
<button (click)="save()">Save</button>
`,
})
export class BoundaryEditorComponent {
@ViewChild(OverlayMapComponent) map!: OverlayMapComponent;
center = { lat: 40.7128, lng: -74.006 };
initialShapes: MultiPolygon | null = null;
save(): void {
const shapes: MultiPolygon | null = this.map.getPolygons();
// `shapes` is the up-to-date geometry, including edits and newly drawn polygons.
// Each coordinate is returned as { lat: number, lng: number }.
console.log(shapes);
}
}getPolygons() always reflects the current state, so it includes vertex edits the user made after
loading and any polygons they drew. Pass the same data back through [multiPolygon] later to restore
the shapes.
Other public methods
| Method | Description |
| -------------------------------------------- | ------------------------------------------------------------------------------------------ |
| getPolygons(): MultiPolygon \| null | Returns the current polygon geometry, or null if none. |
| openMarker(marker: MarkerCoordinate): void | Programmatically opens the info window for a marker (same effect as the user clicking it). |
| deleteOverlay(): void | Removes all polygons and the circle, and clears any selection or in-progress drawing. |
8. Custom marker info windows
When a marker is clicked, the component can render your own Angular component inside the Google info window. This keeps the library agnostic about how your marker details should look.
Step 1. Build any standalone component. Inject MAP_INFO_WINDOW_DATA to receive the clicked
marker's context (data, the full marker, and a close callback).
import { Component, inject } from '@angular/core';
import { MAP_INFO_WINDOW_DATA, MapInfoWindowContext } from '@propmix/overlay-map';
interface CompInfo {
address: string;
price: number;
}
@Component({
selector: 'app-comp-info',
standalone: true,
template: `
<h4>{{ info.address }}</h4>
<p>{{ info.price | currency }}</p>
<button (click)="ctx.close()">Close</button>
`,
})
export class CompInfoComponent {
private ctx: MapInfoWindowContext<CompInfo> = inject(MAP_INFO_WINDOW_DATA);
info = this.ctx.data;
}Step 2. Attach the payload to each marker via data, and pass the component to the map.
markers: MarkerCoordinate[] = [
{
lat: 40.72,
lng: -74.0,
type: 'sold',
label: '$1.2M',
id: 'comp-1',
data: { address: '123 Main St', price: 1200000 } satisfies CompInfo,
},
];
infoWindow = CompInfoComponent;<pmx-overlay-map [center]="center" [markerList]="markers" [infoWindowComponent]="infoWindow"> </pmx-overlay-map>When a marker is clicked, CompInfoComponent is created with that marker's data, rendered inside
the info window, and destroyed when the window closes. Set [hideInfoWindow]="true" to turn marker
clicks off entirely.
9. Example: Boundary editor in a dialog
This mirrors a common pattern: edit a property boundary in a modal and return the result.
import { Component, ViewChild } from '@angular/core';
import { MapNotification, MultiPolygon, OverlayMapComponent } from '@propmix/overlay-map';
@Component({
standalone: true,
imports: [OverlayMapComponent],
template: `
<div style="height: 70vh">
<pmx-overlay-map
[center]="center"
[multiPolygon]="boundary"
[readOnly]="readOnly"
(notify)="onNotify($event)"
></pmx-overlay-map>
</div>
<button (click)="save()">Save</button>
`,
})
export class BoundaryDialogComponent {
@ViewChild(OverlayMapComponent) map!: OverlayMapComponent;
center = { lat: 40.7128, lng: -74.006 };
boundary: MultiPolygon | null = null;
readOnly = false;
onNotify(n: MapNotification): void {
// forward to your snackbar/toast service
console.log(n.type, n.message);
}
save(): void {
const result = this.map.getPolygons();
// close the dialog and return `result`
}
}