Custom component mapbox model update issue

Hello there,

I have been having fun building an app. I have decided to use a custom mapbox to allow a draggable point and boundary to update a row in a table. I have butchered together some code from samples on the retool and mapbox forums.

Currently the map loads the point from the model data. I cannot seem to update the model data on a mapbox event handler.

Model data:

{  boundarydata: {},latitude:{{projectQuery.data.latitude[0]}},longitude:{{projectQuery.data.longitude[0]}} }

iframe code:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Extract drawn polygon area</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<link href="https://api.mapbox.com/mapbox-gl-js/v2.1.1/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v2.1.1/mapbox-gl.js"></script>
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>


<script src="https://api.tiles.mapbox.com/mapbox.js/plugins/turf/v3.0.11/turf.min.js"></script>
<script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-draw/v1.2.1/mapbox-gl-draw.js"></script>
<link rel="stylesheet" href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-draw/v1.2.1/mapbox-gl-draw.css" type="text/css">
<div id="map"></div>
<div class="calculation-box">
    <p>Draw a polygon using the draw tools.</p>
    <div id="calculated-area"></div>
</div>

<script>


mapboxgl.accessToken = 'tokencode';

var draw = new MapboxDraw({
        displayControlsDefault: false,
        controls: {
            polygon: true,
            trash: true
        }
    });
var marker1 = new mapboxgl.Marker({
    draggable: true,
    color: "red"
});

function updateData(e) {
  var data = draw.getAll();
  var lat = marker1._lngLat.lat;
  var lng = marker1._lgnLat.lng;
  window.Retool.modelUpdate({ boundarydata:data, latitude:lat, longitude:lng });
}
                        
var map = new mapboxgl.Map({
  container: 'map', // container id
  style: 'mapbox://styles/mapbox/dark-v10', //hosted style id
  zoom: 15 // starting zoom
  });

    
map.addControl(draw);
map.on('draw.create', updateData);
map.on('draw.delete', updateData);
map.on('draw.update', updateData);
map.on('marker1.update', updateData);


window.Retool.subscribe(function(model) {
              if (!model) {
                return;
              }
    map.setCenter([parseFloat(model.longitude),parseFloat(model.latitude)]);
  
    marker1.setLngLat([parseFloat(model.longitude),parseFloat(model.latitude)])
    marker1.addTo(map);
});



</script>

</body>
</html>

If I create a button to run a query to update the table the new values are not inserted or updated. If I try and create an action to update a text box similarly the new model data is not updated when accessed from the component e.g. {{customMapbox2.model.latitude}}.

Also the issue is that the boundary polygon wont finish on doubleclick like the tutorial.

I used the javascript implemntation window/model mentioned here.

Any help or ideas would be welcome.

Had a go. Marker needs a seperate marker1.on dragend event, I added its own function as well. See mapbox tutorial.

Updated code:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Extract drawn polygon area</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<link href="https://api.mapbox.com/mapbox-gl-js/v2.1.1/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v2.1.1/mapbox-gl.js"></script>
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>


<script src="https://api.tiles.mapbox.com/mapbox.js/plugins/turf/v3.0.11/turf.min.js"></script>
<script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-draw/v1.2.1/mapbox-gl-draw.js"></script>
<link rel="stylesheet" href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-draw/v1.2.1/mapbox-gl-draw.css" type="text/css">
<style>
.coordinates {
background: rgba(0, 0, 0, 0.5);
color: #fff;
position: absolute;
bottom: 20px;
left: 10px;
padding: 5px 10px;
margin: 0;
font-size: 11px;
line-height: 18px;
border-radius: 3px;
display: none;
}
</style>
 
<div id="map"></div>
<pre id="coordinates" class="coordinates"></pre>

<script>


mapboxgl.accessToken = 'token';

var draw = new MapboxDraw({
        displayControlsDefault: false,
        controls: {
            polygon: true,
            trash: true
        }
    });
var marker1 = new mapboxgl.Marker({
    draggable: true,
    color: "red"
});

function updateData() {
  var dataPts = draw.getAll();
  window.Retool.modelUpdate({boundarydata:dataPts});    
}

