Examples
Review the Python and JavaScript examples here to see gSquare in action.
Python
"""
Create and monitor a gSquare estimate using a point
"""
import json
import os
import sys
import requests
import time
GEOSPAN_API_KEY = os.getenv("GEOSPAN_API_KEY") or input("Please enter your API key: ")
API_HOME = os.getenv("GEOSPAN_API_HOME", "https://api.geospan.com/remote4d/v1/api/")
# Ensure this point is on a rooftop!
LOCATION = [-93.18736857022286, 44.713507124491315]
def create_estimate(lon: float, lat: float) -> dict:
"""
Use a longitude and latitude to create a request for a
gSquare estimate.
The longitude and latitude should be on a roof top.
"""
url = f"{API_HOME}/gsquare/estimate"
headers = {
"Authorization": f"Api-Key {GEOSPAN_API_KEY}",
}
data = {
"wkt": f"POINT({lon} {lat})",
"includeImagery": False,
"includeWeather": True,
}
res = requests.post(url, headers=headers, json=data)
# Accept both 200 (OK) and 201 (Created) as valid responses
if res.status_code not in (200, 201):
raise ValueError(f"Failed to create estimate. Status code: {res.status_code}, Response: {res.text}")
try:
return res.json()
except json.JSONDecodeError:
raise ValueError(f"Response is not valid JSON. Status code: {res.status_code}, Response: {res.text}")
def check_estimate(query_key: str):
"""
Check the query status
Expected reuslts:
{
"queryKey": "sqm-abcdef12345"
}
"""
url = f"{API_HOME}/gsquare/query/{query_key}"
headers = {
"Authorization": f"Api-Key {GEOSPAN_API_KEY}",
}
res = requests.get(url, headers=headers)
return res.json()
def run_example(silent=False):
"""
Execute a gSquare query with a point.
Expected results:
{
"state": "SUCCESS",
"results": {
"computedFootprint": "POLYGON ((-93.187349 44.713629, -93.187347 44.713569, -93.187328 44.713569, -93.187322 44.71344, -93.187381 44.713439, -93.187381 44.713425, -93.187413 44.713425, -93.187414 44.713441, -93.187441 44.713441, -93.18745 44.713627, -93.187349 44.713629))",
"totalArea": {
"area": 197.8059923890073,
"units": "sqm"
},
"pitchResult": {
"primaryPitch": 3,
"deviation": 1
},
"confidence": 6,
"imagery": [],
"weather": [
{
"datecode": "231024",
"hailSize": 1.0,
"distance": 9.760984757545051
},
{
"datecode": "240803",
"hailSize": 1.0,
"distance": 7.845437799202324
}
]
}
}
"""
# get the lon-lat for the address
# start the gsquare estimate
query_key = create_estimate(LOCATION[0], LOCATION[1])["queryKey"]
# monitor the estimate until it is ready then pretty-print the results
max_polls = 20
while True:
estimate = check_estimate(query_key)
if estimate["state"] != "PENDING":
if not silent:
print(json.dumps(estimate, indent=2))
if estimate["state"] != "SUCCESS":
return 1
return 0
max_polls -= 1
if max_polls == 0:
return 1
time.sleep(2)
if __name__ == "__main__":
if not GEOSPAN_API_KEY:
print("API key is required.")
sys.exit(1) # Exit with non-zero exit code for failure
sys.exit(run_example())
Javascript
An example of using Mapbox and Geospan APIs to replicate gSquare functionality.
Creates a Mapbox GL JS map with building outlines. When buildings are clicked, it produces a gSquare estimate.
Note
The API keys and tokens in index.js are for demonstration only. In a real application, secure them using appropriate methods, such as environment variables, backend proxies, and IAM services with temporary credentials.
// Config
const MAPBOX_ACCESS_TOKEN = "";
const GEOSPAN_API_TOKEN = "";
// Constants
const MARKETPLACE_API_HOME = "https://api.geospan.com/remote4d/v1/api";
const FOOTPRINTS_URL = `${MARKETPLACE_API_HOME}/spatial/footprints`;
const ESTIMATE_URL = `${MARKETPLACE_API_HOME}/gsquare/estimate`;
const ESTIMATE_RESULT_URL = `${MARKETPLACE_API_HOME}/gsquare/query`;
const AUTH_HEADER = { Authorization: `Api-Key ${GEOSPAN_API_TOKEN}` };
const RESULTS_MAX_ATTEMPTS = 20;
const RESULTS_CHECK_DELAY = 2000;
const SQ_FT_PER_SQ_M = 10.7639;
mapboxgl.accessToken = MAPBOX_ACCESS_TOKEN;
// Initialize our map
const map = new mapboxgl.Map({
container: "map", // container ID
center: [-74.568, 39.983], // starting position [lng, lat]
zoom: 19, // starting zoom
style: "mapbox://styles/mapbox/satellite-v9",
});
// Once the map loads, set up our datasources and layers
map.on("load", () => {
// Create a new data source with an empty feature collection
map.addSource("building-footprints", {
type: "geojson",
data: {
type: "FeatureCollection",
features: [],
},
// Make sure all features have a unique ID
generateId: true,
});
// Add a new layer that uses our new data source to create features visible in
// the map
map.addLayer({
id: "building-footprints-layer",
type: "fill",
source: "building-footprints",
layout: {},
paint: {
// Conditional fill-color formatting for highlighting when clicked
"fill-color": [
"case",
["boolean", ["feature-state", "click"], false],
"#FFFF00",
"#0080ff",
],
"fill-opacity": 0.5,
},
});
// Load our footprint data for the first time
loadNewFootprints();
});
// takes a polygon feature and returns its center coordinate as a WKT string
const getCenterWKT = (feature) => {
const bounds = new mapboxgl.LngLatBounds();
feature.geometry.coordinates[0].forEach((coordinate) => {
bounds.extend(coordinate);
});
const center = bounds.getCenter();
return `POINT (${center.lng} ${center.lat})`;
};
// Given the current bounds of the map, load building footprints for that area
// and add them into our building-footprints datasource
const loadNewFootprints = async () => {
const bounds = map.getBounds().toArray().flat();
const params = new URLSearchParams({
bounds,
}).toString();
const footprints = await fetch(`${FOOTPRINTS_URL}?${params}`, {
headers: AUTH_HEADER,
})
.then((data) => data.json())
.catch((e) => displayResults("Could not load footprints"));
if (footprints) {
map.getSource("building-footprints").setData(footprints);
}
};
// Submits and retrieves the results of the estimate
const requestEstimate = async (wkt) => {
// Submit estimate request
const { queryKey } = await fetch(ESTIMATE_URL, {
method: "POST",
headers: {
...AUTH_HEADER,
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
wkt,
includeImagery: true,
includeWeather: true,
}),
}).then((data) => data.json());
// Poll Geospan for results
let counter = RESULTS_MAX_ATTEMPTS;
return new Promise((resolve, reject) => {
const loadResults = () => {
fetch(`${ESTIMATE_RESULT_URL}/${queryKey}`, {
headers: AUTH_HEADER,
})
.then((data) => data.json())
.then(({ state, results }) => {
if (state === "SUCCESS") {
resolve(results);
} else if (state === "PENDING" && counter > 0) {
setTimeout(() => {
loadResults(queryKey);
}, RESULTS_CHECK_DELAY);
} else if (state === "FAILURE") {
reject("There was an error in processing this request.");
} else {
reject("Your request has timed out.");
}
counter--;
})
.catch((e) => reject(e));
};
if (queryKey) {
loadResults(queryKey);
} else {
reject(
"There was an error creating the request. Please select another location or try again in a moment."
);
}
});
};
// Displays the results in the sidebar
const displayResults = (results) => {
document.querySelector("#results").innerHTML = results;
};
let highlightedBuildingId = null;
// Remove highlight from a building and clear results in the sidebar
const clearResults = () => {
if (highlightedBuildingId !== null) {
map.setFeatureState(
{ source: "building-footprints", id: highlightedBuildingId },
{ click: false }
);
}
highlightedBuildingId = null;
displayResults("");
};
// Adds a highlight to a clicked building
const highlightClickedBuilding = (mapboxFeature) => {
clearResults();
map.setFeatureState(
{ source: "building-footprints", id: mapboxFeature.id },
{ click: true }
);
highlightedBuildingId = mapboxFeature.id;
};
// Make sure that every time we pan / zoom the map, we update our building
// footprints
map.on("moveend", () => {
clearResults();
loadNewFootprints();
});
// When we click on a building, highlight the building and request an estimate
map.on("click", "building-footprints-layer", (e) => {
highlightClickedBuilding(e.features[0]);
displayResults("Loading gSquare Estimate...");
// Get the center coordinate of the building and convert it to a WKT string
// that we can use to submit our estimate request
const wkt = getCenterWKT(e.features[0]);
requestEstimate(wkt)
.then((data) =>
displayResults(`
<div><b>Total Area</b>: ${(
data.totalArea.area * SQ_FT_PER_SQ_M
).toFixed(2)} sq ft</div>
<div><b>Primary Pitch</b>: ${data.pitchResult.primaryPitch}</div>
`)
)
.catch((e) => displayResults("Unable to load estimate"));
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>gSquare Lite</title>
<link
href="https://api.mapbox.com/mapbox-gl-js/v3.6.0/mapbox-gl.css"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="map"></div>
<div id="sidebar">
<h1>Click a building to get a gSquare Estimate</h1>
<div id="results"></div>
</div>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.6.0/mapbox-gl.js"></script>
<script src="index.js"></script>
</body>
</html>
html, body {
height: 100%;
width: 100%;
font-family: sans-serif;
}
body {
padding: 0;
margin: 0;
display: flex;
align-content: stretch;
}
#map {
width: 80%;
}
#sidebar {
width: 20%;
padding: 20px;
}