<jupyter_start><jupyter_text>Finding fieldsThis notebook shows the different ways of searching for fields through the Agrimetrics Graph Query API.There are four main ways of retrieving fields:1. Find fields which lie entirely or partially within a given polygon.2. Find fields whose centres lie within a distance of a given point.3. Find the field which a point falls in.4. Find a field using its unique id.This query example selects all the fields which overlap a given polygon (in this case, an area just east of Letchworth in the UK), and returns the area, altitude and topsoil information for each one.First, we import the necessary Python modules.<jupyter_code>import json
import os
from pprint import pprint
import requests

import pandas as pd<jupyter_output><empty_output><jupyter_text>Define the GraphQL endpoint and the API subscription key.<jupyter_code>GRAPHQL_ENDPOINT = "https://api.agrimetrics.co.uk/graphql/v1/"

if "API_KEY" in os.environ:
    API_KEY = os.environ["API_KEY"]
else:
    API_KEY = input("Query API Subscription Key: ").strip()
    
common_headers = {
    "OCP-APIM-Subscription-Key": API_KEY
}<jupyter_output><empty_output><jupyter_text>We will also need a short function to help catch and report errors from making GraphQL queries.<jupyter_code>def check_results(result):
    if result.status_code != 200:
        raise Exception(f"Request failed with code {result.status_code}.\n{result.text}")
    errors = result.json().get("errors", [])
    if errors:
        for err in errors:
            print(f"{err['message']}:")
            print( "  at", " and ".join([f"line {loc['line']}, col {loc['column']}" for loc in err['locations']]))
            print( "  path", ".".join(err['path']))
            print(f"  {err['extensions']}")
        raise Exception(f"GraphQL reported {len(errors)} errors")<jupyter_output><empty_output><jupyter_text>Search by polygonWe will first search for the fields within or overlapping a polygon.The polygon to search must be defined as a [GeoJSON](https://geojson.org/) `Polygon` structure, which is the structure shown below. The points within the polygon are in `[longitude, latitude]` order, and must circle the boundary of the field in anticlockwise order. The last point must be the same as the first.<jupyter_code>POLYGON_LOCATION = {
  "type": "Polygon",
  "coordinates": [
    [
      [-0.434818, 51.790037],
      [-0.305557, 51.790037],
      [-0.305557, 51.838111],
      [-0.434818, 51.838111],
      [-0.434818, 51.790037]
    ]
  ]
}<jupyter_output><empty_output><jupyter_text>Define the base query. Note that the query has the mandatory `$location` parameter, which is used to define the area being searched. The search expression is then provided in the `filter` parameter to the `fields` property of the query.<jupyter_code>polygon_query = """
  query PolygonQuery($location: LocationFilter!) {
    fields(geoFilter: { location: $location }) {
      id
      soil {
        topSoil {
          texture {
            type
          }
        }
      }
    }
  }
"""<jupyter_output><empty_output><jupyter_text>Construct the query object to pass to the API. Note that the `variables` section of the `query_object` contains a key called `location`, mirroring the `$location` parameter in the query text above, and that the optional `operationName` matches the name of the `polygon_query` defined above.Retrieve the data from the API using `requests`, and parse the returned JSON data.<jupyter_code>polygon_variables = {
  "location": POLYGON_LOCATION
}
polygon_query_object = {
  "query": polygon_query,
  "variables": polygon_variables,
  "operationName": "PolygonQuery"
}
result = requests.post(GRAPHQL_ENDPOINT, headers=common_headers, json=polygon_query_object)
check_results(result)
data = result.json()["data"]<jupyter_output><empty_output><jupyter_text>Finally, process the data for each field and put it into a data frame.<jupyter_code>field_data = ((field["id"], field["soil"]["topSoil"]["texture"]["type"]) for field in data["fields"])
data_frame = pd.DataFrame(field_data)
data_frame<jupyter_output><empty_output><jupyter_text>Search by radiusThe second method of searching is by radius from a given point. This method finds fields whose centres (or, more precisely, centroids) fall within a distance of a point. This will typically be much faster than a polygon query.The centre point of the query region must be a [GeoJSON](https://geojson.org/) `Point` object, with the coordinates in `[longitude, latitude]` order. The distance in the query is measured in metres.<jupyter_code>POINT_LOCATION = {
    "type": "Point",
    "coordinates": [-0.368728, 51.809994]
}
QUERY_DISTANCE = 3500

radius_query = """
  query RadiusQuery($location: LocationFilter!, $range: Float!) {
    fields(geoFilter: { location: $location, distance: {LE: $range} }) {
      id
      soil {
        topSoil {
          texture {
            type
          }
        }
      }
    }
  }
"""<jupyter_output><empty_output><jupyter_text>Run the query as before.<jupyter_code>radius_variables = {
  "location": POINT_LOCATION,
  "range": QUERY_DISTANCE
}
radius_query_object = {
  "query": radius_query,
  "variables": radius_variables,
  "operationName": "RadiusQuery"
}
result = requests.post(GRAPHQL_ENDPOINT, headers=common_headers, json=radius_query_object)
check_results(result)
data = result.json()["data"]