function onDragEnd() {
  const lngLat = marker1.getLngLat();
  coordinates.style.display = 'block';
  coordinates.innerHTML = `Longitude: ${lngLat.lng}<br />Latitude: ${lngLat.lat}`;
  window.Retool.modelUpdate({latitude:lngLat.lat,longitude:lngLat.lng});
}
 
                        
var map = new mapboxgl.Map({
  container: 'map', // container id
  style: 'mapbox://styles/mapbox/dark-v10', //hosted style id
  zoom: 15 // starting zoom
  });

  
map.addControl(draw);
map.on('draw.create', updateData);
map.on('draw.delete', updateData);
map.on('draw.update', updateData);



window.Retool.subscribe(function(model) {
              if (!model) {
                return;
              }
    map.setCenter([parseFloat(model.longitude),parseFloat(model.latitude)]);
  
    marker1.setLngLat([parseFloat(model.longitude),parseFloat(model.latitude)])
    marker1.addTo(map);
  
     
});

marker1.on('dragend', onDragEnd);

</script>

</body>
</html>

Once I have got the draw boundary to load presaved points I will update.

Hi @ivthecat Thanks for sharing this! I imported it into my own app, and I see that the model gets updated correctly now. It also seems like the boundary polygon does finish on double click now.

I'm not sure if I'm understanding what you're working on with the draw boundary. Are you looking to start with a certain polygon on load? Keep me posted!

Hello Tess,

I will get back onto working with the presaved points. Its a string that is then split into a list of points when the page is loaded.

@ivthecat Keep me posted on how I can help! :slightly_smiling_face:

Hello I have been working on the custom iframe map. MapBox has poor quality satellite underlays at the level I require so I resorted to Google Maps API.

Its working with a movable marker and redline boundary. On load it will draw the marker and boundary if not null in the query otherwise a default zoom on a location without marker and boundary.

There is currently a bug in that if drawing a new boundary the existing one remains on the screen until it is reloaded.

I have tried numerous ways to add polygons to lists and then set old ones to null but to no avail.

Its quite hard to debug the custom comonent with no console.log. I can't seem to see it in the chrome inspection. If there is a tutorial that would be great.

The coordinates are from an SQL query and then passed through a javascript transformer returning [] or JSON.parse(stringCoords). The transformer.value is in the custom component model. The latitude and longitude are also part of the model.
Code below:

<!DOCTYPE html>
<html>
<body>
<div id="googleMap" style="width:100%;height:100%;"></div>
<script>

var zoomStart = 19;


var polyList = [];
projectPoly = null;

var polyOptions = { strokeColor: "red",
                    strokeWeight: 4,
                    fillOpacity: 0,
                    editable: true,
                    draggable: true
                };

function updateMarker(marker){
   google.maps.event.addListener(marker,'dragend',function() {
      window.Retool.modelUpdate({latitude: this.getPosition().lat(),
                                longitude: this.getPosition().lng()});  
    });
};

function updateBoundary(poly){
    window.Retool.modelUpdate({boundarydata:JSON.stringify(poly.getPath().getArray())});
    google.maps.event.addListener(poly.getPath(),'set_at',function() {
      window.Retool.modelUpdate({boundarydata:JSON.stringify({coords:poly.getPath().getArray()})});  
    });
    google.maps.event.addListener(poly.getPath(),'insert_at',function() {
      window.Retool.modelUpdate({boundarydata:JSON.stringify({coords:poly.getPath().getArray()})});  
    });
    google.maps.event.addListener(poly.getPath(),'remove_at',function() {
      window.Retool.modelUpdate({boundarydata:JSON.stringify({coords:poly.getPath().getArray()})});  
    });
};

