# Advanced Google Map Widget

> Embedded Google Map with 8 map types (basic, multiple marker, static, polyline, polygon, overlay, with routes, panorama) and 2 theme groups (Google standard 5 themes + Snazzy Maps 9 themes).

**Class file:** `includes/Elements/Google_Map.php` (1,823 lines)
**Slug:** `adv-google-map` (widget id `eael-adv-google-map`) ⚠ slug uses `adv-` not `advanced-`
**Public docs:** <https://essential-addons.com/elementor/docs/google-map/>
**Pro-shared:** Pro-only. **External integration: Google Maps JS API**.

## Overview

Embeds a Google Map driven by the Maps JavaScript API. User configures the API key once in EA settings (`eael_save_google_map_api` option), then can place multiple Google Map widgets on a page — each gets its own map type, markers, route, and theme. Themes come from `includes/Elements/advance-gmap-themes.php` data file (14 presets across 2 groups). Heaviest widget asset-wise — Maps JS is loaded once per page via `Enqueue::before_enqueue_scripts`, not via `config.php` deps.

## Pro vs Lite

Pro-only. No Lite counterpart.

## File Map

| File | Role |
| --- | --- |
| `includes/Elements/Google_Map.php` | Widget class — controls + render |
| `includes/Elements/advance-gmap-themes.php` | **Data file** — returns 14 JSON theme styles (gstandard: 5, snazzymaps: 9) |
| `src/css/view/adv-google-map.scss` → `assets/front-end/css/view/adv-google-map.min.css` | Layout / pin / info-window styling |
| `src/js/view/adv-google-map.js` → `assets/front-end/js/view/adv-google-map.min.js` | Map init using `google.maps.*` API |
| `assets/front-end/js/lib-view/gmap/gmap.min.js` | gmap utility wrapper (lib) |
| `includes/Traits/Enqueue.php` `before_enqueue_scripts()` | **Runtime enqueue** of `https://maps.googleapis.com/maps/api/js?key=<KEY>&callback=Function.prototype` |
| `config.php` entry `'adv-google-map'` | self CSS + lib JS (gmap) + self JS — but NOT the Maps JS itself |

## Architecture

