Examples

Review the Python and JavaScript examples here to see gSquare in action.

Python

Download 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.

Download Zip

// 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;
}