def seconds_to_minutes(src_raster): """Convert travel times from seconds to minutes. Parameters ---------- src_raster : str Path to raster to convert. """ log.info( f"Converting {os.path.basename(src_raster)} from seconds to minutes.") with rasterio.open(src_raster) as src: dst_profile = src.profile dst_profile.update(**default_compression(src.dtypes[0]), nodata=-9999) with TemporaryDirectory(prefix="geohealthaccess_") as tmpdir: tmpfile = os.path.join(tmpdir, "converted.tif") with rasterio.open( tmpfile, "w", **dst_profile) as dst, rasterio.open(src_raster) as src: for _, window in dst.block_windows(1): seconds = src.read(window=window, indexes=1) minutes = seconds / 60 minutes[seconds < 0] = dst_profile.get("nodata") dst.write(minutes, window=window, indexes=1) shutil.copyfile(tmpfile, src_raster)
def speed_from_landcover(src_landcover, dst_file, speeds=None, overwrite=False): """Create travel speed raster from land cover. Assign speed in km/h to each raster cell based on its land cover category. Speed values are provided from the `speeds` dictionnary. Parameters ---------- src_landcover : str Path to input land cover raster (multiband raster with one band per class, band descriptions with land cover label, and pixel values corresponding to land cover percentages. dst_file : str Path to output raster. speeds : dict, optional Speeds associated to each land cover category. overwrite : bool, optional Overwrite existing files. Returns ------- dst_file : str Path to output raster. """ log.info("Creating travel speeds raster from land cover.") if os.path.isfile(dst_file) and not overwrite: log.info(f"{os.path.basename(dst_file)} already exists. Skipping.") return dst_file if not speeds: log.info("No land cover speeds provided. Using default values.") speeds = default_landcover_speeds() with rasterio.open(src_landcover) as src: dst_profile = src.profile.copy() dst_profile.update(count=1, dtype="float32", nodata=-9999, **default_compression("float32")) with rasterio.open(src_landcover) as src, rasterio.open( dst_file, "w", **dst_profile) as dst: for _, window in dst.block_windows(1): speed = np.zeros(shape=(window.height, window.width), dtype=np.float32) for id, landcover in enumerate(src.descriptions, start=1): coverfraction = src.read(window=window, indexes=id) speed += (coverfraction / 100.0) * speeds[landcover] speed[coverfraction == src.nodata] = 0 speed[speed < 0] = -9999 dst.write(speed, window=window, indexes=1) log.info( f"Land cover travel speeds saved as `{os.path.basename(dst_file)}`.") return dst_file
def compute_friction(speed_raster, dst_file, max_time=3600, one_meter=False): """Convert speed raster to friction, i.e. time to cross a given pixel. Parameters ---------- speed_raster : str Path to speed raster, as computed by `combine_speed()`. dst_file : str Path to output raster. max_time : int, optional Max. friction value (seconds). one_meter : bool, optional Compute time to cross one meter instead of one pixel. Returns ------- dst_file : str Path to output raster. """ log.info( f"Computing friction surface from `{os.path.basename(speed_raster)}`.") with rasterio.open(speed_raster) as src: dst_profile = src.profile xres = abs(src.transform.a) dst_profile.update(dtype="float64", nodata=-9999, **default_compression("float64")) with rasterio.open(speed_raster) as src, rasterio.open( dst_file, "w", **dst_profile) as dst: for _, window in dst.block_windows(1): speed = src.read(window=window, indexes=1).astype(np.float64) speed /= 3.6 # From km/hour to m/second nonzero = speed != 0 time_to_cross = np.zeros_like(speed, dtype="float64") if one_meter: # Time to cross one meter time_to_cross[nonzero] = 1 / speed[nonzero] else: # Time to cross one pixel time_to_cross[nonzero] = xres / speed[nonzero] # Clean bad values time_to_cross[speed == 0] = -9999 time_to_cross[np.isnan(time_to_cross)] = -9999 time_to_cross[np.isinf(time_to_cross)] = -9999 time_to_cross[time_to_cross >= max_time] = -9999 dst.write(time_to_cross, window=window, indexes=1) return dst_file
def create_water_raster( osm_water, dst_file, dst_crs, dst_shape, dst_transform, include_streams=False, geom=None, overwrite=False, ): """Create a raster of surface water from OSM data. Parameters ---------- osm_water : str Path to input OSM features (.gpkg or .geojson). dst_file : str Path to output raster. dst_crs : CRS Target coordinate reference system as a rasterio CRS object. dst_shape : tuple of int Output raster shape (height, width). dst_transform : Affine Output raster transform. include_streams : bool, optional Include smallest rivers and streams. overwrite : bool, optional Overwrite existing files. """ log.info("Starting rasterization of OSM water objects.") if os.path.isfile(dst_file) and not overwrite: log.info(f"`{os.path.basename(dst_file)}` already exists. Skipping.") return if not os.path.isfile(osm_water): raise MissingDataError("OSM water data is missing.") water = gpd.read_file(osm_water) if water.crs != dst_crs: water = water.to_crs(dst_crs) water_bodies = water[water.water.isin( ("lake", "basin", "reservoir", "lagoon"))] large_rivers = water[(water.water == "river") | (water.waterway == "riverbank")] small_rivers = water[water.waterway.isin(("river", "canal"))] streams = water[water.waterway == "stream"] features = [water_bodies, large_rivers, small_rivers] if include_streams: features.append(streams) geoms = [] for objects in features: geoms += [g.__geo_interface__ for g in objects.geometry] log.info(f"Found {len(geoms)} OSM water objects.") rst = rasterize( geoms, out_shape=dst_shape, fill=0, default_value=1, transform=dst_transform, all_touched=True, dtype="uint8", ) dst_profile = rasterio.default_gtiff_profile dst_profile.update( count=1, dtype="uint8", transform=dst_transform, crs=dst_crs, height=dst_shape[0], width=dst_shape[1], nodata=255, **default_compression("uint8"), ) with rasterio.open(dst_file, "w", **dst_profile) as dst: dst.write(rst, 1) log.info(f"OSM water raster saved as `{os.path.basename(dst_file)}`.")
def speed_from_roads( src_roads, dst_file, dst_transform, dst_crs, dst_width, dst_height, src_ferry=None, speeds=None, overwrite=False, ): """Convert network geometries to a raster with travel speed as cell values. Parameters ---------- src_roads : str Path to input network geometries (with the following columns: geometry, highway, smoothness, tracktype and surface). dst_file : str Path to output raster. dst_transform : Affine Affine transform of the output raster. dst_crs : dict CRS of the output raster. dst_width : int Output raster width. dst_height : int Output raster height. src_ferry : str, optional Path to ferry routes data from OSM. speeds : dict, optional Speeds associated to each OSM tag. If not provided, default values will be used. overwrite : bool, optional Overwrite existing files. Returns ------- dst_file : str Path to output raster. """ log.info("Creating travel speeds raster from the road network.") if os.path.isfile(dst_file) and not overwrite: log.info(f"{os.path.basename(dst_file)} already exists. Skipping.") return dst_file if not speeds: log.info("No land cover speeds provided. Using default values.") speeds = default_landcover_speeds() # Raster profile dst_profile = rasterio.default_gtiff_profile dst_profile.update( count=1, transform=dst_transform, crs=dst_crs, width=dst_width, height=dst_height, dtype="float32", nodata=-9999, **default_compression("float32"), ) network = gpd.read_file(src_roads) if src_ferry: ferry = gpd.read_file(src_ferry) network = pd.concat((network, ferry)) network = network.to_crs(dst_crs) # Get shapes and speed values of road segments shapes = [] for _, row in network.iterrows(): speed = get_segment_speed(row.highway, row.tracktype, row.smoothness, row.surface, speeds) if speed: shapes.append((row.geometry.__geo_interface__, speed)) # Add ferry routes if needed if src_ferry: ferry = gpd.read_file(src_ferry) speed = speeds["route"]["ferry"] shapes += [(geom.__geo_interface__, speed) for geom in ferry.geometry] speed_raster = rasterize( shapes=shapes, out_shape=(dst_height, dst_width), transform=dst_transform, fill=0, all_touched=True, dtype="float32", ) log.info(f"{len(shapes)} transport network segments rasterized.") with rasterio.open(dst_file, "w", **dst_profile) as dst: dst.write(speed_raster, 1) log.info( f"Transport network travel speeds saved as `{os.path.basename(dst_file)}`." ) return dst_file
def rasterize_destinations( src_features, dst_file, dst_transform, dst_crs, dst_height, dst_width, overwrite=False, ): """Rasterize input destination features. Parameters ---------- src_features : str Path to input destination features (.geojson or .gpkg). dst_file : str Path to output geotiff file. dst_transform : Affine Target raster transform. dst_crs : CRS Target raster CRS. dst_height : int Target raster height. dst_width : int Target raster width. overwrite : bool, optional Overwrite existing files. Returns ------- dst_file : str Path to output raster. """ log.info( f"Rasterizing destination points from `{os.path.basename(src_features)}`." ) if os.path.isfile(dst_file) and not overwrite: log.info(f"{os.path.basename(dst_file)} already exists. Skipping.") return dst_file # Load source features as geodataframe and reproject geometries if needed features = gpd.read_file(src_features) if not features.crs: features.crs = CRS.from_epsg(4326) if features.crs != dst_crs: features = features.to_crs(dst_crs) shapes = [(g.__geo_interface__, i + 1) for i, g in enumerate(features.geometry)] raster = rasterize( shapes=shapes, transform=dst_transform, out_shape=(dst_height, dst_width), all_touched=True, fill=0, dtype="int16", ) dst_profile = rasterio.default_gtiff_profile dst_profile.update( count=1, dtype="int16", nodata=-32768, transform=dst_transform, crs=dst_crs, width=dst_width, height=dst_height, **default_compression("int16"), ) with rasterio.open(dst_file, "w", **dst_profile) as dst: dst.write(raster, 1) log.info(f"{len(shapes)} destination points rasterized.") return dst_file
def combine_speed( landcover, transport, obstacle, dst_file, mode="car", walk_basespeed=5, bike_basespeed=15, ): """Compute per-cell max. speed (km/h) depending on transport mode. Parameters ---------- landcover : str Path to land cover speed raster, as computed by `speed_from_landcover()`. transport : str Path to transport network speed raster, as computed by `speed_from_roads()`. obstacle : str Path to obstacle raster, as computed by `travel_obstacles()`. dst_file : str Path to output speed raster. mode : str, optional Transport mode: 'car', 'bike' or 'walk'. bike_basespeed : int, optional Bicycling base speed in km/h on a flat surface. Default=15. walk_basespeed : int, optional Walking base speed in km/h on a flat surface. Default=5. Returns ------- dst_file : str Path to output speed raster. """ log.info(f"Combining travel speeds rasters for `{mode}` transport mode.") with rasterio.open(landcover) as src: dst_profile = src.profile dst_profile.update(dtype="float32", nodata=-9999, **default_compression("float32")) # Check mode parameter if mode not in ("car", "bike", "walk"): raise ValueError("Unrecognized transport mode.") with rasterio.open(landcover) as srcland, rasterio.open( transport) as srcnet, rasterio.open( obstacle) as srcobs, rasterio.open(dst_file, "w", **dst_profile) as dst: for _, window in dst.block_windows(1): speed_landcover = srcland.read(window=window, indexes=1) speed_roads = srcnet.read(window=window, indexes=1) obstacle = srcobs.read(window=window, indexes=1) speed_landcover[obstacle == 1] = 0 speed = np.maximum(speed_landcover, speed_roads) on_road = speed_roads > 0 if mode == "bike": speed[on_road] = bike_basespeed if mode == "walk": speed[on_road] = walk_basespeed # Update nodata values and write block to disk speed[np.isnan(speed_landcover)] = -9999 speed[np.isnan(speed_roads)] = -9999 speed[speed < 0] = -9999 dst.write(speed.astype(np.float32), window=window, indexes=1) return dst_file
def travel_obstacles(src_water, src_slope, dst_file, max_slope=30, overwrite=False): """Compute obstacle raster from water and slope data. Positive cells in `src_water` and cells with a slope superior or equal to `max_slope` are considered obstacles. Output raster is an uint8 geotiff with 0 for non-obstacle cells, 1 for obstacle cells, and 255 for nodata. Parameters ---------- src_water : str or list of str Input water raster with positive values for water cells. If multiple rasters are provided, they are merged using a maximum aggregate function. src_slope : str Input slope raster in degrees. dst_file : str Path to output raster. max_slope : float, optional Slope threshold in degrees (default=30°). overwrite : bool, optional Overwrite existing files. Returns ------- str Path to output raster. """ log.info("Creating obstacle raster from water and slope.") if os.path.isfile(dst_file) and not overwrite: log.info(f"{os.path.basename(dst_file)} already exists. Skipping.") return dst_file # Convert src_water to a list if a single raster is provided if not isinstance(src_water, list) and not isinstance(src_water, tuple): src_water = [src_water] # Get raster profile information with rasterio.open(src_water[0]) as src: dst_profile = src.profile nrows, ncols = src.height, src.width dst_profile.update(dtype="uint8", nodata=255, **default_compression("uint8")) obstacle = np.zeros(shape=(nrows, ncols), dtype=np.uint8) for water in src_water: log.info( f"Setting water pixels from {os.path.basename(water)} as obstacles." ) with rasterio.open(water) as src: obstacle[src.read(1) > 0] = 1 with rasterio.open(src_slope) as src: log.info(f"Setting slope pixels > {max_slope}° as obstacles.") obstacle[src.read(1) >= max_slope] = 1 with rasterio.open(dst_file, "w", **dst_profile) as dst: dst.write(obstacle, 1) log.info( f"Computed obstacle raster ({np.count_nonzero(obstacle)} obstacle pixels)." ) return dst_file