@trackunit/react-map-adapter-google 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.cjs.js CHANGED
@@ -180,6 +180,16 @@ class GoogleMapsLayerPort {
180
180
  this.interactionHandlers = new Set();
181
181
  this.backgroundClickHandlers = new Set();
182
182
  this.entityClickedInTick = false;
183
+ /**
184
+ * True for one tick after a point (marker/cluster) click is emitted. Symbol
185
+ * markers render on a full-viewport canvas overlay that is not a Google-native
186
+ * marker, so — unlike `AdvancedMarkerElement` DOM markers, whose `gmp-click`
187
+ * makes Google swallow the underlying map click — a single click on a symbol
188
+ * marker overlapping a polygon fires BOTH the canvas marker click and the
189
+ * polygon's geometric click. This flag lets the trailing shape click bail so
190
+ * the marker the user visibly clicked wins.
191
+ */
192
+ this.pointEntityClickedInTick = false;
183
193
  this.mapClickListener = null;
184
194
  // Tracked sources, keyed by source ID
185
195
  this.markerSources = new Map();
@@ -1969,6 +1979,9 @@ class GoogleMapsLayerPort {
1969
1979
  return new google.maps.Polygon({
1970
1980
  paths,
1971
1981
  map,
1982
+ // Great-circle edges natively (no densification needed); honours the shape
1983
+ // style flag, defaulting to true per the ShapeStyle.geodesic contract.
1984
+ geodesic: style.geodesic ?? true,
1972
1985
  fillColor: style.fill ?? "rgba(0,0,0,0.1)",
1973
1986
  fillOpacity: style.fillOpacity ?? this.shapeStyleDefaults.polygon.fillOpacity,
1974
1987
  strokeColor: style.stroke ?? "#000000",
@@ -1982,6 +1995,9 @@ class GoogleMapsLayerPort {
1982
1995
  return new google.maps.Polyline({
1983
1996
  path,
1984
1997
  map,
1998
+ // Match the polygon: native great-circle stroke so a tiled outline curves
1999
+ // the same way as its densified fill instead of drawing a straight chord.
2000
+ geodesic: style.geodesic ?? true,
1985
2001
  strokeColor: style.stroke ?? "#000000",
1986
2002
  strokeWeight: style.strokeWidth ?? this.shapeStyleDefaults.line.strokeWidth,
1987
2003
  strokeOpacity: style.strokeOpacity ?? this.shapeStyleDefaults.line.strokeOpacity,
@@ -1998,6 +2014,11 @@ class GoogleMapsLayerPort {
1998
2014
  return new google.maps.Polygon({
1999
2015
  paths,
2000
2016
  map,
2017
+ // Intentionally NOT geodesic: this is clipped tiling geometry. Its original
2018
+ // edges are already densified to great-circle arcs by the fill-tiling pass,
2019
+ // while the clip cuts must stay as straight Web-Mercator segments so they
2020
+ // land exactly on the covering shape's rendered edge. geodesic:true would
2021
+ // re-curve those straight cuts and break the tiling seam.
2001
2022
  fillColor: style.fill ?? "rgba(0,0,0,0.1)",
2002
2023
  fillOpacity: style.fillOpacity ?? this.shapeStyleDefaults.polygon.fillOpacity,
2003
2024
  strokeWeight: 0,
@@ -2364,10 +2385,47 @@ class GoogleMapsLayerPort {
2364
2385
  /** Emit entity interaction event to all subscribed handlers */
2365
2386
  emitEntityInteraction(type, entity) {
2366
2387
  if (type === "click") {
2388
+ // On Google Maps vector maps, a canvas OverlayView (used for symbol
2389
+ // markers and clusters) emits its click event before the tile renderer
2390
+ // dispatches geometric polygon events for the same user gesture. This is
2391
+ // because canvas draw calls and their DOM-level pointer events are
2392
+ // processed in the same microtask / paint frame, which completes before
2393
+ // the vector-tile renderer's async polygon hit-test resolves. In
2394
+ // practice this ordering has been consistent across all tested Google
2395
+ // Maps JS API versions; however it is not contractually guaranteed by
2396
+ // Google's public documentation.
2397
+ //
2398
+ // We exploit this ordering to let the point-entity click (marker /
2399
+ // cluster) win when it spatially overlaps a shape: the marker sets
2400
+ // `pointEntityClickedInTick` first, and the shape click that follows in
2401
+ // the same tick is suppressed here.
2402
+ //
2403
+ // Fallback risk: if Google ever reverses the order, the shape click
2404
+ // would arrive first and be emitted normally, then the marker click
2405
+ // would also arrive and be emitted — resulting in two consecutive
2406
+ // selection events for the same gesture. The user experience degrades to
2407
+ // "shape is briefly selected, then replaced by the marker", which is
2408
+ // recoverable with a second click. No data loss or error occurs.
2409
+ //
2410
+ // A position-based deduplication approach (comparing click coordinates
2411
+ // within a pixel tolerance) would be more robust against ordering
2412
+ // changes, but adds meaningful complexity: we would need to store the
2413
+ // last click coordinate, handle clustered events whose centres may not
2414
+ // match pixel-exactly, and choose a tolerance that works across zoom
2415
+ // levels. The ordering-based guard is simpler and has been reliable
2416
+ // in production.
2417
+ if (entity.type === "shape" && this.pointEntityClickedInTick)
2418
+ return;
2367
2419
  this.entityClickedInTick = true;
2368
2420
  setTimeout(() => {
2369
2421
  this.entityClickedInTick = false;
2370
2422
  }, 0);
2423
+ if (entity.type === "marker" || entity.type === "cluster") {
2424
+ this.pointEntityClickedInTick = true;
2425
+ setTimeout(() => {
2426
+ this.pointEntityClickedInTick = false;
2427
+ }, 0);
2428
+ }
2371
2429
  }
2372
2430
  const event = { type, entity };
2373
2431
  this.interactionHandlers.forEach(handler => handler(event));
package/index.esm.js CHANGED
@@ -178,6 +178,16 @@ class GoogleMapsLayerPort {
178
178
  this.interactionHandlers = new Set();
179
179
  this.backgroundClickHandlers = new Set();
180
180
  this.entityClickedInTick = false;
181
+ /**
182
+ * True for one tick after a point (marker/cluster) click is emitted. Symbol
183
+ * markers render on a full-viewport canvas overlay that is not a Google-native
184
+ * marker, so — unlike `AdvancedMarkerElement` DOM markers, whose `gmp-click`
185
+ * makes Google swallow the underlying map click — a single click on a symbol
186
+ * marker overlapping a polygon fires BOTH the canvas marker click and the
187
+ * polygon's geometric click. This flag lets the trailing shape click bail so
188
+ * the marker the user visibly clicked wins.
189
+ */
190
+ this.pointEntityClickedInTick = false;
181
191
  this.mapClickListener = null;
182
192
  // Tracked sources, keyed by source ID
183
193
  this.markerSources = new Map();
@@ -1967,6 +1977,9 @@ class GoogleMapsLayerPort {
1967
1977
  return new google.maps.Polygon({
1968
1978
  paths,
1969
1979
  map,
1980
+ // Great-circle edges natively (no densification needed); honours the shape
1981
+ // style flag, defaulting to true per the ShapeStyle.geodesic contract.
1982
+ geodesic: style.geodesic ?? true,
1970
1983
  fillColor: style.fill ?? "rgba(0,0,0,0.1)",
1971
1984
  fillOpacity: style.fillOpacity ?? this.shapeStyleDefaults.polygon.fillOpacity,
1972
1985
  strokeColor: style.stroke ?? "#000000",
@@ -1980,6 +1993,9 @@ class GoogleMapsLayerPort {
1980
1993
  return new google.maps.Polyline({
1981
1994
  path,
1982
1995
  map,
1996
+ // Match the polygon: native great-circle stroke so a tiled outline curves
1997
+ // the same way as its densified fill instead of drawing a straight chord.
1998
+ geodesic: style.geodesic ?? true,
1983
1999
  strokeColor: style.stroke ?? "#000000",
1984
2000
  strokeWeight: style.strokeWidth ?? this.shapeStyleDefaults.line.strokeWidth,
1985
2001
  strokeOpacity: style.strokeOpacity ?? this.shapeStyleDefaults.line.strokeOpacity,
@@ -1996,6 +2012,11 @@ class GoogleMapsLayerPort {
1996
2012
  return new google.maps.Polygon({
1997
2013
  paths,
1998
2014
  map,
2015
+ // Intentionally NOT geodesic: this is clipped tiling geometry. Its original
2016
+ // edges are already densified to great-circle arcs by the fill-tiling pass,
2017
+ // while the clip cuts must stay as straight Web-Mercator segments so they
2018
+ // land exactly on the covering shape's rendered edge. geodesic:true would
2019
+ // re-curve those straight cuts and break the tiling seam.
1999
2020
  fillColor: style.fill ?? "rgba(0,0,0,0.1)",
2000
2021
  fillOpacity: style.fillOpacity ?? this.shapeStyleDefaults.polygon.fillOpacity,
2001
2022
  strokeWeight: 0,
@@ -2362,10 +2383,47 @@ class GoogleMapsLayerPort {
2362
2383
  /** Emit entity interaction event to all subscribed handlers */
2363
2384
  emitEntityInteraction(type, entity) {
2364
2385
  if (type === "click") {
2386
+ // On Google Maps vector maps, a canvas OverlayView (used for symbol
2387
+ // markers and clusters) emits its click event before the tile renderer
2388
+ // dispatches geometric polygon events for the same user gesture. This is
2389
+ // because canvas draw calls and their DOM-level pointer events are
2390
+ // processed in the same microtask / paint frame, which completes before
2391
+ // the vector-tile renderer's async polygon hit-test resolves. In
2392
+ // practice this ordering has been consistent across all tested Google
2393
+ // Maps JS API versions; however it is not contractually guaranteed by
2394
+ // Google's public documentation.
2395
+ //
2396
+ // We exploit this ordering to let the point-entity click (marker /
2397
+ // cluster) win when it spatially overlaps a shape: the marker sets
2398
+ // `pointEntityClickedInTick` first, and the shape click that follows in
2399
+ // the same tick is suppressed here.
2400
+ //
2401
+ // Fallback risk: if Google ever reverses the order, the shape click
2402
+ // would arrive first and be emitted normally, then the marker click
2403
+ // would also arrive and be emitted — resulting in two consecutive
2404
+ // selection events for the same gesture. The user experience degrades to
2405
+ // "shape is briefly selected, then replaced by the marker", which is
2406
+ // recoverable with a second click. No data loss or error occurs.
2407
+ //
2408
+ // A position-based deduplication approach (comparing click coordinates
2409
+ // within a pixel tolerance) would be more robust against ordering
2410
+ // changes, but adds meaningful complexity: we would need to store the
2411
+ // last click coordinate, handle clustered events whose centres may not
2412
+ // match pixel-exactly, and choose a tolerance that works across zoom
2413
+ // levels. The ordering-based guard is simpler and has been reliable
2414
+ // in production.
2415
+ if (entity.type === "shape" && this.pointEntityClickedInTick)
2416
+ return;
2365
2417
  this.entityClickedInTick = true;
2366
2418
  setTimeout(() => {
2367
2419
  this.entityClickedInTick = false;
2368
2420
  }, 0);
2421
+ if (entity.type === "marker" || entity.type === "cluster") {
2422
+ this.pointEntityClickedInTick = true;
2423
+ setTimeout(() => {
2424
+ this.pointEntityClickedInTick = false;
2425
+ }, 0);
2426
+ }
2369
2427
  }
2370
2428
  const event = { type, entity };
2371
2429
  this.interactionHandlers.forEach(handler => handler(event));
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@trackunit/react-map-adapter-google",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
7
7
  "node": ">=24.x"
8
8
  },
9
9
  "dependencies": {
10
- "@trackunit/react-map-adapter-shared": "0.0.24",
11
- "@trackunit/react-map-color-utils": "0.0.9",
12
- "@trackunit/geo-json-utils": "1.14.43",
10
+ "@trackunit/react-map-adapter-shared": "0.0.26",
11
+ "@trackunit/react-map-color-utils": "0.0.11",
12
+ "@trackunit/geo-json-utils": "1.14.45",
13
13
  "@googlemaps/markerclusterer": "^2.6.2",
14
14
  "@vis.gl/react-google-maps": "^1.7.1",
15
15
  "es-toolkit": "^1.39.10",
@@ -31,6 +31,16 @@ export declare class GoogleMapsLayerPort implements LayerPort {
31
31
  private readonly interactionHandlers;
32
32
  private readonly backgroundClickHandlers;
33
33
  private entityClickedInTick;
34
+ /**
35
+ * True for one tick after a point (marker/cluster) click is emitted. Symbol
36
+ * markers render on a full-viewport canvas overlay that is not a Google-native
37
+ * marker, so — unlike `AdvancedMarkerElement` DOM markers, whose `gmp-click`
38
+ * makes Google swallow the underlying map click — a single click on a symbol
39
+ * marker overlapping a polygon fires BOTH the canvas marker click and the
40
+ * polygon's geometric click. This flag lets the trailing shape click bail so
41
+ * the marker the user visibly clicked wins.
42
+ */
43
+ private pointEntityClickedInTick;
34
44
  private mapClickListener;
35
45
  private readonly markerSources;
36
46
  /** featureId → element, keyed by sourceId. Cleared when a source is removed. */