field_data = ((field["id"], field["soil"]["topSoil"]["texture"]["type"]) for field in data["fields"])
data_frame = pd.DataFrame(field_data)
data_frame<jupyter_output><empty_output><jupyter_text>Search by pointThe third method of searching is to find the field which lies at a specific point (e.g. the point on a map which a user clicked on). Making a point query is almost identical to making a radius query, except that the distance is omitted.<jupyter_code>POINT_LOCATION = {
    "type": "Point",
    "coordinates": [-0.368728, 51.809994]
}

point_query = """
  query PointQuery($location: LocationFilter!) {
    fields(geoFilter: { location: $location }) {
      id
      soil {
        topSoil {
          texture {
            type
          }
        }
      }
    }
  }
"""<jupyter_output><empty_output><jupyter_text>Run the query. Note that the `fields` property is still a list of fields, containing one field. If the query point does not lie on any field, the `fields` property will be an empty list.<jupyter_code>point_variables = {
  "location": POINT_LOCATION,
}
point_query_object = {
  "query": point_query,
  "variables": point_variables,
  "operationName": "PointQuery"
}
result = requests.post(GRAPHQL_ENDPOINT, headers=common_headers, json=point_query_object)
check_results(result)
data = result.json()["data"]

field_data = ((field["id"], field["soil"]["topSoil"]["texture"]["type"]) for field in data["fields"])
data_frame = pd.DataFrame(field_data)
print("Field id:", data_frame[0][0])
print("Topsoil type:", data_frame[1][0])<jupyter_output>Field id: https://data.agrimetrics.co.uk/fields/gf7bB7-sDmLNA-IrADs4ew
Topsoil type: LOAM<jupyter_text>![Field location](img/field-single.png)  Search by idThe fourth and final method of selecting fields is to select a single field or collection of fields by using the id(s) of the fields. For a single field, the `id` parameter should be supplied. For multiple fields in a single query, pass a list of field ids in the `ids` parameter.Field ids should be strings, containing either the full URI, as returned from the Graph Query API (see the examples above), or it can be an abbreviated id, using the `agfd:` prefix and the part of the URI following the last `/`. Both forms are shown in the example below.<jupyter_code>FIELD_ID = "agfd:0-7PISj6IaqS8_MQnRsbNw"
FIELD_IDS = ["agfd:0-7PISj6IaqS8_MQnRsbNw", "https://data.agrimetrics.co.uk/fields/1jEJOnLYkmmOLzSVt8hKyQ"]

id_query = """
  query FieldById($fieldId: [ID!]!) {
    fields(where: {id: {EQ: $fieldId}}) {
      id
      area {
        value
      }
      soil {
        topSoil {
          texture {
            type
          }
        }
      }
    }
  }
"""<jupyter_output><empty_output><jupyter_text>Run the query to retrieve a single field by id:<jupyter_code>id_variables = {
  "fieldId": FIELD_ID
}
id_query_object = {
  "query": id_query,
  "variables": id_variables,
  "operationName": "FieldById"
}
result = requests.post(GRAPHQL_ENDPOINT, headers=common_headers, json=id_query_object)
check_results(result)
data = result.json()["data"]

field_data = ((field["id"], field["soil"]["topSoil"]["texture"]["type"]) for field in data["fields"])
data_frame = pd.DataFrame(field_data)
print("Field id:", data_frame[0][0])
print("Topsoil type:", data_frame[1][0])<jupyter_output>Field id: https://data.agrimetrics.co.uk/fields/0-7PISj6IaqS8_MQnRsbNw
Topsoil type: LOAM<jupyter_text>Run the same query with different parameters to retrieve several fields by their ids:<jupyter_code>id_variables = {
  "fieldId": FIELD_IDS
}
id_query_object = {
  "query": id_query,
  "variables": id_variables,
  "operationName": "FieldById"
}
result = requests.post(GRAPHQL_ENDPOINT, headers=common_headers, json=id_query_object)
check_results(result)
data = result.json()["data"]

field_data = ((field["id"], field["soil"]["topSoil"]["texture"]["type"]) for field in data["fields"])
data_frame = pd.DataFrame(field_data)
data_frame<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11712482618461867, 'fraction_numerical': 0.015188790244945984, 'mean_word_length': 4.4903112155020555, 'pattern_counts': {'":': 28, '<': 55, '<?xml version=': 0, '>': 55, 'https://': 6, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '14', 'dir': 'jupyter-structured-clean-dedup', 'id': '120485', 'license': 'MIT', 'n_tokens_mistral': 3035, 'n_tokens_neox': 2956, 'n_words': 882, 'path': 'graphql-examples/geo_search.ipynb', 'repo_name': 'markbneal/api-examples', 'size': '9349'}