gSquare

gSquare is GEOSPAN’s “instant” roof area estimation APIs. gSquare supplies area and predominant pitch in seconds.

Use these APIs to:

Before you begin

Before starting with the gSquare APIs, take time to review the following resources:

  • Get Started — set up the right account, API key, and quota
  • gSquare examples — review the Python and JavaScript code examples
  • Concepts — familiarize yourself with geoprocessing concepts and review the gSquare Lifecycle diagram. This asynchronous approach allows for efficient handling of potentially time-consuming estimation tasks without blocking your application:
    1. Request: You initiate the lifecycle with a request to the /estimate/ endpoint to create an estimate.
    2. Get query key: The API returns a query key immediately.
    3. Poll: You then need to poll the /query/:queryKey endpoint to monitor the status of the estimate.
    4. Monitor status: You check the response from the API, which indicates one of these statuses:
      • PENDING: The estimate is still being processed.
      • FAILURE: The estimation process encountered an error.
      • SUCCESS: The estimate is complete and results are available.
    5. Act on result:
      • If FAILURE, an error report is generated.
      • If SUCCESS, read and utilize the results.
Note

While you can use the gSquare APIs to get an instant estimate, it is not required when generating an order. This step can be skipped. For information about ordering, see gPro.

Estimating roof areas

The following sections provide an example of the HTTP REST method for each API endpoint, along with explanations of request parameters and an example result that GEOSPAN returns.

POST /estimate

HTTP method

Create a new area estimation task by providing a roof footprint in Well-Known Text (WKT) format.

curl -X 'POST' \
  'https://api.geospan.com/remote4d/v1/gsquare/estimate' \
  -H 'accept: application/json' \
  -H 'Authorization: Api-Key <geospanapikeystring>' \
  -H 'Content-Type: application/json' \
  -d '{
  "wkt": "POINT ( -93.18739 44.71353 )",
  "includeImagery": true,
  "includeWeather": true
}'
{
    "queryKey": "sqm-if_plI1MRIasdf1234lkjh"
}

Parameters

wkt

[required] [type: string] Well-Known Text representation of a roof footprint. Can be a POLYGON or POINT.

includeImagery

[optional] [type: boolean] Toggle whether to include oblique views. Default is false.

includeWeather

[optional] [type: boolean] Toggle whether to include weather data. Default is false.

model

[optional] [type: string] Specify the model to use for estimation. Default is “default”.

GET /query/{query_key}

HTTP method

Retrieve the status and results of a specific estimation task using the query key returned by the POST /estimate endpoint.

curl -X 'GET' \
  'https://api.geospan.com/remote4d/v1/gsquare/query/abcd1234-5678-90ef-ghij-klmnopqrstuv' \
  -H 'accept: application/json' \
  -H 'Authorization: Api-Key <geospanapikeystring>'
{
    "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": [
        {
            "orientation": "nadir",
            "image": "/9j/4AAQSkZJRgABAgAAAQABAAD/4QjKaHR0cDov
            .
            .
            .
            61TvbxGcBRkY5OakkJkyJGI+lVZoUGQG604os//2Q=="
        },
        {
            "orientation": "north",
            "image": "/9j/4AAQSkZJRgABAgAAAQABAAD/4QiwaHR0cDov
            .
            .
            .
            WzZUQnbvPeolBsGrn/2Q=="
        },
        {
            "orientation": "east",
            "image": "/9j/4AAQSkZJRgABAgAAAQABAAD/4Qi9aHR0cDov
            .
            .
            .
            ZP/AIql9al0gwVFPqf/2Q=="
        },
        {
            "orientation": "south",
            "image": "/9j/4AAQSkZJRgABAgAAAQABAAD/4QiuaHR0cDov
            .
            .
            .
            /+tWcqji9hXR//9k="
        },
        {
            "orientation": "west",
            "image": "/9j/4AAQSkZJRgABAgAAAQABAAD/4Qi+aHR0cDov
            .
            .
            .
            pnMBiIeNTuZcEdQf5UquF5WjOULM//2Q=="
        }
    ],
    "weather": [
        {
            "datecode": "231024",
            "hailSize": 1,
            "distance": 9.760984757545051
        },
        {
            "datecode": "240803",
            "hailSize": 1,
            "distance": 7.845437799202324
        }
    ]
}

Parameters

query_key

[required] [type: string] The unique identifier for the gSquare query.

Subsections of gSquare

API Specification

Before getting started

Before starting with the gSquare API, take time to review Get Started and familiarize yourself with the Concepts.

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