Get footprint data from OSM then assemble it into a GeoDataFrame.

    Parameters
    ----------
    polygon : shapely Polygon or MultiPolygon
        geographic shape to fetch the footprints within
    north : float
        northern latitude of bounding box
    south : float
        southern latitude of bounding box
    east : float
        eastern longitude of bounding box
    west : float
        western longitude of bounding box
    footprint_type : string
        type of footprint to be downloaded. OSM tag key e.g. 'building', 'landuse', 'place', etc.
    retain_invalid : bool
        if False discard any footprints with an invalid geometry

    Returns
    -------
    GeoDataFrame

def create_footprints_gdf(polygon=None, north=None, south=None, east=None, west=None, 
                          footprint_type='building', retain_invalid=False):
    """
    Get footprint data from OSM then assemble it into a GeoDataFrame.

    Parameters
    ----------
    polygon : shapely Polygon or MultiPolygon
        geographic shape to fetch the footprints within
    north : float
        northern latitude of bounding box
    south : float
        southern latitude of bounding box
    east : float
        eastern longitude of bounding box
    west : float
        western longitude of bounding box
    footprint_type : string
        type of footprint to be downloaded. OSM tag key e.g. 'building', 'landuse', 'place', etc.
    retain_invalid : bool
        if False discard any footprints with an invalid geometry

    Returns
    -------
    GeoDataFrame
    """

    responses = osm_footprints_download(polygon, north, south, east, west, footprint_type)

    # list of polygons to removed at the end of the process
    pop_list = []

    vertices = {}
    for response in responses:
        for result in response['elements']:
            if 'type' in result and result['type']=='node':
                vertices[result['id']] = {'lat' : result['lat'],
                                          'lon' : result['lon']}

    footprints = {}
    for response in responses:
        for result in response['elements']:
            if 'type' in result and result['type']=='way':
                nodes = result['nodes']
                try:
                    polygon = Polygon([(vertices[node]['lon'], vertices[node]['lat']) for node in nodes])
                except Exception:
                    log('Polygon has invalid geometry: {}'.format(nodes))
                footprint = {'nodes' : nodes,
                            'geometry' : polygon}

                if 'tags' in result:
                    for tag in result['tags']:
                        footprint[tag] = result['tags'][tag]

                # if polygons are untagged or not tagged with the footprint_type
                # add them to pop_list to be removed from the final dictionary
                if 'tags' not in result:
                    pop_list.append(result['id'])
                elif footprint_type not in result['tags']:
                    pop_list.append(result['id'])

                footprints[result['id']] = footprint

    # Create multipolygon footprints and pop untagged supporting polygons from footprints
    for response in responses:
        for result in response['elements']:
            if 'type' in result and result['type']=='relation':

                outer_polys = []
                inner_polys = []
                multipoly = []

                for member in result['members']:
                    if 'role' in member and member['role']=='outer':
                        outer_polys.append(member['ref'])
                    if 'role' in member and member['role']=='inner':
                        inner_polys.append(member['ref'])

                # osm allows multiple outer polygons in a relation
                for outer_poly in outer_polys:
                    temp_poly=footprints[outer_poly]['geometry']

                    for inner_poly in inner_polys:
                        temp_poly=temp_poly.difference(footprints[inner_poly]['geometry'])

                    multipoly.append(temp_poly)

                footprint = {'geometry' : MultiPolygon(multipoly)}

                if 'tags' in result:
                    for tag in result['tags']:
                        footprint[tag] = result['tags'][tag]

                footprints[result['id']] = footprint

    # remove supporting geometry from footprints dictionary
    for item in pop_list:
        footprints.pop(item)

    gdf = gpd.GeoDataFrame(footprints).T
    gdf.crs = settings.default_crs

    if not retain_invalid:
        # drop all invalid geometries
        gdf = gdf[gdf['geometry'].is_valid]

    return gdf