- **API key is site-wide, not per-widget** — stored in WP option `eael_save_google_map_api` (set in EA settings page, not in widget controls). Pro reads it at runtime in `Enqueue::before_enqueue_scripts` and concatenates into the Maps JS URL.
- **Runtime enqueue gated on widget presence** — `Enqueue::before_enqueue_scripts` checks `in_array('adv-google-map', $widgets) && '' != get_option('eael_save_google_map_api')` before enqueuing Maps JS. No widget on page → no Maps JS. No API key set → no Maps JS (widget will fail silently in editor).
- **8 map types in one widget** — bar / line / area pattern; render branches on `eael_google_map_type` value. Each type has its own set of controls (markers for `marker`, route waypoints for `routes`, polygon coordinates for `polygon`, etc.).
- **Themes are data, not controls** — themes live in `advance-gmap-themes.php` and are loaded as JSON into the map options. Two theme groups: `gstandard` (Google's official 5: standard / silver / retro / dark / night / aubergine) and `snazzymaps` (9 community themes: simple / colorful / complex / dark / greyscale / light / monochrome / nolabels / twotone).
- **API key concatenated into URL with no `urlencode`** in `Enqueue.php:11`. API keys are alphanumeric so practical risk is low; tighten when touched.
- **`callback=Function.prototype` parameter** is a no-op callback — used so Maps JS doesn't throw, even though Pro's init runs through standard Elementor `frontend/element_ready` lifecycle instead of the callback.

## ACF Repeater Data Source

Markers can be populated from an **ACF Repeater field** instead of the manual `eael_google_map_markers` repeater. Controls live in the **Content → Data Source** section (`eael_register_acf_controls()`), reusing the shared helpers `Pro_Helper::eael_get_acf_repeater_options()` / `eael_get_acf_repeater_sub_fields()` / `eael_acf_notice_controls()` — the same pattern used by Testimonial Slider, Team Member Carousel, and Price Menu.

**Flow:**

1. `eael_gmap_acf_data_source` = `custom` (default) → manual markers. `acf_repeater` → ACF mode.
2. `eael_gmap_acf_repeater_field` selects which ACF repeater to read.
3. Per-repeater field-mapping selects map sub-fields to the marker shape: `lat`, `lng`, `title`, `content`, `icon` (image).
4. `eael_gmap_acf_extras_<repeater>` (SELECT2 multiple) — any *other* sub-fields (e.g. category, distance) appended to each marker's info window as `<ul class="eael-gmap-marker-extras"><li><strong>Label:</strong> value</li></ul>`.

**Server-side resolution — `get_map_markers( $settings )`:**

- Returns the native `eael_google_map_markers` value when source = `custom` (zero impact on existing custom maps — first-line early return).
- ACF mode: `get_field( $field_name, get_the_ID() ?: get_queried_object_id() )`, maps each row to the native marker array shape. Empty field / missing `get_field()` / non-array rows → `[]` (no fatal).
- Rows with empty lat **or** lng are skipped.
- **Image field handles all three ACF return formats** — array (`['url'=>, 'ID'=>]`), URL string, or numeric attachment ID (`wp_get_attachment_image_url()`).
- ACF marker rows include `icon_width`/`icon_height` (32) + `icon_color` defaults — **required**, because the JS `scaledSize: new google.maps.Size(width, height)` produces `Size(NaN, NaN)` and silently drops the marker if they're absent.

**Forced Multiple Marker + hidden General Settings:**

- When source = `acf_repeater`, the **General Settings section is hidden** (`condition` → `eael_gmap_acf_data_source = custom`) since per-type coordinate inputs are irrelevant for repeater markers.
- `render()` forces `$settings['eael_google_map_type'] = 'marker'` in ACF mode, so markers render regardless of the (hidden) map-type control's stored value. Switching back to `custom` restores the real map type — no data loss.

**Consumed in two places** (both via `get_map_markers()`): the `data-map_markers` attribute in `map_render_data_attributes()`, and the auto-center pluck in `get_polyline_center_point()` (which also guards against `max([])` on empty arrays).

> ACF markers only render for the **Multiple Marker** map type — that's why ACF mode force-sets it. The Basic map type uses a single address/coordinate marker and ignores the markers array (`adv-google-map.js` `if ("basic" == $mapType)` branch).

## Render Output

```html
<div class="eael-google-map-wrap">
  <div class="eael-google-map"
       id="eael-google-map-{element-id}"
       data-map-type="basic|marker|static|polyline|polygon|overlay|routes|panorama"
       data-map-options='{ ...JSON... }'
       data-map-zoom="14"
       data-map-lat="..."
       data-map-lng="..."
       data-map-theme="silver">
  </div>
</div>
```

The `data-map-options` payload is built server-side from settings, including the theme JSON pulled from `advance-gmap-themes.php`. JS reads the attributes and calls `new google.maps.Map(container, options)`.

## Controls Reference

| Control id | Tab → Section | Type | Purpose |
| --- | --- | --- | --- |
| `eael_gmap_acf_data_source` | Content → Data Source | SELECT | `custom` (manual markers) / `acf_repeater` (ACF) |
| `eael_gmap_acf_repeater_field` | Content → Data Source | SELECT | Which ACF repeater field to read (shown when source = acf_repeater) |
| `eael_gmap_acf_{lat,lng,title,content,icon}_<repeater>` | Content → Data Source | SELECT | Sub-field → marker mapping (per repeater) |
| `eael_gmap_acf_extras_<repeater>` | Content → Data Source | SELECT2 (multiple) | Extra sub-fields appended to the marker info window |
| `eael_google_map_type` | Content → Map | SELECT | `basic` / `marker` / `static` / `polyline` / `polygon` / `overlay` / `routes` / `panorama` (forced to `marker`, section hidden, in ACF mode) |
| `eael_google_map_address_type` | Content → Location | CHOOSE | `address` (string) / `coordinates` (lat,lng) |
| `eael_google_map_address` | Content → Location | TEXT | Address string (when type = address) |
| `eael_google_map_latitude` / `_longitude` | Content → Location | NUMBER | Coords (when type = coordinates) |
| `eael_google_map_zoom` | Content → Map | SLIDER | Initial zoom (1–22) |
| `eael_google_map_theme_group` | Style → Theme | SELECT | `gstandard` / `snazzymaps` |
| `eael_google_map_theme` | Style → Theme | SELECT | Theme within the selected group |
| `eael_google_map_markers` | Content → Markers | REPEATER | Marker rows (when type = marker) |
| `eael_google_map_route_origin` / `_destination` / `_waypoints` | Content → Route | various | Route configuration (when type = routes) |
| `eael_google_map_height` | Style → General | SLIDER | Map height (CSS) |

## Conditional Dependencies

```text
eael_google_map_type = 'marker'
  └── shows eael_google_map_markers repeater
  └── hides route / polygon / polyline / overlay controls

eael_google_map_type = 'routes'
  └── shows route origin, destination, waypoints, travel mode controls

eael_google_map_type = 'polygon' | 'polyline'
  └── shows coordinates repeater + fill/stroke style controls

eael_google_map_type = 'panorama'
  └── shows pano-specific heading/pitch controls

eael_google_map_address_type = 'coordinates'
  └── shows lat/lng inputs, hides address input

eael_gmap_acf_data_source = 'acf_repeater'
  └── shows repeater field selector + per-repeater field mapping + extras
  └── hides the entire General Settings section
  └── render() forces map type = 'marker'

eael_gmap_acf_data_source = 'custom' (default)
  └── hides all ACF mapping controls; manual markers + full General Settings
```

## JavaScript Lifecycle

`src/js/view/adv-google-map.js`:

```js
var GoogleMapHandler = function( $scope, $ ) {
    var $map = $scope.find( '.eael-google-map' );
    if ( ! $map.length ) return;
    if ( typeof google === 'undefined' || ! google.maps ) {
        console.warn( 'EA Google Map: Google Maps API not loaded. Check the API key.' );
        return;
    }

    var type = $map.data( 'map-type' );
    var options = JSON.parse( $map.data( 'map-options' ) );
    var map = new google.maps.Map( $map[0], options );

    // Type-specific init: add markers, draw route, etc.
};

jQuery( window ).on( 'elementor/frontend/init', function() {
    if ( eael.elementStatusCheck( 'eaelGoogleMapLoad' ) ) return false;
    elementorFrontend.hooks.addAction(
        'frontend/element_ready/eael-adv-google-map.default',
        GoogleMapHandler
    );
} );
```

## Hooks & Filters

### Elementor hooks consumed

Standard widget render. No extension-style hook registration.

### Pro / EA hooks emitted

None.

## Common Issues

| Symptom | Likely cause | Diagnose | Fix |
| --- | --- | --- | --- |
| Map shows "For development purposes only" overlay | Google API key not authorized for production / billing not enabled | Network: check the `maps/api/js` response | Enable billing on Google Cloud project; restrict API key to the site's referrer |
| Map area is blank | No API key set in EA settings | Verify `eael_save_google_map_api` option non-empty | Set the API key in EA settings |
| `google is not defined` console error | Maps JS not enqueued | DevTools Sources: `maps.googleapis.com` script present? | Verify EA settings has the API key AND the page actually has this widget |
| Multiple maps on a page break | Maps JS loaded once but multiple init calls race | Inspect console for `InvalidValueError` | Ensure `eael.elementStatusCheck` guard is in place — see `_patterns.md` |
| Theme not applying | Theme JSON in `advance-gmap-themes.php` is malformed | Check JSON validity | Theme strings are JSON-encoded inside PHP strings — quotes can escape badly when editing the data file |
| Editor preview shows map but blank on frontend | `is_edit_mode` branch in JS, OR Asset_Builder missing the page | Verify `Asset_Builder` detected this widget on the page | If widget is inside a template / popup / shortcode, see `asset-loading.md` § Asset_Builder detection limits |
| ACF Repeater selected but no markers appear | Map type was Basic (manual config) — Basic ignores the markers array | Inspect `data-map_type` — should be `marker` | Fixed in code: ACF mode force-sets `marker`. If editing render, keep that override |
| ACF markers drop when a custom Marker Icon is mapped | Marker array missing `icon_width`/`icon_height` → `google.maps.Size(NaN, NaN)` | Console: invalid icon size; markers absent | Ensure `get_map_markers()` sets `icon_width`/`icon_height`/`icon_color` defaults on each ACF marker |
| ACF custom icon not showing (no error) | ACF image field return format = ID, but resolver only handled array/URL | Check the mapped sub-field's return format | `get_map_markers()` resolves array / URL / numeric ID — keep all three branches |

## Known Limitations

- **API key is site-wide** — can't have per-page or per-environment keys via the widget
- **API key in URL on every page-load** — visible in HTML source. Google's referrer-restriction is the only mitigation
- **No `urlencode` on the API key concatenation** — tight when touched (API keys are alphanumeric so practical risk is low)
- **Maps JS is heavy (~120KB)** — every page with a Google Map widget pays the cost; no lazy-load
- **No fallback when Maps JS fails** — widget renders an empty div with no message to the visitor
- **`advance-gmap-themes.php` theme JSON is inline strings, not loaded from JSON files** — editing requires careful escape of inner quotes
- **No Snazzy Maps catalogue refresh** — themes are frozen at the version bundled in `advance-gmap-themes.php`. New Snazzy themes aren't auto-pulled.
- **Geocoding usage** for address-based maps is metered by Google — high-traffic sites with address-based maps may hit quota
- **ACF Repeater source is marker-only** — the feature force-sets the Multiple Marker type; other map types are not available while ACF is selected
- **ACF data reads the current object** — `get_field( $field, get_the_ID() ?: get_queried_object_id() )`. The ACF field group must target the page/post where the widget renders; in archive/loop contexts the queried object is used
- **ACF extras are flat scalar fields** — only scalar sub-fields render in the info window (`<ul class="eael-gmap-marker-extras">`); nested repeaters / arrays are skipped
- **ACF Pro required for the Repeater field type** — without ACF active, the Data Source section shows an install notice and ACF markers resolve to empty

## Cross-References

- Architecture: [`docs/architecture/external-api-integrations.md`](../architecture/external-api-integrations.md) — Maps JS / Snazzy data
- Architecture: [`docs/architecture/asset-loading.md`](../architecture/asset-loading.md) — runtime enqueue pattern
- Shared patterns: [`_patterns.md`](_patterns.md)
- Rule: [`.claude/rules/external-api-integrations.md`](../../.claude/rules/external-api-integrations.md)
- ACF Repeater source pattern shared with: [`testimonial-slider.md`](testimonial-slider.md), [`team-member-carousel.md`](team-member-carousel.md), [`price-menu.md`](price-menu.md) — helpers in `includes/Classes/Helper.php` (`eael_get_acf_repeater_options` / `eael_get_acf_repeater_sub_fields` / `eael_acf_notice_controls`)
