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 |
|
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'])
1. Portal Search¶
searchPortal hits the standard ArcGIS /sharing/rest/search endpoint —
identical across IIPP, ArcGIS Online, and any Enterprise install.
# Search IIPP (default) for NAIP imagery
results = el.searchPortal("naip", limit=10)
print(f"Found {len(results)} results on IIPP:\n")
for r in results:
print(f" [{r['type']}] {r['title']}")
print(f" URL: {r['url']}")
# Search ArcGIS Online for wildfire perimeter data
agol_results = el.searchPortal("wildfire perimeter 2023", portal="agol", limit=5)
print(f"Found {len(agol_results)} results on AGOL:")
for r in agol_results:
print(f" [{r['type']}] {r['title']}")
# Search with data_only=False to see all item types
all_types = el.searchPortal("imagery", portal="agol", limit=5, data_only=False)
print("All item types (no filter):")
for r in all_types:
print(f" [{r['type']}] {r['title']}")
# Power-user raw query — portal DSL syntax, bypasses data_only entirely
raw = el.searchPortal(
"",
portal="agol",
raw_q='type:"Feature Service" tags:fire owner:USFS',
limit=5,
)
print(f"Raw DSL results: {len(raw)}")
for r in raw:
print(f" {r['title']}")
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 |
|---|---|
|
Search any ArcGIS Portal |
|
Fetch |
|
Add ImageServer as XYZ tiles |
|
Add cached MapServer as XYZ tiles |
|
Fetch + add vector GeoJSON layer |
|
Auto-detect service type, then dispatch |
Constant |
Description |
|---|---|
|
Dict of portal short names → base URLs |
Key pitfalls¶
ArcGIS tile order is
{z}/{y}/{x}, not{z}/{x}/{y}. TheaddEsriImageService/addEsriMapServicehelpers emit the correct order.Feature Service overflow raises
ValueErrorby 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.