Add a custom map marker source for the Map content class

Metadata

Field Value
Article type Recipe (Developer)
Plugin slug mam-main
Applies to plugin version 2.1.11+
Category Extending MAM Suite
Audience PHP developer
Estimated time 30–60 minutes
Last verified 2026-07-20

Goal

Supply your own markers — each with a latitude, longitude, icon, and snippet — to a Map screen in a MAM app, instead of (or alongside) the built-in WP-category or GeoDirectory marker sources.

By the end you’ll have a small sibling plugin that injects markers into the mobile JSON payload, with the markers shaped the way the native map view expects, and you’ll know the two main ways markers go missing from a map.


How the Map class actually works

It helps to know what you’re extending before you write code.

The Map content class (local_app_map, source includes/content-classes/local-app-map-class.php) is a thin nav-button registrar. It declares a Map content type with vc_type map, and it is what an admin picks in Mobile App Manager → App Setup when they add a map button. Its get_data_for_app() is a pass-through and its content-source form returns No settings required. In other words, the class itself does not build markers.

Markers reach the native map view through the mobile JSON payload — the response from the local_app_get_phone_data AJAX action. Sibling plugins inject marker data into that payload by subscribing to the mam_get_phone_data_before_send filter. mam-geodirectory contributes its own listings/events to the map through this same phone-data pipeline (see content-classes/local-app-geodirectory-v2-class.php). The native map view (Apple Maps / Google Maps depending on platform and config) does the rendering; your job server-side is to produce correctly-shaped markers.

So “add a custom marker source” means: subscribe to mam_get_phone_data_before_send and inject markers in the frozen marker shape.

Note: the top-level key the native map view reads for a custom marker source is coordinated with the mobile client — it must match the key the app (or the Map button’s custom marker source) is configured to read. The examples below use a plugin-prefixed placeholder, my_plugin_map_markers; substitute the key your app build actually reads.


The marker shape (frozen)

Every marker is an object with this shape:

{
  "id": "post_123",
  "title": "Venue name",
  "lat": "40.7128",
  "lon": "-74.0060",
  "marker_icon": "https://example.com/icon.png",
  "snippet": "Short description shown in the callout"
}

Rules that are frozen — older mobile parsers break if you ignore them:

  • lat and lon are strings, not numbers. Emit "40.7128", never 40.7128. Older mobile parsers reject numeric coordinates.
  • id is a string even when it’s a numeric post id ("123", not 123).
  • marker_icon is a full URL to the icon image. If you omit it, the app falls back to its default marker.
  • snippet is the short text shown when a marker is tapped.

See Mobile JSON shape for the full set of payload conventions.


Prerequisites

  • A sibling plugin with its own slug (don’t add this to a theme’s functions.php for production).
  • mam-main active — the Map class and the phone-data pipeline live there.
  • A Map button already placed in the app via App Setup, so you have a screen to render onto.
  • Your marker data available server-side (a custom post type, an external dataset you’ve already cached locally, etc.).
  • Coordination with the mobile client team: the app must know which payload key to read your markers from.

Steps

1. Build your markers in the frozen shape

Produce an array of marker objects. Cast coordinates and ids to strings:

private function build_markers(): array {

    $markers = array();

    foreach ( $this->get_my_locations() as $loc ) {

        // Skip anything without real coordinates — see "exclude-from-map" gotchas.
        if ( empty( $loc['lat'] ) || empty( $loc['lon'] ) ) {
            continue;
        }

        $markers[] = array(
            'id'          => (string) $loc['id'],
            'title'       => $loc['name'],
            'lat'         => (string) $loc['lat'],   // string, not float
            'lon'         => (string) $loc['lon'],   // string, not float
            'marker_icon' => $loc['icon_url'],       // full URL, or '' for default
            'snippet'     => $loc['blurb'],
        );
    }

    return $markers;
}

2. Inject the markers via mam_get_phone_data_before_send

Register a subscriber at the default content-provider priority (10). Prefix your top-level key with your plugin slug so it can’t collide with another plugin’s key:

add_filter( 'mam_get_phone_data_before_send', function ( array $data_array ): array {

    // Replace 'my_plugin_map_markers' with the key the native map view reads.
    $data_array['my_plugin_map_markers'] = $this->build_markers();

    return $data_array;
}, 10 );

Priority 10 is the default cohort for content providers. Do not register at 1000 — that slot is reserved for home_cats. See Hook: mam_get_phone_data_before_send for the full priority table.