function myMap() {
  window.Retool.subscribe(function(model) {
                if (!model) {
                  return;
                }
      if (model.latitude!=null){
        lat = model.latitude;
        lng = model.longitude;
        showMarker = true;
        zoomLevel = 19;
      } else{
        lat = 51.3321548;
        lng = 1.4177604;
        showMarker = false;
        zoomLevel =15;
      }
      if (model.boundarydata!=null){
        polyCoords = model.boundarydata;
        polyShow = true;
      };
  });

var mapProp= {
  center:new google.maps.LatLng(lat,lng),
  zoom:zoomLevel,
  mapTypeId: google.maps.MapTypeId.SATELLITE,
  disableDefaultUI: true,
  zoomControl: true
};
var map = new google.maps.Map(document.getElementById("googleMap"),mapProp);

    drawingManager = new google.maps.drawing.DrawingManager({
          drawingControlOptions: {
            position: google.maps.ControlPosition.TOP_RIGHT,
            drawingModes: [
                  google.maps.drawing.OverlayType.MARKER,
                  google.maps.drawing.OverlayType.POLYGON]
          },
          markerOptions:polyOptions,
          polygonOptions:polyOptions
    });
    drawingManager.setMap(map);
    

  
  const projectMarker = new google.maps.Marker({position:{lat:lat,lng:lng},draggable:true});
 
  
  if (showMarker){
    projectMarker.setMap(map)
    updateMarker(projectMarker);
   
  };
  
  if (polyShow){
    const projectPolyOpts = structuredClone(polyOptions);
    projectPolyOpts.paths = polyCoords.coords;
    const projectPoly = new google.maps.Polygon(projectPolyOpts);
    projectPoly.setMap(map);
    //var infoWindow = new        google.maps.InfoWindow({content:JSON.stringify(polyCoords)}); // useful to debug.
    //infoWindow.open(map,projectMarker);
    updateBoundary(projectPoly);
    polyList.push("projectPoly"); //todo adding the google object to a list breaks the script. 
    }
  
  google.maps.event.addListener(drawingManager, 'overlaycomplete', function (e) {
                    if (e.type == google.maps.drawing.OverlayType.MARKER) {
                          google.maps.event.clearListeners(projectMarker,'dragend');
                          updateMarker(e.overlay);
                          projectMarker.setMap(null);
                          projectMarker = e.overlay;
                          projectMarker.setMap(map);
                          drawingManager.setDrawingMode(null);
                        }
                    if (e.type == google.maps.drawing.OverlayType.POLYGON) {
                          polyList.push("e.overlay"); //todo I was going to then set polyList[0] = null... to delete old polygon.
                          updateBoundary(e.overlay);
                          projectPoly.setMap(null);
                          projectPoly = e.overlay;
                          drawingManager.setDrawingMode(null);
                    };
                   
                    });

};



</script>

<script src="https://maps.googleapis.com/maps/api/js?key=yourGoogleMapsAPIkey&libraries=drawing&callback=myMap"></script>

</body>
</html>

Note you need a google maps API key from google.

An external button can then be added to save / update the query of the updated model information to the database.

Its a good idea for the main query to have a success trigger to reload the custom component if the page is used over multiple records.

Screenshot 2023-03-05 124207

Hi @ivthecat,

Thanks for the update!

Where are you attempting to console.log (and where)? It may depend on what data you're logging, but I see this console log in the Chrome dev tools (it doesn't show up in the Retool debugger)

I found an external post with a similar use case here. :crossed_fingers: Hope it helps: https://stackoverflow.com/questions/14166546/google-maps-drawing-manager-limit-to-1-polygon

Hello, sorry to bump but thought I would share what I have been looking at and rebuilding the google map using a the new custom react component using vis.gl and google maps examples via Retool.useStateString. The javascript map had lots of issues with updating the site boundary.

The component has 5 inputs.

The following typescript modules are required:
index.tsx:

import { APIProvider } from "@vis.gl/react-google-maps";
import Supermap from "./supermap";
import { Retool } from "@tryretool/custom-component-support";


export const SuperMapOut: React.FC = () => {
    const [apiKey] = Retool.useStateString({ name: "googlemapsapikey" });

    if (!apiKey) {
        console.error("Missing Google Maps API Key!");
        return <div style={{ color: "red" }}>Google Maps API Key is missing!</div>;
    }

    return (
        <APIProvider apiKey={apiKey} libraries={["marker", "places"]}>
            <Supermap/>
        </APIProvider>
    );
};

