Skip to content

Commit fb54985

Browse files
feat(google-maps): add ScriptGoogleMapsGeoJson component (#656)
Co-authored-by: Damian Głowala <damian.glowala.rebkow@gmail.com>
1 parent 1128933 commit fb54985

File tree

7 files changed

+449
-2
lines changed

7 files changed

+449
-2
lines changed

docs/content/scripts/google-maps.md

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,7 @@ All Google Maps SFC components must work within a `<ScriptGoogleMaps>`{lang="htm
434434
- `<ScriptGoogleMapsPolyline>`{lang="html"} - Line paths
435435
- `<ScriptGoogleMapsRectangle>`{lang="html"} - Rectangular overlays
436436
- `<ScriptGoogleMapsHeatmapLayer>`{lang="html"} - Heatmap visualization
437+
- `<ScriptGoogleMapsGeoJson>`{lang="html"} - GeoJSON data layers
437438

438439
### Basic Usage
439440

@@ -521,6 +522,48 @@ onMounted(() => {
521522
</template>
522523
```
523524

525+
**GeoJSON Data Layer**
526+
527+
Load GeoJSON from a URL or inline object and apply custom styling:
528+
529+
```vue
530+
<script setup lang="ts">
531+
const geoJsonStyle = {
532+
fillColor: '#4285F4',
533+
fillOpacity: 0.4,
534+
strokeColor: '#4285F4',
535+
strokeWeight: 2,
536+
}
537+
538+
function handleFeatureClick(event: google.maps.Data.MouseEvent) {
539+
console.log('Clicked feature:', event.feature.getProperty('name'))
540+
}
541+
</script>
542+
543+
<template>
544+
<ScriptGoogleMaps api-key="your-api-key">
545+
<!-- Load from URL -->
546+
<ScriptGoogleMapsGeoJson
547+
src="https://example.com/data.geojson"
548+
:style="geoJsonStyle"
549+
@click="handleFeatureClick"
550+
/>
551+
552+
<!-- Or pass inline GeoJSON -->
553+
<ScriptGoogleMapsGeoJson
554+
:src="{
555+
type: 'FeatureCollection',
556+
features: [{
557+
type: 'Feature',
558+
geometry: { type: 'Point', coordinates: [150.644, -34.397] },
559+
properties: { name: 'My Point' },
560+
}],
561+
}"
562+
/>
563+
</ScriptGoogleMaps>
564+
</template>
565+
```
566+
524567
### Component Hierarchy
525568

526569
```text
@@ -531,10 +574,11 @@ ScriptGoogleMaps (root)
531574
├── ScriptGoogleMapsAdvancedMarkerElement
532575
│ ├── ScriptGoogleMapsPinElement (optional)
533576
│ └── ScriptGoogleMapsInfoWindow (optional)
577+
├── ScriptGoogleMapsGeoJson (GeoJSON data layer)
534578
└── ScriptGoogleMapsCircle / Polygon / Polyline / Rectangle / HeatmapLayer
535579
```
536580

537-
All SFC components accept an `options` prop matching their Google Maps API options type (excluding `map`, which the parent component injects automatically). Options are reactive - changes update the basic Google Maps object. Components clean up automatically on unmount.
581+
Most SFC components accept an `options` prop matching their Google Maps API options type (excluding `map`, which the parent component injects automatically). `ScriptGoogleMapsGeoJson` uses `src` and `style` props instead. Options are reactive - changes update the basic Google Maps object. Components clean up automatically on unmount.
538582

539583
### Component Reference
540584

@@ -550,6 +594,34 @@ All SFC components accept an `options` prop matching their Google Maps API optio
550594
| `ScriptGoogleMapsPolyline` | `google.maps.PolylineOptions` | |
551595
| `ScriptGoogleMapsRectangle` | `google.maps.RectangleOptions` | |
552596
| `ScriptGoogleMapsHeatmapLayer` | `google.maps.visualization.HeatmapLayerOptions` | |
597+
| `ScriptGoogleMapsGeoJson` | `src`: `string \| object`, `style`: `google.maps.Data.StylingFunction \| google.maps.Data.StyleOptions` | Emits mouse & feature events |
598+
599+
### `ScriptGoogleMapsGeoJson`{lang="html"}
600+
601+
Loads GeoJSON data onto the map using `google.maps.Data` and either `loadGeoJson` (when `src` is a URL) or `addGeoJson` (when `src` is an inline object).
602+
603+
#### Props
604+
605+
| Prop | Type | Description |
606+
|---|---|---|
607+
| `src` | `string \| object` | URL to load via `loadGeoJson()`{lang="ts"} or a GeoJSON object to add via `addGeoJson()`{lang="ts"}. Reactive - changing it clears existing features and loads the new data. |
608+
| `style` | `google.maps.Data.StylingFunction \| google.maps.Data.StyleOptions` | Styling applied to the data layer. Reactive with deep watching. |
609+
610+
#### Events
611+
612+
**Mouse events**: emitted with a `google.maps.Data.MouseEvent` payload:
613+
614+
`click`, `contextmenu`, `dblclick`, `mousedown`, `mousemove`, `mouseout`, `mouseover`, `mouseup`
615+
616+
**Feature lifecycle events:**
617+
618+
| Event | Payload |
619+
|---|---|
620+
| `addfeature` | `google.maps.Data.AddFeatureEvent` |
621+
| `removefeature` | `google.maps.Data.RemoveFeatureEvent` |
622+
| `setgeometry` | `google.maps.Data.SetGeometryEvent` |
623+
| `setproperty` | `google.maps.Data.SetPropertyEvent` |
624+
| `removeproperty` | `google.maps.Data.RemovePropertyEvent` |
553625

554626
## [`useScriptGoogleMaps()`{lang="ts"}](/scripts/google-maps){lang="ts"}
555627

playground/pages/third-parties/google-maps/sfcs.vue

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,28 @@ const heatmapLayerData = ref<google.maps.LatLng[]>([])
3636
3737
const isCircleShown = ref(false)
3838
39+
const isGeoJsonShown = ref(false)
40+
41+
const geoJsonData = {
42+
type: 'FeatureCollection',
43+
features: [
44+
{
45+
type: 'Feature',
46+
geometry: {
47+
type: 'Polygon',
48+
coordinates: [[
49+
[151.20, -33.87],
50+
[151.25, -33.87],
51+
[151.25, -33.90],
52+
[151.20, -33.90],
53+
[151.20, -33.87],
54+
]],
55+
},
56+
properties: { name: 'Sydney CBD' },
57+
},
58+
],
59+
}
60+
3961
const googleMapsRef = useTemplateRef('googleMapsRef')
4062
4163
whenever(() => googleMapsRef.value?.googleMaps, (googleMaps) => {
@@ -191,6 +213,17 @@ whenever(() => googleMapsRef.value?.googleMaps, (googleMaps) => {
191213
}"
192214
/>
193215

216+
<ScriptGoogleMapsGeoJson
217+
v-if="isGeoJsonShown"
218+
:src="geoJsonData"
219+
:style="{
220+
fillColor: '#4285F4',
221+
fillOpacity: 0.3,
222+
strokeColor: '#4285F4',
223+
strokeWeight: 2,
224+
}"
225+
/>
226+
194227
<ScriptGoogleMapsCircle
195228
v-if="isCircleShown"
196229
:options="{
@@ -269,6 +302,13 @@ whenever(() => googleMapsRef.value?.googleMaps, (googleMaps) => {
269302
{{ `${isHeatmapLayerShown ? 'Hide' : 'Show'} heatmap layer` }}
270303
</button>
271304

305+
<button
306+
class="bg-[#ffa500] rounded-lg px-2 py-1"
307+
@click="isGeoJsonShown = !isGeoJsonShown"
308+
>
309+
{{ `${isGeoJsonShown ? 'Hide' : 'Show'} geojson` }}
310+
</button>
311+
272312
<button
273313
class="bg-[#ffa500] rounded-lg px-2 py-1"
274314
@click="isCircleShown = !isCircleShown"
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<script setup lang="ts">
2+
import { watch } from 'vue'
3+
import { useGoogleMapsResource } from './useGoogleMapsResource'
4+
5+
const props = defineProps<{
6+
src: string | object
7+
style?: google.maps.Data.StylingFunction | google.maps.Data.StyleOptions
8+
}>()
9+
10+
const emit = defineEmits<{
11+
(event: typeof dataEvents[number], payload: google.maps.Data.MouseEvent): void
12+
(event: 'addfeature', payload: google.maps.Data.AddFeatureEvent): void
13+
(event: 'removefeature', payload: google.maps.Data.RemoveFeatureEvent): void
14+
(event: 'setgeometry', payload: google.maps.Data.SetGeometryEvent): void
15+
(event: 'setproperty', payload: google.maps.Data.SetPropertyEvent): void
16+
(event: 'removeproperty', payload: google.maps.Data.RemovePropertyEvent): void
17+
}>()
18+
19+
const dataEvents = [
20+
'click',
21+
'contextmenu',
22+
'dblclick',
23+
'mousedown',
24+
'mousemove',
25+
'mouseout',
26+
'mouseover',
27+
'mouseup',
28+
] as const
29+
30+
const featureEvents = [
31+
'addfeature',
32+
'removefeature',
33+
'setgeometry',
34+
'setproperty',
35+
'removeproperty',
36+
] as const
37+
38+
function loadGeoJson(src: string | object, layer: google.maps.Data) {
39+
if (typeof src === 'string')
40+
layer.loadGeoJson(src)
41+
else
42+
layer.addGeoJson(src)
43+
}
44+
45+
const dataLayer = useGoogleMapsResource<google.maps.Data>({
46+
create({ mapsApi, map }) {
47+
const layer = new mapsApi.Data({ map })
48+
49+
if (props.style)
50+
layer.setStyle(props.style)
51+
52+
loadGeoJson(props.src, layer)
53+
setupEventListeners(layer)
54+
55+
return layer
56+
},
57+
cleanup(layer, { mapsApi }) {
58+
mapsApi.event.clearInstanceListeners(layer)
59+
layer.setMap(null)
60+
},
61+
})
62+
63+
watch(() => props.src, (src) => {
64+
if (!dataLayer.value)
65+
return
66+
dataLayer.value.forEach(feature => dataLayer.value!.remove(feature))
67+
loadGeoJson(src, dataLayer.value)
68+
})
69+
70+
watch(() => props.style, (style) => {
71+
if (dataLayer.value)
72+
dataLayer.value.setStyle(style ?? {})
73+
}, { deep: true })
74+
75+
function setupEventListeners(layer: google.maps.Data) {
76+
dataEvents.forEach((event) => {
77+
layer.addListener(event, (payload: google.maps.Data.MouseEvent) => emit(event, payload))
78+
})
79+
featureEvents.forEach((event) => {
80+
layer.addListener(event, (payload: any) => (emit as any)(event, payload))
81+
})
82+
}
83+
</script>
84+
85+
<template>
86+
</template>

test/e2e/base.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ describe('base', async () => {
2020
await page.waitForTimeout(500)
2121
// get content of #script-src
2222
const text = await page.$eval('#script-src', el => el.textContent)
23-
expect(text).toMatchInlineSnapshot(`"/foo/_scripts/assets/6bEy8slcRmYcRT4E2QbQZ1CMyWw9PpHA7L87BtvSs2U.js"`)
23+
expect(text).toMatchInlineSnapshot(`"/foo/_scripts/assets/PHzhM8DFXcXVSSJF110cyV3pjg9cp8oWv_f4Dk2ax1w.js"`)
2424
})
2525
})

test/unit/__helpers__/google-maps-test-utils.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,37 @@ export async function simulateMarkerClustererLifecycle(mocks: MocksType, options
134134
return { clusterer, markers }
135135
}
136136

137+
/**
138+
* Simulates the component lifecycle for GeoJson data layers
139+
*/
140+
export function simulateGeoJsonLifecycle(mocks: MocksType, geoJson: string | object, style?: any) {
141+
// Creation
142+
const dataLayer = new mocks.mockMapsApi.Data({ map: ref({}).value })
143+
144+
// Style setup
145+
if (style) {
146+
dataLayer.setStyle(style)
147+
}
148+
149+
// GeoJson loading
150+
if (typeof geoJson === 'string') {
151+
dataLayer.loadGeoJson(geoJson)
152+
}
153+
else {
154+
dataLayer.addGeoJson(geoJson)
155+
}
156+
157+
// Event listener setup
158+
dataLayer.addListener('click', vi.fn())
159+
dataLayer.addListener('addfeature', vi.fn())
160+
161+
// Cleanup
162+
mocks.mockMapsApi.event.clearInstanceListeners(dataLayer)
163+
dataLayer.setMap(null)
164+
165+
return dataLayer
166+
}
167+
137168
/**
138169
* Test options for various Google Maps objects
139170
*/
@@ -159,4 +190,14 @@ export const TEST_OPTIONS = {
159190
scale: 1.5,
160191
background: '#FF0000',
161192
},
193+
geoJson: {
194+
type: 'FeatureCollection',
195+
features: [
196+
{
197+
type: 'Feature',
198+
geometry: { type: 'Point', coordinates: [151.2093, -33.8688] },
199+
properties: { name: 'Sydney' },
200+
},
201+
],
202+
},
162203
} as const

0 commit comments

Comments
 (0)