Skip to content

Commit 604b99e

Browse files
authored
feat: implement variable location-formats (#22)
allows the position to be specified in various different formats.
1 parent 14767bb commit 604b99e

8 files changed

+339
-230
lines changed

examples/playground/src/code-samples/02.update-position.ts

+20-4
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,26 @@
22
import {Marker} from '@ubilabs/google-maps-marker';
33

44
export default (map: google.maps.Map) => {
5-
const m1 = new Marker({map});
5+
// position can be speecified in a number of different formats:
6+
const m1 = new Marker({
7+
position: new google.maps.LatLng(53.555, 10.01),
8+
map
9+
});
610

7-
// at any point in time we can change the position
8-
m1.position = {lat: 53.555, lng: 10.001};
11+
// google.maps.LatLngLiteral
12+
const m2 = new Marker({
13+
position: {lat: 53.555, lng: 10.0},
14+
map
15+
});
916

10-
map.setCenter(m1.position);
17+
// GeoJSON style [lng, lat] format
18+
const m3 = new Marker({
19+
position: [10.02, 53.555],
20+
map
21+
});
22+
23+
// the position can be accessed and changed anytime, even to a different format
24+
setTimeout(() => {
25+
m1.position = [10.01, 53.5];
26+
}, 2000);
1127
};

src/computed-marker-attributes.ts

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import {toLatLng} from './position-formats';
2+
import {
3+
AttributeKey,
4+
attributeKeys,
5+
Attributes,
6+
Position
7+
} from './marker-attributes';
8+
9+
import type {Marker} from './marker';
10+
import type {StaticAttributes} from './marker-attributes';
11+
12+
/**
13+
* ComputedMarkerAttributes resolves all attributes based on dynamic and static
14+
* values and makes them behave as if there were only static attributes.
15+
*/
16+
export class ComputedMarkerAttributes<T = unknown>
17+
implements Partial<StaticAttributes>
18+
{
19+
private marker_: Marker<T>;
20+
private callbackDepth_: number = 0;
21+
22+
// Attributes are declaration-only, the implementataion uses dynamic
23+
// getters/setters created in the static initializer
24+
25+
// note: internally, the position-attribute uses the google.maps.LatLng
26+
// type instead of the generic Position type.
27+
declare readonly position?: google.maps.LatLng;
28+
declare readonly draggable?: StaticAttributes['draggable'];
29+
declare readonly collisionBehavior?: StaticAttributes['collisionBehavior'];
30+
declare readonly title?: StaticAttributes['title'];
31+
declare readonly zIndex?: StaticAttributes['zIndex'];
32+
33+
declare readonly glyph?: StaticAttributes['glyph'];
34+
declare readonly scale?: StaticAttributes['scale'];
35+
declare readonly color?: StaticAttributes['color'];
36+
declare readonly backgroundColor?: StaticAttributes['backgroundColor'];
37+
declare readonly borderColor?: StaticAttributes['borderColor'];
38+
declare readonly glyphColor?: StaticAttributes['glyphColor'];
39+
declare readonly icon?: StaticAttributes['icon'];
40+
41+
constructor(marker: Marker<T>) {
42+
this.marker_ = marker;
43+
}
44+
45+
private getComputedAttributeValue<TKey extends AttributeKey>(
46+
key: TKey
47+
): StaticAttributes[TKey] | undefined {
48+
const value = this.marker_[key];
49+
if (typeof value !== 'function') {
50+
return this.marker_[key] as StaticAttributes[TKey];
51+
}
52+
53+
this.callbackDepth_++;
54+
if (this.callbackDepth_ > 10) {
55+
throw new Error(
56+
'maximum recursion depth reached. ' +
57+
'This is probably caused by a cyclic dependency in dynamic attributes.'
58+
);
59+
}
60+
61+
const {map, data, marker} = this.marker_.getDynamicAttributeState();
62+
const res = value({data, map, marker, attr: this});
63+
this.callbackDepth_--;
64+
65+
return res as StaticAttributes[TKey];
66+
}
67+
68+
/**
69+
* The static initializer sets up the implementation for the properties of the
70+
* ComputedMarkerAttributes class.
71+
*/
72+
static {
73+
Object.defineProperty(this.prototype, 'position', {
74+
get(this: ComputedMarkerAttributes) {
75+
const userValue = this.getComputedAttributeValue('position');
76+
77+
if (userValue) return toLatLng(userValue);
78+
}
79+
});
80+
81+
// position is treated separately, so it is skipped here
82+
for (const key of attributeKeys) {
83+
if (key === 'position') {
84+
continue;
85+
}
86+
87+
Object.defineProperty(this.prototype, key, {
88+
get(this: ComputedMarkerAttributes) {
89+
return this.getComputedAttributeValue(key);
90+
}
91+
});
92+
}
93+
}
94+
}

src/icons.ts

+3
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export function PlaceIcons(): IconProvider {
7171

7272
/**
7373
* Creates the span element for the specified icon.
74+
*
7475
* @param className
7576
* @param content
7677
*/
@@ -86,6 +87,7 @@ function createSpan(className: string, content: string): HTMLElement {
8687
/**
8788
* Checks existing google fonts link tags for the specified material-icons
8889
* font-family.
90+
*
8991
* @param family
9092
*/
9193
function isFontLoaded(family: string): boolean {
@@ -116,6 +118,7 @@ function isFontLoaded(family: string): boolean {
116118

117119
/**
118120
* Appends the stylesheet to load the specified font-family.
121+
*
119122
* @param family
120123
*/
121124
function appendFontStylesheet(family: string) {

src/marker-attributes.ts

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import type {CollisionBehavior, MarkerState} from './marker';
2+
import type {MapState} from './map-state-observer';
3+
import type {ComputedMarkerAttributes} from './computed-marker-attributes';
4+
5+
// These keys are used to create the dynamic properties (mostly to save us
6+
// from having to type them all out and to make adding attributes a bit easier).
7+
export const attributeKeys: readonly AttributeKey[] = [
8+
'position',
9+
'draggable',
10+
'collisionBehavior',
11+
'title',
12+
'zIndex',
13+
14+
'color',
15+
'backgroundColor',
16+
'borderColor',
17+
'glyphColor',
18+
19+
'icon',
20+
'glyph',
21+
'scale'
22+
] as const;
23+
24+
export type LngLatArray = [lng: number, lat: number];
25+
export type Position =
26+
| google.maps.LatLngLiteral
27+
| google.maps.LatLng
28+
| {latitude: number; longitude: number}
29+
| LngLatArray;
30+
31+
/** StaticAttributes contains the base definition for all attribute-values. */
32+
export interface StaticAttributes {
33+
/**
34+
* The position of the marker on the map, specified as
35+
* google.maps.LatLngLiteral.
36+
*/
37+
position: Position;
38+
/**
39+
* Should the marker be draggable? In this case whatever value is written to
40+
* the position-attribute will be automatically overwritten by the maps-API
41+
* when the position changes.
42+
*/
43+
draggable: boolean;
44+
collisionBehavior: CollisionBehavior;
45+
title: string;
46+
zIndex: number;
47+
48+
color: string;
49+
backgroundColor: string;
50+
borderColor: string;
51+
glyphColor: string;
52+
icon: string;
53+
glyph: string | Element | URL;
54+
scale: number;
55+
}
56+
57+
// just the keys for all attributes
58+
export type AttributeKey = keyof StaticAttributes;
59+
60+
/**
61+
* DynamicAttributeValues are functions that take a state object consisting of
62+
* internal state and user-data and return the attribute value. They are
63+
* evaluated whenever a state-change happens or user-data is updated.
64+
*/
65+
export type DynamicAttributeValue<TUserData, TAttr> = (
66+
state: {data: TUserData | null} & {
67+
map: MapState;
68+
marker: MarkerState;
69+
attr: ComputedMarkerAttributes<TUserData>;
70+
}
71+
) => TAttr | undefined;
72+
73+
/** An AttributeValue can be either a static value of a dynamic attribute. */
74+
export type AttributeValue<TUserData, T> =
75+
| T
76+
| DynamicAttributeValue<TUserData, T>;
77+
78+
/** Internally used to store the attributes with dynamic values separately. */
79+
export type DynamicAttributes<T> = {
80+
[key in AttributeKey]: DynamicAttributeValue<T, StaticAttributes[key]>;
81+
};
82+
83+
/**
84+
* These are the attribute-types as specified to the constructor and individual
85+
* attribute setters
86+
*/
87+
export type Attributes<T = unknown> = {
88+
[key in AttributeKey]: AttributeValue<T, StaticAttributes[key]>;
89+
};

src/marker-collection.ts

+18-19
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
11
import {Marker, MarkerOptions} from './marker';
2-
import type {Attributes} from './marker';
32
import {warnOnce} from './util';
3+
import type {Attributes} from './marker-attributes';
44

55
/**
6-
* a collection provides bindings between an array of arbitrary records and
7-
* the corresponding markers.
6+
* A collection provides bindings between an array of arbitrary records and the
7+
* corresponding markers.
88
*
9-
* - attributes: attributes are shared with all markers, which is where
10-
* dynamic attributes can really shine
11-
*
12-
* - data-updates: data in the collection can be updated after creation.
13-
* This will assume that complete sets of records are passed on every
14-
* update. If incremental updates are needed, those have to be applied
15-
* to the data before updating the marker collection.
16-
* When transitions are implemented (also for performance reasons), it
17-
* will become important to recognize identical records, so those can be
18-
* updated instead of re-created with every update.
9+
* - Attributes: attributes are shared with all markers, which is where dynamic
10+
* attributes can really shine
11+
* - Data-updates: data in the collection can be updated after creation. This will
12+
* assume that complete sets of records are passed on every update. If
13+
* incremental updates are needed, those have to be applied to the data before
14+
* updating the marker collection. When transitions are implemented (also for
15+
* performance reasons), it will become important to recognize identical
16+
* records, so those can be updated instead of re-created with every update.
1917
*/
2018

2119
/**
22-
* Markers in a collection can have additional (virtual) attributes that
23-
* are defined here.
20+
* Markers in a collection can have additional (virtual) attributes that are
21+
* defined here.
2422
*/
2523
export type CollectionMarkerAttributes<T> = Attributes<T> & {
2624
key: (data: T) => string;
@@ -39,15 +37,16 @@ export class MarkerCollection<TUserData extends object = object> {
3937
private generatedKeyCache_ = new WeakMap<TUserData, string>();
4038

4139
/**
42-
* Creates a new MarkerCollection without specifying the data yet.
43-
* This could be useful since fetching the data typically happens at
44-
* a different time Providing data when creating the marker-collection
45-
* is optional.
40+
* Creates a new MarkerCollection without specifying the data yet. This could
41+
* be useful since fetching the data typically happens at a different time
42+
* Providing data when creating the marker-collection is optional.
43+
*
4644
* @param options
4745
*/
4846
constructor(options: MarkerCollectionOptions<TUserData>);
4947
/**
5048
* Creates a new MarkerCollection with existing data.
49+
*
5150
* @param data
5251
* @param options
5352
*/

0 commit comments

Comments
 (0)