Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 73 additions & 1 deletion docs/content/scripts/google-maps.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,7 @@ All Google Maps SFC components must work within a `<ScriptGoogleMaps>`{lang="htm
- `<ScriptGoogleMapsPolyline>`{lang="html"} - Line paths
- `<ScriptGoogleMapsRectangle>`{lang="html"} - Rectangular overlays
- `<ScriptGoogleMapsHeatmapLayer>`{lang="html"} - Heatmap visualization
- `<ScriptGoogleMapsGeoJson>`{lang="html"} - GeoJSON data layers

### Basic Usage

Expand Down Expand Up @@ -521,6 +522,48 @@ onMounted(() => {
</template>
```

**GeoJSON Data Layer**

Load GeoJSON from a URL or inline object and apply custom styling:

```vue
<script setup lang="ts">
const geoJsonStyle = {
fillColor: '#4285F4',
fillOpacity: 0.4,
strokeColor: '#4285F4',
strokeWeight: 2,
}
function handleFeatureClick(event: google.maps.Data.MouseEvent) {
console.log('Clicked feature:', event.feature.getProperty('name'))
}
</script>
<template>
<ScriptGoogleMaps api-key="your-api-key">
<!-- Load from URL -->
<ScriptGoogleMapsGeoJson
src="https://example.com/data.geojson"
:style="geoJsonStyle"
@click="handleFeatureClick"
/>
<!-- Or pass inline GeoJSON -->
<ScriptGoogleMapsGeoJson
:src="{
type: 'FeatureCollection',
features: [{
type: 'Feature',
geometry: { type: 'Point', coordinates: [150.644, -34.397] },
properties: { name: 'My Point' },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could (but we don't need to) repeat :style and @click attrs for explicitness.

}],
}"
/>
</ScriptGoogleMaps>
</template>
```

### Component Hierarchy

```text
Expand All @@ -531,10 +574,11 @@ ScriptGoogleMaps (root)
β”œβ”€β”€ ScriptGoogleMapsAdvancedMarkerElement
β”‚ β”œβ”€β”€ ScriptGoogleMapsPinElement (optional)
β”‚ └── ScriptGoogleMapsInfoWindow (optional)
β”œβ”€β”€ ScriptGoogleMapsGeoJson (GeoJSON data layer)
└── ScriptGoogleMapsCircle / Polygon / Polyline / Rectangle / HeatmapLayer
```

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.
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.

### Component Reference

Expand All @@ -550,6 +594,34 @@ All SFC components accept an `options` prop matching their Google Maps API optio
| `ScriptGoogleMapsPolyline` | `google.maps.PolylineOptions` | |
| `ScriptGoogleMapsRectangle` | `google.maps.RectangleOptions` | |
| `ScriptGoogleMapsHeatmapLayer` | `google.maps.visualization.HeatmapLayerOptions` | |
| `ScriptGoogleMapsGeoJson` | `src`: `string \| object`, `style`: `google.maps.Data.StylingFunction \| google.maps.Data.StyleOptions` | Emits mouse & feature events |

### `ScriptGoogleMapsGeoJson`{lang="html"}

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).

#### Props

| Prop | Type | Description |
|---|---|---|
| `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. |
| `style` | `google.maps.Data.StylingFunction \| google.maps.Data.StyleOptions` | Styling applied to the data layer. Reactive with deep watching. |

#### Events

**Mouse events**: emitted with a `google.maps.Data.MouseEvent` payload:

`click`, `contextmenu`, `dblclick`, `mousedown`, `mousemove`, `mouseout`, `mouseover`, `mouseup`

**Feature lifecycle events:**

| Event | Payload |
|---|---|
| `addfeature` | `google.maps.Data.AddFeatureEvent` |
| `removefeature` | `google.maps.Data.RemoveFeatureEvent` |
| `setgeometry` | `google.maps.Data.SetGeometryEvent` |
| `setproperty` | `google.maps.Data.SetPropertyEvent` |
| `removeproperty` | `google.maps.Data.RemovePropertyEvent` |

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

Expand Down
40 changes: 40 additions & 0 deletions playground/pages/third-parties/google-maps/sfcs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,28 @@ const heatmapLayerData = ref<google.maps.LatLng[]>([])
const isCircleShown = ref(false)
const isGeoJsonShown = ref(false)
const geoJsonData = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [[
[151.20, -33.87],
[151.25, -33.87],
[151.25, -33.90],
[151.20, -33.90],
[151.20, -33.87],
]],
},
properties: { name: 'Sydney CBD' },
},
],
}
const googleMapsRef = useTemplateRef('googleMapsRef')
whenever(() => googleMapsRef.value?.googleMaps, (googleMaps) => {
Expand Down Expand Up @@ -191,6 +213,17 @@ whenever(() => googleMapsRef.value?.googleMaps, (googleMaps) => {
}"
/>

<ScriptGoogleMapsGeoJson
v-if="isGeoJsonShown"
:src="geoJsonData"
:style="{
fillColor: '#4285F4',
fillOpacity: 0.3,
strokeColor: '#4285F4',
strokeWeight: 2,
}"
/>

<ScriptGoogleMapsCircle
v-if="isCircleShown"
:options="{
Expand Down Expand Up @@ -269,6 +302,13 @@ whenever(() => googleMapsRef.value?.googleMaps, (googleMaps) => {
{{ `${isHeatmapLayerShown ? 'Hide' : 'Show'} heatmap layer` }}
</button>

<button
class="bg-[#ffa500] rounded-lg px-2 py-1"
@click="isGeoJsonShown = !isGeoJsonShown"
>
{{ `${isGeoJsonShown ? 'Hide' : 'Show'} geojson` }}
</button>

<button
class="bg-[#ffa500] rounded-lg px-2 py-1"
@click="isCircleShown = !isCircleShown"
Expand Down
86 changes: 86 additions & 0 deletions src/runtime/components/GoogleMaps/ScriptGoogleMapsGeoJson.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<script setup lang="ts">
import { watch } from 'vue'
import { useGoogleMapsResource } from './useGoogleMapsResource'
const props = defineProps<{
src: string | object
style?: google.maps.Data.StylingFunction | google.maps.Data.StyleOptions
}>()
const emit = defineEmits<{
(event: typeof dataEvents[number], payload: google.maps.Data.MouseEvent): void
(event: 'addfeature', payload: google.maps.Data.AddFeatureEvent): void
(event: 'removefeature', payload: google.maps.Data.RemoveFeatureEvent): void
(event: 'setgeometry', payload: google.maps.Data.SetGeometryEvent): void
(event: 'setproperty', payload: google.maps.Data.SetPropertyEvent): void
(event: 'removeproperty', payload: google.maps.Data.RemovePropertyEvent): void
}>()
const dataEvents = [
'click',
'contextmenu',
'dblclick',
'mousedown',
'mousemove',
'mouseout',
'mouseover',
'mouseup',
] as const
const featureEvents = [
'addfeature',
'removefeature',
'setgeometry',
'setproperty',
'removeproperty',
] as const
function loadGeoJson(src: string | object, layer: google.maps.Data) {
if (typeof src === 'string')
layer.loadGeoJson(src)
else
layer.addGeoJson(src)
}
const dataLayer = useGoogleMapsResource<google.maps.Data>({
create({ mapsApi, map }) {
const layer = new mapsApi.Data({ map })
if (props.style)
layer.setStyle(props.style)
loadGeoJson(props.src, layer)
setupEventListeners(layer)
return layer
},
cleanup(layer, { mapsApi }) {
mapsApi.event.clearInstanceListeners(layer)
layer.setMap(null)
},
})
watch(() => props.src, (src) => {
if (!dataLayer.value)
return
dataLayer.value.forEach(feature => dataLayer.value!.remove(feature))
loadGeoJson(src, dataLayer.value)
})
watch(() => props.style, (style) => {
if (dataLayer.value)
dataLayer.value.setStyle(style ?? {})
}, { deep: true })
function setupEventListeners(layer: google.maps.Data) {
dataEvents.forEach((event) => {
layer.addListener(event, (payload: google.maps.Data.MouseEvent) => emit(event, payload))
})
featureEvents.forEach((event) => {
layer.addListener(event, (payload: any) => (emit as any)(event, payload))
})
}
</script>

<template>
</template>
2 changes: 1 addition & 1 deletion test/e2e/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ describe('base', async () => {
await page.waitForTimeout(500)
// get content of #script-src
const text = await page.$eval('#script-src', el => el.textContent)
expect(text).toMatchInlineSnapshot(`"/foo/_scripts/assets/6bEy8slcRmYcRT4E2QbQZ1CMyWw9PpHA7L87BtvSs2U.js"`)
expect(text).toMatchInlineSnapshot(`"/foo/_scripts/assets/PHzhM8DFXcXVSSJF110cyV3pjg9cp8oWv_f4Dk2ax1w.js"`)
})
})
41 changes: 41 additions & 0 deletions test/unit/__helpers__/google-maps-test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,37 @@ export async function simulateMarkerClustererLifecycle(mocks: MocksType, options
return { clusterer, markers }
}

/**
* Simulates the component lifecycle for GeoJson data layers
*/
export function simulateGeoJsonLifecycle(mocks: MocksType, geoJson: string | object, style?: any) {
// Creation
const dataLayer = new mocks.mockMapsApi.Data({ map: ref({}).value })

// Style setup
if (style) {
dataLayer.setStyle(style)
}

// GeoJson loading
if (typeof geoJson === 'string') {
dataLayer.loadGeoJson(geoJson)
}
else {
dataLayer.addGeoJson(geoJson)
}

// Event listener setup
dataLayer.addListener('click', vi.fn())
dataLayer.addListener('addfeature', vi.fn())

// Cleanup
mocks.mockMapsApi.event.clearInstanceListeners(dataLayer)
dataLayer.setMap(null)

return dataLayer
}

/**
* Test options for various Google Maps objects
*/
Expand All @@ -159,4 +190,14 @@ export const TEST_OPTIONS = {
scale: 1.5,
background: '#FF0000',
},
geoJson: {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: { type: 'Point', coordinates: [151.2093, -33.8688] },
properties: { name: 'Sydney' },
},
],
},
} as const
Loading
Loading