def visualize_hexagons(hexagons, color="red", folium_map=None): """ hexagons is a list of hexcluster. Each hexcluster is a list of hexagons. eg. [[hex1, hex2], [hex3, hex4]] """ polylines = [] lat = [] lng = [] for hex in hexagons: polygons = h3.h3_set_to_multi_polygon([hex], geo_json=False) # flatten polygons into loops. outlines = [loop for polygon in polygons for loop in polygon] polyline = [outline + [outline[0]] for outline in outlines][0] lat.extend(map(lambda v: v[0], polyline)) lng.extend(map(lambda v: v[1], polyline)) polylines.append(polyline) if folium_map is None: m = folium.Map(location=[sum(lat) / len(lat), sum(lng) / len(lng)], zoom_start=13, tiles='cartodbpositron') else: m = folium_map for polyline in polylines: my_PolyLine = folium.PolyLine(locations=polyline, weight=8, color=color) m.add_child(my_PolyLine) return m
def test_h3_set_to_multi_polygon_single_geo_json(self): h3_addresses = ['89283082837ffff'] multi_polygon = h3.h3_set_to_multi_polygon(h3_addresses, True) vertices = h3.h3_to_geo_boundary(h3_addresses[0], True) # We shift the expected circular list so that it starts from multi_polygon[0][0][0], # since output starting from any vertex would be correct as long as it's in order. expected_coords = self.shift_circular_list(multi_polygon[0][0][0], [ vertices[2], vertices[3], vertices[4], vertices[5], vertices[0], vertices[1] ]) expected = [[expected_coords]] self.assertEqual(len(multi_polygon), 1, 'polygon count matches expected') self.assertEqual(len(multi_polygon[0]), 1, 'loop count matches expected') self.assertEqual( len(multi_polygon[0][0]), 7, 'coord count 7 matches expected according to geojson format') self.assertEqual( multi_polygon[0], multi_polygon[-1], 'first coord should be the same as last coord according to geojson format' ) self.assertAlmostEqual( multi_polygon[0][0][0][0], -122.42778275313199, None, 'the coord should be (lng, lat) according to geojson format (1)') self.assertAlmostEqual( multi_polygon[0][0][0][1], 37.77598951883773, None, 'the coord should be (lng, lat) according to geojson format (2)') # Discard last coord for testing below, since last coord is the same as the first one multi_polygon[0][0].pop() self.assertEqual(multi_polygon, expected, 'outline matches expected')
def test_h3_set_to_multi_polygon_contiguous(self): # the second hexagon shares v0 and v1 with the first h3_addresses = ['89283082837ffff', '89283082833ffff'] multi_polygon = h3.h3_set_to_multi_polygon(h3_addresses) vertices0 = h3.h3_to_geo_boundary(h3_addresses[0]) vertices1 = h3.h3_to_geo_boundary(h3_addresses[1]) # We shift the expected circular list so that it starts from multi_polygon[0][0][0], # since output starting from any vertex would be correct as long as it's in order. expected_coords = self.shift_circular_list(multi_polygon[0][0][0], [ vertices1[0], vertices1[1], vertices1[2], vertices0[1], vertices0[2], vertices0[3], vertices0[4], vertices0[5], vertices1[4], vertices1[5], ]) expected = [[expected_coords]] self.assertEqual(len(multi_polygon), 1, 'polygon count matches expected') self.assertEqual(len(multi_polygon[0]), 1, 'loop count matches expected') self.assertEqual(len(multi_polygon[0][0]), 10, 'coord count 10 matches expected') self.assertTrue(multi_polygon == expected, 'outline matches expected')
def test_h3_set_to_multi_polygon_2k_ring(self): # 2-ring in order returned by algo h3_addresses = h3.k_ring('8930062838bffff', 2) multi_polygon = h3.h3_set_to_multi_polygon(h3_addresses) self.assertEqual( len(multi_polygon), 1, 'polygon count matches expected' ) self.assertEqual( len(multi_polygon[0]), 1, 'loop count matches expected' ) self.assertEqual( len(multi_polygon[0][0]), 6 * (2 * 2 + 1), 'coord count matches expected' ) # Same k-ring in random order h3_addresses = [ '89300628393ffff', '89300628383ffff', '89300628397ffff', '89300628067ffff', '89300628387ffff', '893006283bbffff', '89300628313ffff', '893006283cfffff', '89300628303ffff', '89300628317ffff', '8930062839bffff', '8930062838bffff', '8930062806fffff', '8930062838fffff', '893006283d3ffff', '893006283c3ffff', '8930062831bffff', '893006283d7ffff', '893006283c7ffff' ] multi_polygon = h3.h3_set_to_multi_polygon(h3_addresses) self.assertEqual( len(multi_polygon), 1, 'polygon count matches expected' ) self.assertEqual( len(multi_polygon[0]), 1, 'loop count matches expected' ) self.assertEqual( len(multi_polygon[0][0]), 6 * (2 * 2 + 1), 'coord count matches expected' ) h3_addresses = list(h3.k_ring('8930062838bffff', 6)) h3_addresses.sort() multi_polygon = h3.h3_set_to_multi_polygon(h3_addresses) self.assertEqual( len(multi_polygon[0]), 1, 'loop count matches expected' )
def get_spatial_features_hex(df, resolution=6, use_cache=True): print('Now creating spatial features') cache_path = os.path.join(CACHE_DIR, f'hex_spatial_df.msgpack') if os.path.exists(cache_path) and use_cache: print(f'{cache_path} exists') temp = pd.read_msgpack(cache_path) else: minlat = min(df.pickup_latitude) minlong = min(df.pickup_longitude) maxlat = max(df.pickup_latitude) maxlong = max(df.pickup_longitude) geoJson = { 'type': 'Polygon', 'coordinates': [[[minlat, minlong], [minlat, maxlong], [maxlat, maxlong], [maxlat, minlong]]] } hexagons = list(h3.polyfill(geoJson, resolution)) xy_pickup = utm.from_latlon(df.pickup_latitude.values, df.pickup_longitude.values) x_pickup = list(xy_pickup[0]) y_pickup = list(xy_pickup[1]) pickup_point = list(zip(x_pickup, y_pickup)) poly_hex = dict() for i, hex in enumerate(hexagons): polygons = h3.h3_set_to_multi_polygon([hex], geo_json=False) a = np.array(polygons[0][0]) b = utm.from_latlon(a[:, 0], a[:, 1]) poly_hex[i] = list(zip(b[0], b[1])) pick_zone = np.zeros(len(df)) - 1 for j, p in enumerate(pickup_point): point = Point(p) for i in range(len(poly_hex)): polygon = Polygon(poly_hex[i]) if polygon.contains(point): pick_zone[j] = int(i) break df['pickup_zone'] = pick_zone grouped_tmp = df[[ 'driver_id', 'pickup_zone', 'pickup_latitude' ]].groupby(['driver_id', 'pickup_zone']).count() / df[[ 'driver_id', 'pickup_zone', 'pickup_latitude' ]].groupby(['driver_id'])[['pickup_latitude']].count() temp = grouped_tmp.unstack(level=0).T temp.fillna(0, inplace=True) temp.reset_index(inplace=True) temp.drop(columns=['level_0'], inplace=True) pd.to_msgpack(cache_path, temp) print(f'Dumping to {cache_path}') return temp
def test_h3_set_to_multi_polygon_single_geo_json(self): h3_addresses = ['89283082837ffff'] multi_polygon = h3.h3_set_to_multi_polygon(h3_addresses, True) vertices = h3.h3_to_geo_boundary(h3_addresses[0], True) # As above, could require an order update later on expected = [[[ vertices[2], vertices[3], vertices[4], vertices[5], vertices[0], vertices[1], vertices[2] ]]] self.assertEqual(multi_polygon, expected, 'outline matches expected')
def test_h3_set_to_multi_polygon_single(self): h3_addresses = ['89283082837ffff'] multi_polygon = h3.h3_set_to_multi_polygon(h3_addresses) vertices = h3.h3_to_geo_boundary(h3_addresses[0]) # This is tricky, because output in an order starting from any vertex # would also be correct, but that's difficult to assert and there's # value in being specific here expected = [[[ vertices[2], vertices[3], vertices[4], vertices[5], vertices[0], vertices[1] ]]] self.assertEqual(multi_polygon, expected, 'outline matches expected')
def test_h3_set_to_multi_polygon_non_contiguous(self): # the second hexagon does not touch the first h3_addresses = ['89283082837ffff', '8928308280fffff'] multi_polygon = h3.h3_set_to_multi_polygon(h3_addresses) self.assertEqual(len(multi_polygon), 2, 'polygon count matches expected') self.assertEqual(len(multi_polygon[0]), 1, 'loop count matches expected') self.assertEqual(len(multi_polygon[0][0]), 6, 'coord count 1 matches expected') self.assertEqual(len(multi_polygon[1][0]), 6, 'coord count 2 matches expected')
def test_h3_set_to_multi_polygon_non_contiguous(self): # the second hexagon does not touch the first h3_addresses = ['89283082837ffff', '8928308280fffff'] multi_polygon = h3.h3_set_to_multi_polygon(h3_addresses) # TODO: Update to appropriate expectations when the algorithm correctly # returns two polygons self.assertEqual(len(multi_polygon), 1, 'polygon count matches expected') self.assertEqual(len(multi_polygon[0]), 2, 'loop count matches expected') self.assertEqual(len(multi_polygon[0][0]), 6, 'coord count 1 matches expected') self.assertEqual(len(multi_polygon[0][1]), 6, 'coord count 2 matches expected')
def test_h3_set_to_multi_polygon_single(self): h3_addresses = ['89283082837ffff'] multi_polygon = h3.h3_set_to_multi_polygon(h3_addresses) vertices = h3.h3_to_geo_boundary(h3_addresses[0]) # We shift the expected circular list so that it starts from multi_polygon[0][0][0], # since output starting from any vertex would be correct as long as it's in order. expected_coords = self.shift_circular_list(multi_polygon[0][0][0], [ vertices[2], vertices[3], vertices[4], vertices[5], vertices[0], vertices[1] ]) expected = [[expected_coords]] self.assertEqual(multi_polygon, expected, 'outline matches expected')
def test_h3_set_to_multi_polygon_hole(self): # Six hexagons in a ring around a hole h3_addresses = [ '892830828c7ffff', '892830828d7ffff', '8928308289bffff', '89283082813ffff', '8928308288fffff', '89283082883ffff' ] multi_polygon = h3.h3_set_to_multi_polygon(h3_addresses) self.assertEqual(len(multi_polygon), 1, 'polygon count matches expected') self.assertEqual(len(multi_polygon[0]), 2, 'loop count matches expected') self.assertEqual(len(multi_polygon[0][0]), 6 * 3, 'outer coord count matches expected') self.assertEqual(len(multi_polygon[0][1]), 6, 'inner coord count matches expected')
def test_h3_set_to_multi_polygon_contiguous(self): # the second hexagon shares v0 and v1 with the first h3_addresses = ['89283082837ffff', '89283082833ffff'] multi_polygon = h3.h3_set_to_multi_polygon(h3_addresses) vertices0 = h3.h3_to_geo_boundary(h3_addresses[0]) vertices1 = h3.h3_to_geo_boundary(h3_addresses[1]) # As above: This test is brittle but worthwhile; it's possible we'll # need to update to a new start vertex if the algo is changed expected = [[[ vertices1[0], vertices1[1], vertices1[2], vertices0[1], vertices0[2], vertices0[3], vertices0[4], vertices0[5], vertices1[4], vertices1[5], ]]] self.assertEqual(multi_polygon, expected, 'outline matches expected')
def map_interacted(event): if event["type"] == "change" and event["name"] == "bounds": self.ht.value = f"{self.tile_id}, Map zoom: {int(a_map.zoom)}" self.slider.max = int(a_map.zoom) + self._max_zoom_delta m = event["owner"] b_poly = list(m.bounds_polygon) b_poly += [tuple(b_poly[0])] # m += Polyline(locations=b_poly) poly = geojson.Polygon(coordinates=[[(p[0], p[1]) for p in b_poly]]) hexagons = list(h3.polyfill(dict(poly), self.slider.value)) fc = geojson.FeatureCollection(features=[ geojson.Polygon(coordinates=h3.h3_set_to_multi_polygon( [h], geo_json=True)[0], id=h) for h in hexagons ]) self.gj.data = fc # Ipyleaflet buglet(?): This name is updated in the GeoJSON layer, # but not in the LayersControl! self.gj.name = f"H3" # level {self.level}" self.gj.on_hover(hover)
def create_hexgrid(polygon, hex_res, geometry_col='geometry', buffer=0.000): """ Takes in a geopandas geodataframe, the desired resolution, the specified geometry column and some map parameters to create a hexagon grid (and potentially plot the hexgrid Arguments: polygon {geopandas.geoDataFrame} -- geoDataFrame to be used hex_res {int} -- Resolution to use Keyword Arguments: geometry_col {str} -- column in the geoDataFrame that contains the geometry (default: {'geometry'}) buffer {float} -- buffer to be used (default: {0.000}) Returns: geopandas.geoDataFrame -- geoDataFrame with the hexbins and the hex_id_{resolution} column """ centroid = list(polygon.centroid.values[0].coords)[0] # Explode multipolygon into individual polygons exploded = polygon.explode().reset_index(drop=True) # Master lists for geodataframe hexagon_polygon_list = [] hexagon_geohash_list = [] # For each exploded polygon for poly in exploded[geometry_col].values: # Reverse coords for original polygon reversed_coords = [[i[1], i[0]] for i in list(poly.exterior.coords)] # Reverse coords for buffered polygon buffer_poly = poly.buffer(buffer) reversed_buffer_coords = [[i[1], i[0]] for i in list(buffer_poly.exterior.coords)] # Format input to the way H3 expects it aoi_input = { 'type': 'Polygon', 'coordinates': [reversed_buffer_coords] } # Generate list geohashes filling the AOI geohashes = list(h3.polyfill(aoi_input, hex_res)) for geohash in geohashes: polygons = h3.h3_set_to_multi_polygon([geohash], geo_json=True) outlines = [loop for polygon in polygons for loop in polygon] polyline_geojson = [ outline + [outline[0]] for outline in outlines ][0] hexagon_polygon_list.append( shapely.geometry.Polygon(polyline_geojson)) hexagon_geohash_list.append(geohash) # Create a geodataframe containing the hexagon geometries and hashes hexgrid_gdf = gpd.GeoDataFrame() hexgrid_gdf['geometry'] = hexagon_polygon_list id_col_name = 'hex_id_' + str(hex_res) hexgrid_gdf[id_col_name] = hexagon_geohash_list hexgrid_gdf.crs = {'init': 'epsg:4326'} # Drop duplicate geometries geoms_wkb = hexgrid_gdf["geometry"].apply(lambda geom: geom.wkb) hexgrid_gdf = hexgrid_gdf.loc[geoms_wkb.drop_duplicates().index] return hexgrid_gdf
def create_hexgrid(polygon, hex_res, geometry_col='geometry', map_zoom=12, buffer=0.000, stroke_weight=0.5, stroke_color='blue', plot=True): """ Takes in a geopandas geodataframe, the desired resolution, the specified geometry column and some map parameters to create a hexagon grid (and potentially plot the hexgrid """ centroid = list(polygon.centroid.values[0].coords)[0] fol_map = folium.Map(location=[centroid[1], centroid[0]], zoom_start=map_zoom, tiles='cartodbpositron') # Explode multipolygon into individual polygons exploded = polygon.explode().reset_index(drop=True) # Master lists for geodataframe hexagon_polygon_list = [] hexagon_geohash_list = [] # For each exploded polygon for poly in exploded[geometry_col].values: # Reverse coords for original polygon coords = list(poly.exterior.coords) reversed_coords = [] for i in coords: reversed_coords.append([i[1], i[0]]) # Reverse coords for buffered polygon buffer_poly = poly.buffer(buffer) buffer_coords = list(buffer_poly.exterior.coords) reversed_buffer_coords = [] for i in buffer_coords: reversed_buffer_coords.append([i[1], i[0]]) # Format input to the way H3 expects it aoi_input = { 'type': 'Polygon', 'coordinates': [reversed_buffer_coords] } # Add polygon outline to map outline = reversed_coords outline.append(outline[0]) outline = folium.PolyLine(locations=outline, weight=1, color='black') fol_map.add_child(outline) # Generate list geohashes filling the AOI geohashes = list(h3.polyfill(aoi_input, hex_res)) # Generate hexagon polylines for Folium polylines = [] lat = [] lng = [] for geohash in geohashes: polygons = h3.h3_set_to_multi_polygon([geohash], geo_json=False) outlines = [loop for polygon in polygons for loop in polygon] polyline = [outline + [outline[0]] for outline in outlines][0] lat.extend(map(lambda x: x[0], polyline)) lng.extend(map(lambda x: x[1], polyline)) polylines.append(polyline) hexagon_geohash_list.append(geohash) # Add the hexagon polylines to Folium map for polyline in polylines: my_polyline = folium.PolyLine(locations=polyline, weight=stroke_weight, color=stroke_color) fol_map.add_child(my_polyline) # Generate hexagon polygons for Shapely for geohash in geohashes: polygons = h3.h3_set_to_multi_polygon([geohash], geo_json=True) outlines = [loop for polygon in polygons for loop in polygon] polyline_geojson = [ outline + [outline[0]] for outline in outlines ][0] hexagon_polygon_list.append( shapely.geometry.Polygon(polyline_geojson)) if plot: display(fol_map) # Create a geodataframe containing the hexagon geometries and hashes hexgrid_gdf = gpd.GeoDataFrame() hexgrid_gdf['geometry'] = hexagon_polygon_list id_col_name = 'hex_id_' + str(hex_res) hexgrid_gdf[id_col_name] = hexagon_geohash_list hexgrid_gdf.crs = {'init': 'epsg:4326'} # Drop duplicate geometries geoms_wkb = hexgrid_gdf["geometry"].apply(lambda geom: geom.wkb) hexgrid_gdf = hexgrid_gdf.loc[geoms_wkb.drop_duplicates().index] return hexgrid_gdf
# geoJson = {'type': 'Polygon', # 'coordinates': [[[37.813318999983238, -122.4089866999972145], [ 37.7866302000007224, -122.3805436999997056 ], [37.7198061999978478, -122.3544736999993603], [ 37.7076131999975672, -122.5123436999983966 ], [37.7835871999971715, -122.5247187000021967], [37.8151571999998453, -122.4798767000009008]]] } polyline = geoJson['coordinates'][0] polyline.append(polyline[0]) lat = [p[0] for p in polyline] lng = [p[1] for p in polyline] m = folium.Map(location=[sum(lat) / len(lat), sum(lng) / len(lng)], zoom_start=13, tiles='cartodbpositron') my_PolyLine = folium.PolyLine(locations=polyline, weight=8, color="blue") m.add_child(my_PolyLine) hexagons = list(h3.polyfill(geoJson, 7)) polylines = [] lat = [] lng = [] for hex in hexagons: polygons = h3.h3_set_to_multi_polygon([hex], geo_json=False) # flatten polygons into loops. outlines = [loop for polygon in polygons for loop in polygon] polyline = [outline + [outline[0]] for outline in outlines][0] lat.extend(map(lambda v: v[0], polyline)) lng.extend(map(lambda v: v[1], polyline)) polylines.append(polyline) for polyline in polylines: my_PolyLine = folium.PolyLine(locations=polyline, weight=8, color='red') m.add_child(my_PolyLine) display(m)
def test_h3_set_to_multi_polygon_empty(self): h3_addresses = [] multi_polygon = h3.h3_set_to_multi_polygon(h3_addresses) self.assertEqual(multi_polygon, [], 'no hexagons yields an empty array')