3. Respect anonymous vs. signed-in users

The filter fires for every request, including anonymous app launches. If your markers are user-scoped, gate on the current user instead of building for everyone:

add_filter( 'mam_get_phone_data_before_send', function ( array $data_array ): array {

    if ( ! mam_user_id() ) {
        return $data_array;            // anonymous — skip user-scoped markers
    }

    $data_array['my_plugin_map_markers'] = $this->build_markers();
    return $data_array;
}, 10 );

4. Cache if your marker build is expensive

This filter is on the hot path — it runs on every phone-data request, and a slow subscriber adds latency to every app launch. If build_markers() runs heavy queries or (worse) an HTTP call, wrap it in your own cache so unchanged data isn’t rebuilt on every request:

add_filter( 'mam_get_phone_data_before_send', function ( array $data_array ): array {

    $markers = get_transient( 'my_plugin_map_markers' );
    if ( false === $markers ) {
        $markers = $this->build_markers();                   // expensive: heavy query / HTTP
        set_transient( 'my_plugin_map_markers', $markers, HOUR_IN_SECONDS );
    }

    $data_array['my_plugin_map_markers'] = $markers;
    return $data_array;
}, 10 );

When your underlying location data changes, clear your cache and bump the global JSON cursor so clients know to refetch:

delete_transient( 'my_plugin_map_markers' );
JSON_Cursor_Manager::reset_cursor();

JSON_Cursor_Manager::get_cursor() returns a single global cache token (the mam_json_cursor option) and takes no per-user argument; reset_cursor() rolls it to a fresh value, which is what forces every client to pull a new payload.

Advanced: the mobile contract also allows a section-root null as a “no change since cursor” signal — distinct from a field-level null, which mam_replace_null_with_empty_string() converts during output shaping (so never put null inside an individual marker’s fields). Emitting a section-root null correctly depends on the platform’s cursor-cache internals; see Cursor cache mechanism before relying on it, and default to the transient cache above.

5. Verify the markers land in the payload

Snapshot the phone-data JSON before and after activating your plugin and diff it:

curl 'https://example.com/wp-admin/admin-ajax.php?action=local_app_get_phone_data' > before.json
# activate your plugin, then:
curl 'https://example.com/wp-admin/admin-ajax.php?action=local_app_get_phone_data' > after.json
diff before.json after.json

Confirm your key appears at the top level, that lat/lon are quoted strings, and that no marker has a null field. Then open the Map screen in the Previewer / a device build and confirm the pins render where you expect.


Exclude-from-map gotchas

Markers most often go missing for one of these reasons. Check them before assuming the injection itself failed.

  • Missing or empty coordinates. A marker with no lat/lon (or with null/"" after output shaping) can’t be placed and is effectively excluded. Filter these out in build_markers() (Step 1) rather than shipping empty coordinates.
  • lat/lon shipped as numbers. If a marker silently doesn’t appear, check that the coordinates are strings. Numeric coordinates are rejected by older parsers — the rest of the payload may still load, so the failure looks like “just that map is empty.”
  • The GeoDirectory events exclusion. If mam-geodirectory is active and also contributing to a map, the filter mam_gd_events_exclude_from_map (default false) controls whether gd_event posts appear as markers. A __return_true anywhere on the site removes events from every map request site-wide — there is no per-screen override through that hook alone. If your custom markers share a map with GeoDirectory events and the events vanish, look for this filter. (It does not touch your injected key — it only affects GeoDirectory’s own event markers.)
  • Default coordinates are global. The map’s fallback center (default_lat / default_lon) is a global setting, not per-button. If a map opens “in the ocean,” your markers may be fine but the initial center/zoom is pointing elsewhere.
  • You overwrote a key you don’t own. Two subscribers writing the same top-level key produce last-writer-wins. Keep your plugin-prefixed key and never write into another plugin’s marker key.

What’s next

  • Hook: mam_get_phone_data_before_send — the full signature, priority table, and caching contract for the extension point you used here.
  • Recipe: Inject custom data into the mobile JSON — the general-purpose version of Steps 2–5.
  • Content class: Map — the screen-reference for local_app_map, its settings, and the marker JSON shape.
  • Hook: mam_gd_events_exclude_from_map — details on the GeoDirectory events exclusion called out above.
  • Mobile JSON shape — the frozen payload conventions (string ids, yes/no flags, null handling).
Was this article helpful?
Contents

    Need Support?

    Can't find the answer you're looking for? Don't worry we're here to help!