Source code for util.spatial


from util import BaseUtil

[docs] class SpatialUtil:
[docs] @staticmethod def download_from_esri(url:str, fields=None, where=None, limit=None, **kwargs): """A util to download data from an ESRI endpoints. This is a wrapper around geopandas.read_file that handles the ESRI API's max record count. It loads the data in chunks and concatenates them into a single GeoDataFrame. The final GeoDataFrame is returned and can get large if the limit is not set. One approach to do this is to set the limit to a small number and then filter the data in the where clause. or order them by a field and set the limit to the number of records you want to download. Note: This function uses the pyogrio engine to read the data. This is a faster engine than the default fiona engine. To use this engine, you need to install the pyogrio package. You can install it using `pip install pyogrio`. Note: It uses `get` request. it might reject your request if you send large request body geometries. can use post for such cases but it is not implemented here. ask Majid for help if you need it. :param url: Esri endpoint url for example https://services.arcgis.com/8DAUcrpQcpyLMznu/arcgis/rest/services/COVID19_Cases_by_Zip_Code/FeatureServer/0 :type url: str :param fields: the list of the fields to return, defaults to None which loads all fields by passing `*` to the API :type fields: _type_, optional :param where: the filter criteria, defaults to None which means no filter and it sends `1=1` to the API :type where: _type_, optional :param limit: the max number of records to get, defaults to all the values in the feaure layer :type limit: _type_, optional :return: A pandas DataFrame with the data :rtype: _type_ :example: ```python url = "https://pcrdgis.agriculture.purdue.edu/arcgis/rest/services/DDI/Digital_Divide_Index_COUNTY_USA_2020/MapServer/0" gdf=SpatialUtil.download_from_esri(url=url) ``` """ import geopandas as gpd import pandas as pd import json from tempfile import NamedTemporaryFile gpd.options.io_engine = "pyogrio" # Get the max record count metadata = BaseUtil.get_request(url=url,params=dict(f="pjson")).json() max_record_count = metadata["maxRecordCount"] # default behavior matches all features if where is None: where = "1=1" if fields is None: fields = "*" else: fields = ", ".join(fields) # extract object IDs of features queryURL = f"{url}/query" # Get the total record count params = dict(where=where, returnCountOnly="true", f="json") response = BaseUtil.get_request(url=queryURL, params=params) total_size = BaseUtil.get_json_safely(response)["count"] # Check the limit if limit is not None: total_size = limit # params for this request resultOffset = 0 params = dict( f="json", outSR="4326", outFields=fields, resultOffset=resultOffset, where=where, **kwargs, ) calls = total_size // max_record_count if calls > 10: print( f"Long download time — total download will require {calls} separate requests" ) out = [] while params["resultOffset"] < total_size: remaining = total_size - params["resultOffset"] if remaining < max_record_count: params["resultRecordCount"] = remaining # get raw features response = BaseUtil.get_request(url=queryURL, params=params) json_content = BaseUtil.get_json_safely(response) with NamedTemporaryFile(suffix=".json",mode="w+") as json_file: # write to file json.dump(json_content, json_file) json_file.flush() gdf=gpd.read_file(json_file.name, driver="ESRIJSON",crs="EPSG:4326") out.append(gdf) params["resultOffset"] += len(out[-1]) return pd.concat(out, axis=0).reset_index(drop=True)