export default SuperMapOut;

polygon.tsx

import {
    forwardRef,
    useContext,
    useEffect,
    useImperativeHandle,
    useRef
} from 'react';

import { GoogleMapsContext } from '@vis.gl/react-google-maps';

import type { Ref } from 'react';

type PolygonEventProps = {
    onEdit?: (newPath: google.maps.LatLngLiteral[]) => void;
    onDelete?: () => void; // Handle external deletion
};

type PolygonCustomProps = {
    paths: google.maps.LatLngLiteral[] | null;
};

export type PolygonProps = google.maps.PolygonOptions & PolygonEventProps & PolygonCustomProps;

export type PolygonRef = Ref<google.maps.Polygon | null>;

function usePolygon(props: PolygonProps) {
    const { onEdit, onDelete, paths, ...polygonOptions } = props;
    const polygonRef = useRef<google.maps.Polygon | null>(null);

    const map = useContext(GoogleMapsContext)?.map;

    useEffect(() => {
        if (!map) {
            console.error('<Polygon> must be inside a Map component.');
            return;
        }

        if (polygonRef.current) {
            polygonRef.current.setMap(null); // Remove old polygon before creating a new one
        }

        if (!paths) {
            if (polygonRef.current) {
                polygonRef.current.setMap(null);
                polygonRef.current = null;
            }
            return;
        }

        polygonRef.current = new google.maps.Polygon({
            ...polygonOptions,
            paths: paths || [],
            editable: true, // Ensure polygon is editable
            draggable: true
        });

        polygonRef.current.setMap(map);

        const path = polygonRef.current.getPath();
        if (path) {
            const updatePolygon = () => {
                if (onEdit && polygonRef.current) {
                    const newPath = polygonRef.current.getPath().getArray().map(latlng => latlng.toJSON());
                    onEdit(newPath);
                }
            };

            google.maps.event.addListener(path, 'set_at', updatePolygon);
            google.maps.event.addListener(path, 'insert_at', updatePolygon);
            google.maps.event.addListener(path, 'remove_at', updatePolygon);
        }

        return () => {
            if (polygonRef.current) {
                polygonRef.current.setMap(null);
            }
        };
    }, [map, paths, polygonOptions, onEdit]);

    return polygonRef.current;
}

export const Polygon = forwardRef((props: PolygonProps, ref: PolygonRef) => {
    const polygon = usePolygon(props);

    useImperativeHandle(ref, () => polygon, []);

    return null;
});

use-drawing-manager.tsx

import { useMap, useMapsLibrary } from '@vis.gl/react-google-maps';
import { useEffect, useState } from 'react';

export function useDrawingManager(
    initialValue: google.maps.drawing.DrawingManager | null = null,
    onMarkerPlaced?: (position: google.maps.LatLngLiteral) => void,
    onPolygonDrawn?: (path: google.maps.LatLngLiteral[]) => void,
    isMarkerToolDisabled?: boolean,
    isPolygonToolDisabled?: boolean
) {
  const map = useMap();
  const drawing = useMapsLibrary('drawing');

  const [drawingManager, setDrawingManager] = useState<google.maps.drawing.DrawingManager | null>(initialValue);
  const [polygonExists, setPolygonExists] = useState(false); // Track polygon presence

  useEffect(() => {
    if (!map || !drawing) return;

    const newDrawingManager = new drawing.DrawingManager({
      map,
      drawingMode: null, // Start with no active tool
      drawingControl: true,
      drawingControlOptions: {
        position: google.maps.ControlPosition.TOP_CENTER,
        drawingModes: [
          ...(isMarkerToolDisabled ? [] : [google.maps.drawing.OverlayType.MARKER]),
          ...(isPolygonToolDisabled ? [] : [google.maps.drawing.OverlayType.POLYGON])
        ],
      },
      markerOptions: {
        draggable: true,
      },
    });

    google.maps.event.addListener(newDrawingManager, 'overlaycomplete', (event: google.maps.drawing.OverlayCompleteEvent) => {
      if (event.type === google.maps.drawing.OverlayType.MARKER) {
        if (isMarkerToolDisabled) return; // Prevent multiple markers

        const marker = event.overlay as google.maps.Marker;
        const position = marker.getPosition()?.toJSON();

        if (position) {
          onMarkerPlaced?.(position);
        }

        event.overlay.setMap(null); // Remove overlay (we use AdvancedMarker)
      }

      if (event.type === google.maps.drawing.OverlayType.POLYGON) {
        if (isPolygonToolDisabled) return; // Prevent multiple polygons

        const polygon = event.overlay as google.maps.Polygon;
        const path = polygon.getPath().getArray().map(latlng => latlng.toJSON());

        onPolygonDrawn?.(path);
        setPolygonExists(true);

        polygon.setMap(null); // Remove overlay (we use our own Polygon component)
      }
    });

    setDrawingManager(newDrawingManager);

    return () => {
      newDrawingManager.setMap(null);
    };
  }, [drawing, map, isMarkerToolDisabled, isPolygonToolDisabled]);

  // Function to reset the polygon state when deleted
  const resetPolygon = () => {
    setPolygonExists(false);
  };

  return { drawingManager, polygonExists, setPolygonExists, resetPolygon };
}

