geeViz Esri Integration Examples

Demonstrates geeViz.esriLib — searching any ArcGIS Portal, adding Esri Image Services, Feature Services, and Map Services alongside Google Earth Engine layers in the geeViz viewer.

Section

What it covers

1. Portal search

Search IIPP, AGOL, and custom portals for datasets

2. Service metadata

Inspect any Esri service endpoint

3. Image Service

Add an Esri ImageServer as XYZ tiles

4. Feature Service

Fetch and add vector GeoJSON

5. Combined view

Esri layers + Earth Engine layers together

6. Auto-dispatch

addEsriService for unknown service types

7. Secured portals

Token-based auth pattern

No API key required — IIPP and most agency portals are public. Token-gated examples show the pattern but are clearly marked.


Setup

import geeViz.esriLib as el
import geeViz.geeView as gv

Map = gv.Map
ee = gv.ee

print("Available portals:", list(el.PORTALS.keys()))
print("IIPP URL:", el.PORTALS['iipp'])


2. Service Metadata

Inspect any service endpoint — useful before adding it to the map.

# Pick the first Image Service result from the IIPP search above
image_results = [r for r in results if r['type'] == 'Image Service']

if image_results:
    svc_url = image_results[0]['url']
    print(f"Inspecting: {svc_url}")
    meta = el.getServiceMetadata(svc_url)
    print(f"  Name       : {meta.get('name')}")
    print(f"  Band count : {meta.get('bandCount')}")
    print(f"  Pixel type : {meta.get('pixelType')}")
    print(f"  Min scale  : {meta.get('minScale')}")
    print(f"  Max scale  : {meta.get('maxScale')}")
    extent = meta.get('extent', {})
    print(f"  Extent     : {extent}")
else:
    print("No Image Service results found — try a broader search query.")
# Inspect a public Feature Service (NIFC fire perimeters — always public)
nifc_url = ("https://services3.arcgis.com/T4QMspbfLg3qTGWY/arcgis/rest/services/"
            "WFIGS_Interagency_Perimeters/FeatureServer/0")
try:
    nifc_meta = el.getServiceMetadata(nifc_url)
    print(f"Service name : {nifc_meta.get('name')}")
    print(f"Geometry type: {nifc_meta.get('geometryType')}")
    fields = [f['name'] for f in nifc_meta.get('fields', [])[:8]]
    print(f"Fields (first 8): {fields}")
except ConnectionError as e:
    print(f"Network error: {e}")

3. Adding an Esri Image Service

Image Services are served as XYZ tiles via <service_url>/tile/{z}/{y}/{x}.
Note the ArcGIS tile order is {z}/{y}/{x} (y before x), not the XYZ standard — addEsriImageService handles this automatically.

Map.clearMap()

# ESRI World Imagery basemap (always public, always cached)
el.addEsriMapService(
    "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer",
    name="ESRI World Imagery",
    viz_params={"opacity": 0.7},
)

# Add a NAIP ImageServer if one was found in the search above
if image_results:
    el.addEsriImageService(
        image_results[0],   # pass the full search result dict
        name=image_results[0]['title'],
        viz_params={"opacity": 0.85},
    )

Map.centerObject(ee.Geometry.Point([-111.89, 40.77]), 10)
Map.view()

4. Adding an Esri Feature Service

Feature Services are fetched as GeoJSON and rendered as vector layers.

Always does a pre-flight count check before downloading features. If the count exceeds max_features, a ValueError is raised with remediation options (adjust where, raise max_features, or paginate).

Map.clearMap()

# NIFC interagency fire perimeters — filter to large recent fires
# so we stay well under max_features
nifc_url = ("https://services3.arcgis.com/T4QMspbfLg3qTGWY/arcgis/rest/services/"
            "WFIGS_Interagency_Perimeters/FeatureServer/0")

try:
    el.addEsriFeatureService(
        nifc_url,
        name="NIFC Fire Perimeters (>50k acres, 2020+)",
        where="attr_IncidentSize > 50000 AND attr_FireDiscoveryDateTime >= DATE '2020-01-01'",
        max_features=500,
        viz_params={
            "color": "FF4500",
            "opacity": 0.7,
            "strokeWidth": 2,
        },
    )
except ValueError as e:
    print(f"Feature count exceeded: {e}")
except ConnectionError as e:
    print(f"Network error: {e}")

Map.centerObject(ee.Geometry.BBox(-125, 31, -100, 50), 5)
Map.view()
# Demonstrate the overflow ValueError
print("Attempting to fetch all perimeters with max_features=10 (will fail on purpose)...")
try:
    el.addEsriFeatureService(
        nifc_url,
        max_features=10,
    )
except ValueError as e:
    print(f"ValueError raised as expected:\n{e}")

5. Combined View — Esri + Earth Engine

Esri layers and GEE layers can be combined freely. The viewer handles both tileMapService (Esri) and standard GEE serialized layers.

Map.clearMap()

