Search code examples
pythongeojsonarcgisfolium

How to format an ArcGIS FeatureSet/FeatureCollection to be used with Folium


Goal: To use ArcGIS's drive time calculation data and display the results using Folium instead of ArcGIS/ESRI's mapping.

Explanation: Using the arcgis package, I am able to obtain drive time data surrounding a given point, providing me with a boundary area to be shown on a map. The data from ArcGIS, maps just fine using ArcGIS, however, it is very slow to display and much more time consuming to display other elements on ArcGIS maps in general, such as radii, individual points and anything with a custom icon. However, Folium works quite well to display all this extra data and loads much more quickly than ArcGIS.

Obstacle: I am looking for a way to use this data that is returned from ArcGIS and then display it using Folium. The problem I'm having is that the data from ArcGIS, while it says it includes coordinate data, has values that are way too large to be latitude/longitude. It would seem that there is either some way to 'decode' these values to be used with Folium, or perhaps I am just not using the data from ArcGIS as the correct data type once I try to use it with Folium.

import pandas
import arcgis
import webview

data = {'address': {0:'2015 Terminal Way'}, 'city': {0:'Reno'}, 'state': {0: 'NV'}, 'zip': {0:'89502'}}
df = pandas.DataFrame(data)
# Obviously I'm unable to include my username and password - which I understand probably limits who can help with this question since without logging in, you wouldn't be able to test my code, but there's nothing I can do about it
my_gis = arcgis.gis.GIS("https://www.arcgis.com", username, password)
address_layer = my_gis.content.import_data(df, address_fields={"Address":"address","City":"city","State":"state","Zip":"zip"})
target = arcgis.features.use_proximity.create_drive_time_areas(input_layer=address_layer, break_values=[5], break_units="Minutes", overlap_policy="Overlap")
# NOTE - ArcGIS documentation states that if an output name is given, the return object is a FeatureSet, but since I leave it blank, it is a FeatureCollection - not sure if that matters either
drivetime_data_geojson = target.query().to_geojson
# The above line returns as below, though trimmed down a bit to save space - I'm so sorry it's so long but pasting it in was the only way I could think of to allow others to test it in Folium

drivetime_data_geojson = '{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry":
{"type": "Polygon", "coordinates": [[[-13334722.942400001, 4801659.346199997], [-13334622.9429,
4801594.495999999], [-13334572.9431, 4801335.0988000035], [-13334572.9431, 4800232.736199997], 
[-13334197.9448, 4800232.736199997], [-13334122.9452, 4800297.577799998], [-13334022.945700001, 
4800167.895199999], [-13334047.945500001, 4800005.794399999], [-13334122.9452, 4799940.954700001], 
[-13334572.9431, 4799940.954700001], [-13334622.9429, 4799227.746699996], [-13334497.943500001, 
4799195.329400003], [-13334447.9437, 4799065.6611], [-13334222.944699999, 4799065.6611], 
[-13333947.9459, 4798968.4108000025], [-13333522.947900001, 4798676.666100003], [-13332722.9515, 
4798579.419799998], [-13332622.952, 4798449.759499997], [-13332722.9515, 4798287.686399996], 
[-13333247.9492, 4798320.100699998]]]}}]}'

# This is how it would be displayed using ArcGIS
# Creating the basemap image
map = my_gis.map(location='2015 Terminal Way Reno, NV 89502')
# Adding the layer to the map
map.add_layer(drivetime_data)
folder_path = os.getcwd()
path_to_map = folder_path + '\\arcgis_to_folium.html'
# webview is just a lightweight browser package that allows for easy viewing of these maps
webview.create_window('MAP', path_to_map)
webview.start() 

Folium would look something close to this - though as I state, that the GeoJson data doesn't pull in right now:

import folium

folium_map = folium.Map(location=[39.505745, -119.77869])
drivetime_layer = folium.FeatureGroup(name='Drive Time')
folium.GeoJson(drivetime_data_geojson).add_to(drivetime_layer)
drivetime_layer.add_to(folium_map)
folium.LayerControl().add_to(folium_map)
folium_map.save('folium_drivetime_attempt.html')
path_to_map = folder_path + '\\folium_drivetime_attempt.html'
webview.create_window('MAP', path_to_map)
webview.start()

This map will load, but there won't be any layer to it. I do know that this geojson will work:

drivetime_data_geojson = '{"type": "FeatureCollection", "features": [{
"type": "Feature", "geometry": {"type": "Polygon", "coordinates": [
[[-119.766227, 39.856011], [-120.260612, 39.251417], [-119.067222, 39.246099]]]}}]}'

Because it uses GPS coords instead of these strange values from ArcGIS. So really, it's a matter of figuring out how to 'decode' those values from ArcGIS or converting it to the correct data type. Any help, suggestions or thoughts are much appreciated.


Solution

  • There are two problems you'll have to deal with. The first is converting the ArcGIS x, y to lat, lon which can be done with a function like this:

    import math
    
    def meters_to_coords(y, x):
        if y > 0:
            z = -20037508.3427892
        else:
            z = 20037508.3427892
    
        lon = (y / z) * 180
        lat = (x / z) * 180
        lat = 180 / math.pi * (2 * math.atan(math.exp(lat * math.pi / 180)) - math.pi / 2)
        return [lon, lat]
    

    Instead of drivetime_data_geojson = target.query().to_geojson use drivetime_data_dict = target.query().to_dict() You'll notice that when you convert it to a dictionary, the key 'coordinates' changes to 'rings' which is important for the next step. Next, iterate over the dict and run the above function against the coordinate values to update them to GPS coords. Then you need to adjust the format of the dictionary to be something that folium can recognize and essentially pulling in the 'rings' data which now contains your coordinate values and will be something like this:

    # Grab the same list of drive time values you sent into the ArcGIS request and use it here, 
    # since you'll get a separate set of x,y data for each driving time you request - 
    # per above, break_values was just [5]
    
    drivetime_ranges = break_values
    first_pass = False
    
    for n in range(len(drivetime_ranges)):
        for i, each in enumerate(drivetime_data_dict['features'][n]['geometry']['rings'][0]):
            drivetime_data_dict['features'][n]['geometry']['rings'][0][i] = meters_to_coords(each[0], each[1])
        if first_pass is not True:
            default_geojson_structure = {
                                         "type": "FeatureCollection", 
                                         "features": [{"type": "Feature",
                                         "geometry": {"type": "Polygon", 
                                         "coordinates":
                                         [drivetime_data_dict['features'][n]
                                         ['geometry']['rings'][0]]}}]
                                         }
            first_pass = True
        else:
            default_geojson_structure['features'].append({"type": "Feature",
            "geometry": {"type": "Polygon", "coordinates":
            [dt_map_overlay['features'][n]['geometry']['rings'][0]]}})
    
    print(default_geojson_structure)
    

    Now just create a folium FeatureGroup:

    dt_layer = folium.FeatureGroup(name='Drive Time')
    

    And load the default_geojson_structure into the following function and add that to your layer.

    folium.GeoJson(default_geojson_structure, style_function=lambda x: {'fillColor': 
    '#0000ff'}).add_to(dt_layer)
    

    Do the rest like you did from there, in terms of saving the map and what not and running webview and you'll see the map with your drivetime boundary.

    enter image description here