supermap.tsx

import React, { useEffect, useState } from 'react';
import { ControlPosition, Map, MapControl, AdvancedMarker, useMapsLibrary, useMap } from '@vis.gl/react-google-maps';
import { useDrawingManager } from './use-drawing-manager';
import { Polygon } from './polygon';
import { Retool } from "@tryretool/custom-component-support";

type LatLng = {
    lat: number;
    lng: number;
};


const Supermap = () => {
    const mapRef = useMap();
    const [mapIdStr] =  Retool.useStateString({name:"mapidstr"});
    const [latLng, setLatLng] = Retool.useStateString({name:"defaultlatlng",description:"Use a google latLng JSON e.g.: { lat: 51.332142693333985, lng: 1.4177853449504818 }"});
    //const [latLng, setLatLng] = useState<LatLng>({ lat: 51.332142693333985, lng: 1.4177853449504818 });
    const [markerPosition, setMarkerPosition] = Retool.useStateString({name: "markerposition", initialValue: '{ "lat": 51.332142693333985, "lng": 1.4177853449504818 }'});
    const [polygonCoords, setPolygonPath] = Retool.useStateString({name: "polygoncoords"});
    const [isPolygonToolDisabled, setIsPolygonToolDisabled] = useState(false);
    const [isHover, setIsHover] = useState(false);
    const [zoom, setZoom] = useState(19); // not used yet but will possible save the zoom state to the DB.
    const handleMouseEnter = () => {
        setIsHover(true);
    };
    const handleMouseLeave = () => {
        setIsHover(false);
    };
    // Parse marker position and set to null if lat:0, lng:0
    const parsedMarkerPosition = (() => {
        try {
            if (!markerPosition || markerPosition === "null") return null;
            const parsed = JSON.parse(markerPosition);
            if (typeof parsed === "object" && parsed !== null && "lat" in parsed && "lng" in parsed) {
                if (parsed.lat !== 0 || parsed.lng !== 0) {
                    mapRef?.setCenter(parsed); // Set map center only if lat/lng are valid
                }
            }

            // Ensure it's a valid object with latitude & longitude
            if (typeof parsed !== "object" || parsed === null || !("lat" in parsed) || !("lng" in parsed)) {
                return null;
            }

            return parsed.lat === 0 && parsed.lng === 0 ? null : parsed;
        } catch (error) {
            console.error("Error parsing markerPosition:", error);
            return null;
        }
    })();


    // Parse polygon coordinates and set to null if empty
    const parsedPolygonCoords = (() => {
        try {
            const parsed = JSON.parse(polygonCoords);
            return Array.isArray(parsed) && parsed.length === 0 ? null : parsed;
        } catch (error) {
            return null;
        }
    })();
    const calculatePolygonArea = (coords: string | null) => {
        if (!coords) return '0'; // Handle null or empty state safely

        let parsedCoords: google.maps.LatLngLiteral[];

        try {
            parsedCoords = JSON.parse(coords) as google.maps.LatLngLiteral[];
        } catch (error) {
            console.error('Invalid polygonCoords JSON:', error);
            return '0'; // Return 0 if parsing fails
        }

        if (parsedCoords.length < 3) return '0'; // A polygon must have at least 3 points

        let area = 0;
        const radius = 6378137; // Earth's radius in meters
        for (let i = 0; i < parsedCoords.length; i++) {
            const j = (i + 1) % parsedCoords.length;
            const lat1 = (parsedCoords[i].lat * Math.PI) / 180;
            const lng1 = (parsedCoords[i].lng * Math.PI) / 180;
            const lat2 = (parsedCoords[j].lat * Math.PI) / 180;
            const lng2 = (parsedCoords[j].lng * Math.PI) / 180;
            area += (lng2 - lng1) * (2 + Math.sin(lat1) + Math.sin(lat2));
        }

        area = Math.abs((area * radius * radius) / 2);
        return area.toFixed(2); // Keep the area formatted as a string
    };


    const { drawingManager, polygonExists, setPolygonExists, resetPolygon } = useDrawingManager(null, (position) => {
        setMarkerPosition("");
        setTimeout(() => {
            setMarkerPosition(JSON.stringify(position));
        }, 0);
    }, (path) => {
        setPolygonPath(JSON.stringify(path));
        setIsPolygonToolDisabled(true);
        setPolygonExists(true);
    }, false, isPolygonToolDisabled);

    const buttonStyle = { width: '27px', height: '27px',
        border: '1px solid rgba(0, 0, 0, 0.1)', borderRadius: '2px', display: 'flex',
        alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
        marginTop: '5px', marginLeft: '-5px',
        backgroundColor: isHover ? 'rgb(235, 235, 235)':'white'}

    return (
        <>

            <Map
                defaultZoom={zoom}
                defaultCenter={parsedMarkerPosition !== null ? parsedMarkerPosition : latLng}
                gestureHandling={'greedy'}
                disableDefaultUI={true}
                mapId={mapIdStr}
                mapTypeId={'hybrid'}
            >
                <MapControl position={ControlPosition.TOP_CENTER}>
                    <button style={buttonStyle} onClick={() => {
                        setPolygonPath("");
                        setPolygonExists(false);
                        resetPolygon();
                    }} onMouseEnter={handleMouseEnter}
                            onMouseLeave={handleMouseLeave}
                            disabled={!parsedPolygonCoords} title='Delete current boundary'>
                        <svg width='36' height='36' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
                            <path d='M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6h16zM10 11v6m4-6v6' stroke='currentColor' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round'/>
                        </svg>
                    </button>
                </MapControl>
                {parsedMarkerPosition && (
                    <AdvancedMarker position={parsedMarkerPosition} draggable={true} onDragEnd={(event) => setMarkerPosition(JSON.stringify(event?.latLng?.toJSON() || null))} />
                )}
                <Polygon fillOpacity={0} strokeColor={'#FF0000'} editable={true} draggable={true} onEdit={(newPath) => setPolygonPath(JSON.stringify(newPath))} strokeWeight={6} paths={parsedPolygonCoords} />
            </Map>


            <div>
                <h3>Site information</h3>
                <textarea
                    readOnly
                    value={`Marker Position: ${markerPosition}
Polygon Path: ${JSON.stringify(polygonCoords)}
Site Area: ${polygonCoords ? calculatePolygonArea(polygonCoords) : '0'} m²`}
                    rows={6}
                    style={{ width: '100%' }}
                />
            </div>
        </>
    );
};

export default Supermap;

Use the standard way of importing the module from your editor.

Currently the map uses the database based on suitecrm sql. There is a default location starting point you can edit. There is also a text box at the bottom with lat lng coordinates and site area. These could also be hooked up to the Retool.useStateString.

You need a google maps api key as well as the new mapId which sets certain settings that can be overridden. The map also uses the new AdvancedMarker as the old Marker has been deprecated.

2 Likes