# --- Earth Engine layer: MODIS burned area ---
burned = (
    ee.ImageCollection("MODIS/061/MCD64A1")
    .filterDate("2020-01-01", "2023-12-31")
    .select("BurnDate")
    .max()
    .selfMask()
)
Map.addLayer(
    burned,
    {"min": 1, "max": 366, "palette": ["yellow", "orange", "red", "darkred"]},
    "MODIS Burned Area 2020-2023",
    visible=True,
)

# --- Esri layer: ESRI World Imagery basemap ---
el.addEsriMapService(
    "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer",
    name="ESRI World Imagery (basemap)",
    viz_params={"opacity": 0.5},
)

# --- Esri Feature Service: fire perimeters as vector overlay ---
try:
    el.addEsriFeatureService(
        nifc_url,
        name="NIFC Perimeters (vector)",
        where="attr_IncidentSize > 100000 AND attr_FireDiscoveryDateTime >= DATE '2020-01-01'",
        max_features=200,
        viz_params={"color": "FF6600", "strokeWidth": 2, "opacity": 0.6},
    )
except (ValueError, ConnectionError) as e:
    print(f"Feature service unavailable: {e}")

Map.centerObject(ee.Geometry.BBox(-125, 31, -100, 50), 5)
Map.view()

6. Auto-dispatch with addEsriService

addEsriService inspects the URL and calls the right typed helper automatically. Useful when looping over mixed search results.

Map.clearMap()

# Mixed search — whatever types come back, auto-dispatch handles them
mixed = el.searchPortal("land cover", portal="agol", limit=5)
print(f"Search returned {len(mixed)} items:")

for r in mixed:
    print(f"  [{r['type']}] {r['title']}{r['url'][:60]}...")
    if not r['url']:
        print("    (no service URL, skipping)")
        continue
    try:
        el.addEsriService(
            r,
            name=r['title'],
            max_features=500,
        )
    except ValueError as e:
        print(f"    Skipped: {e}")
    except Exception as e:
        print(f"    Error: {type(e).__name__}: {e}")

Map.centerObject(ee.Geometry.BBox(-125, 24, -66, 50), 4)
Map.view()

7. Secured Portals — Token Auth

Most public portals (IIPP, AGOL public items) need no token. For secured enterprise portals, generate a token first.

POST https://your-portal.gov/portal/sharing/rest/generateToken
  username=myuser
  password=mypassword
  client=requestip
  expiration=60
  f=json

The response contains {"token": "...", "expires": ...}.

import urllib.parse
import urllib.request
import json

def get_esri_token(portal_url: str, username: str, password: str,
                   expiration: int = 60) -> str:
    """Obtain a short-lived ArcGIS token for a secured portal.

    Args:
        portal_url: Base URL of the portal (e.g. 'https://gis.myagency.gov/portal').
        username: Portal username.
        password: Portal password.
        expiration: Token lifetime in minutes (default 60).

    Returns:
        Token string.
    """
    token_url = f"{portal_url.rstrip('/')}/sharing/rest/generateToken"
    payload = urllib.parse.urlencode({
        "username": username,
        "password": password,
        "client": "requestip",
        "expiration": str(expiration),
        "f": "json",
    }).encode("utf-8")
    req = urllib.request.Request(token_url, data=payload,
                                 headers={"User-Agent": "geeViz/esriLib"})
    with urllib.request.urlopen(req, timeout=15) as resp:
        data = json.loads(resp.read().decode("utf-8"))
    if "error" in data:
        raise RuntimeError(f"Token error: {data['error']}")
    return data["token"]


# ---- EXAMPLE (replace with real credentials for a secured portal) ----
# token = get_esri_token(
#     "https://gis.myagency.gov/portal",
#     username="myuser",
#     password="mypassword",
# )
# el.searchPortal("classified data",
#                 portal="https://gis.myagency.gov/portal",
#                 token=token)
# el.addEsriFeatureService(
#     "https://gis.myagency.gov/arcgis/rest/services/Secure/FeatureServer/0",
#     token=token,
# )
print("Token helper defined. Uncomment the example block and fill in credentials.")

Module Reference

Function

Description

searchPortal(query, portal, ...)

Search any ArcGIS Portal

getServiceMetadata(url, token)

Fetch ?f=json metadata for any service

addEsriImageService(url_or_result, ...)

Add ImageServer as XYZ tiles

addEsriMapService(url_or_result, ...)

Add cached MapServer as XYZ tiles

addEsriFeatureService(url_or_result, ...)

Fetch + add vector GeoJSON layer

addEsriService(url_or_result, ...)

Auto-detect service type, then dispatch

Constant

Description

PORTALS

Dict of portal short names → base URLs

Key pitfalls

  • ArcGIS tile order is {z}/{y}/{x}, not {z}/{x}/{y}. The addEsriImageService / addEsriMapService helpers emit the correct order.

  • Feature Service overflow raises ValueError by design — silent truncation would produce partial-data analysis results.

  • outSR=4326 is always requested on Feature Service queries so the viewer renders GeoJSON in WGS84 natively without re-projection.

  • Token expiry — ArcGIS tokens default to 60-minute lifetimes. Re-generate before long notebook sessions.

  • CORS — public Esri endpoints (AGOL, IIPP) send permissive CORS headers. Private Enterprise installs behind a firewall need a backend proxy.