We created Origin and Destination (OD) points to create shortest path in each of the combinations using the costs from ORS instead of the distance.
For the centrality, we used the Global Human Settlement Built-up volume raster to obtain a weighted OD. Those regions with a higher built-up volume had a higher number of OD. The following map shows the weighted OD based on the building density that were used to calculate the core metropolitan centrality of the AoI (Core Metropolitan Area) and to calculate the intracity connectivity (Municipality).
How to create the map that shows weighted OD sampling to calculate centrality of the road network
For the the redundancy, a component of the resiliene, we used a regular OD sampling in isochrones of 10 minutes for the hospitals equiped with ICU beds in the Area of Interest. The following map shows these Isochornes, hospitals as Point of Interest (PoI) and the regular OD samples.
How to create the map that shows regular OD sampling to calculate redundancy of the hospitals
Troubleshootin 10 There were several tables when exporting od_300_v2 to GIS from PostgreSQL database. The reason is that unlike PostgreSQL, QGIS only admit one geometry type column. We solved this checking the attribute table in QGIS and choosing the table accordingly.
5.1 Weighted OD
5.1.1 Create weighted sampling points
5.1.1.1 For the Core Metropolitan Area
We masked the global dataset GHS-Built-up volume to the area of interest named Core Metropolitan Area and took 300 samples based the building density. A higher number of points caused the ERROR: invalid memory alloc request size 2450771848, however, a possible solution is to iterate using batch queries. Although the limitation of the OD to 300 points could be an underrepresented of the phenomena, we did observed similar patterns in the centrality with visual inspections. The function spatSample from the library terra (Hijmans 2024) created the table od_300_v2 that is used to calculate the core metropolitan centrality.
[ACR-1] How to take weighted OD samples in the Core Metropolitan Area
# Import datalibrary(sf) # handle vector fileslibrary(terra) # handle raster filesbuildings <- terra::rast("/home/ricardors/agile-gscience-2024-rs-flood/data/source_data/GHS_BUILT_V_E2020_GLOBE_R2023A_4326_100_V1_0_RioGrandeDoSul.tif")pop_ghs <-300# set up number of sampling points## Select area of interest from the databasemunicipalities_ghs <- sf::read_sf(connection, DBI::Id(schema="agile_gis_2025_rs",table="municipalities_ghs"))municipalities_ghs_reproj <- municipalities_ghs |>st_transform(crs(buildings))build_cropped <-crop(buildings, municipalities_ghs_reproj)## Reproject for samplingbuild_4326 <- terra::project(build_cropped,crs(municipalities_ghs))build_masked <- terra::mask(build_4326, municipalities_ghs)## Weighted samplingset.seed(123)od <-spatSample(build_masked, pop_ghs,"weights",as.points =TRUE) |>st_as_sf() |>st_transform(4326)## Rename columnnames(od)[1] <-"building"## Upload the sampling points to the databasesf::dbWriteTable(connection, DBI::Id(schema ="agile_gis_2025_rs",table ="od_300_v2"), od)# time: 721 ms
building
id
geometry
47979
1
POINT (-50.99947 -29.92425)
26943
2
POINT (-51.14501 -29.86022)
41710
3
POINT (-51.11673 -30.0149)
18807
4
POINT (-51.13919 -30.0174)
9664
5
POINT (-51.06766 -29.94671)
5.1.1.2 For the Intracity connectivity
We iterated through each municipality masking the GHS-Built-up volume and taking 200 weighted OD samples. The list that stored all the iterations is converted into a spatial dataframe (sf).
[ACR-2] How to take weighted OD samples for each Municipality
# Load the municipalities municipalities_ghs <- sf::st_read(connection,Id(schema="agile_gis_2025_rs",table="municipalities_ghs"))# Select size of sample populationn_samples <-200# Create a list before looping for each municipality to generate sampling pointssampled_points_list_200 <-list()library(tidyverse)for (idx in1:nrow(municipalities_ghs)){ shapename <- municipalities_ghs$shapename[idx] municipality_geom <- municipalities_ghs$wkb_geometry[idx] n_od_samples <- n_samples masked_ghs_aoi <-mask(build_masked, vect(municipality_geom))od <-spatSample(masked_ghs_aoi, size = n_od_samples, method ="weights", as.points =TRUE) od_sf <-st_as_sf(od) |>st_transform(4326)# df with the point id and muncipality (shapename) od_sf <- od_sf |>mutate(id_sample =row_number(),shapename = shapename ) |>select(id_sample, shapename, geometry = geometry)# list that stores all the points sampled_points_list_200[[idx]] <- od_sf}# Create a dataframe from the previous list created during the loopsampled_points_df_200 <-bind_rows(sampled_points_list_200)# Export from R to PostgreSQLDBI::dbWriteTable(connection, Id(schema="agile_gis_2025_rs","od_200_each"),sampled_points_df_200)# time: 1468 ms
The following table shows the first 5 observations from the table od_200_each where the municipality is indicated in the field shapename.
id_sample
shapename
geometry
1
Alvorada
POINT (-51.08014 -29.98829)
2
Alvorada
POINT (-51.0685 -30.02239)
3
Alvorada
POINT (-51.08097 -30.03902)
4
Alvorada
POINT (-51.05436 -30.0149)
5
Alvorada
POINT (-51.08097 -29.99577)
5.1.2 Snap weighted sampling points
5.1.2.1 For the Core Metropolitan Area
The sampling points of the OD matrix are not necessary located on the road network, therefore it is necessary to snap these points. Previous to snap the points, we made the largest component of the road network routable. (i.e. ghs_ao_net_largest). Subsequently, we indexed the geometries and ID variables to improve computational efficiency. We calculated the Euclidean distance between each sampling point and the nodes from the road network nodes. By limiting the results by one, we selected the closest node for each sampling point.
[ACR-3] How to snap the sampling points from the OD matrix to the road network
--- (optional) Create "id" column as a pre-requisite for snapping if it is not created before.ALTERTABLE od_300_v2ADDCOLUMNidINTEGERPRIMARYKEYGENERATED ALWAYS AS IDENTITY;--- Similar to [ORN-3], we make the largest component routable--- First we cast the variables according to pgrouting requirementsALTERTABLE ghs_aoi_net_largest ALTERCOLUMN"toId"type bigint,ALTERCOLUMN"fromId"type bigint,ALTERCOLUMN"id"type bigint;--- Second we create the auxiliary table ghs_aoi_net_largest_vertices_pgrSELECT pgr_createverticesTable('ghs_aoi_net_largest',the_geom:='geom',source:='fromId',target:='toId');--- Create indexes to improve queries performance.CREATEINDEX idx_od_300_v2_id ON od_300_v2 USING btree(id);CREATEINDEX idx_od_300_v2_geom ON od_300_v2 USING gist(geometry);CREATEINDEX idx_ghs_aoi_net_largest_vertices_pgr ON ghs_aoi_net_largest_vertices_pgr USING btree(fid);--- Snap the OD to the nodes of ghs_aoi_net_largest_vertices_pgrEXPLAINANALYZECREATETABLE od_300_snapped_v2 ASSELECTDISTINCTON (net.id) pt.idAS pt_id, --- original position of the Origin-Destination net.idAS snapped_id, --- snapped position of the Origin-Destination net.the_geomFROM(SELECT*FROM od_300_v2 AS pt) as ptCROSSJOINLATERAL (SELECT*FROM ghs_aoi_net_largest_vertices_pgr AS netORDERBY net.the_geom <->pt.geometryLIMIT1) AS net;---time: 35.474 ms
5.1.2.2 For the Intracity connectivity
The OD matrix is limited to each municipality instead of the core metropolitan area. We created a numeric column named shapename_id to iterate each municipalities’ networks.
[ACR-4-1] How to snap the sampling points from the OD matrix to each municipality road network
--- Create a ID for each municipalityCREATETABLE od_200_each_group_id ASSELECT*,dense_rank() OVER (ORDERBY shapename) FROM od_200_each;---time: 13.858 ms
First, we create an empty table that contained the final columns.
[ACR-4-2] How to snap the sampling points from the OD matrix to each municipality road network
Then, we snapped the sampling points to the closest vertice of the network. The table net_largest_1_vertices_pgr was created in the loop shown in code chunk [ORN-6]. The final table table_snapped contained the sampling point weighted on the building density for each of the municipalities.
[ACR-5] How to snap all the weighted OD on each of the municipalities to calculate IC
tic() for(idx in1:9){tic(glue("run {idx}/9")) table_name_1_centrality <-glue("net_largest_{as.character(idx)}") temp_table_name <-glue("temp_centrality_{idx}") table_bidirect_id <-glue("t_bidirect_id_{idx}")dbExecute(glue_sql("DROP TABLE IF EXISTS {`temp_table_name`}", .con=connection),conn=connection) query_1_central <-glue_sql("CREATE TABLE {`temp_table_name`} AS SELECT b.id, b.geom, count(b.geom) AS centrality FROM pgr_dijkstra('SELECT id, fromid AS source, toid AS target, weight AS cost FROM {`table_name_1_centrality`}', ARRAY(SELECT snapped_id AS start_id FROM table_snapped WHERE shapename_id = {idx}::text), ARRAY(SELECT snapped_id AS end_id FROM table_snapped WHERE shapename_id = {idx}::text), directed := TRUE) j LEFT JOIN {`table_name_1_centrality`} AS b ON j.edge = b.id GROUP BY b.id, geom;", .con=connection)dbExecute(con=connection, query_1_central)toc()}toc()# Time: 53805 ms
Troubleshooting 9 The query did not find the required tables. Since this iteration required the tables net_largest_x, where x is one of the municipalities id, we observed that these tables were in the public schema. We solved this by defining the search_path or the schema.
5.2 Regular OD
The subject of the study that uses a regular OD is the hospital accessibility and their lack of redundancy. First we create the field location using isochrones generated with ORS. Then we create regular samples and finally we snap these OD to the road networks’ nodes.
5.2.1 Select Point of Interest (PoI)
We used ORS to create 10 minutes ischrones for each of our point of interest, the hospitals. Before selecting running the PostgreSQL query, we created indexes in the geometry column and id column. The first common table expressin (CTE) all_hospitals included the non-spatial information from the hospitals with their location joining both tables by the hospital code cne. Subsequently, the CTE poi_hospitals filtered all the hospitals selecting only those that intersected with the core metropolitan area. Lastly, the table ghs_aoi_net_largest_vertices_pgr created by pgrouting provided the nodes or vertices to which the hospitals are snapped.
[ACR-6] How to select the hospitals and snap them to the road network
CREATEINDEX idx_etlcnes_hospital_selected_cnes ON etlcnes_hospital_selected USING btree("CNES");CREATEINDEX idx_hospitals_cd_cnes ON hospitals USING btree(cd_cnes);EXPLAINANALYZECREATETABLE hospitals_poi AS--- Add ICU bed capacity from ETLCNES to the hospitalsWITH all_hospitals AS (SELECTDISTINCTON (cd_cnes) cd_cnes, ds_cnes, wkb_geometry AS geom,"QTLEITP1","QTLEITP2","QTLEITP3","NAT_JUR", tp_risco, nm_razao_s, nm_municipFROM hospitals AS geom_hospitalLEFTJOIN etlcnes_hospital_selected AS df_hospitalON df_hospital."CNES"=geom_hospital.cd_cnes),--- Define Point Of Interest (POI) using Area of Interest (AoI) poi_hospitals AS (SELECT h.*FROM all_hospitals AS h, core_metropolitan_area AS poiWHERE st_intersects(h.geom, poi.geom))--- Add the original geometry and the snapped positionSELECTDISTINCTON (h.cd_cnes) h.*, f.the_geom <-> h.geom AS distance, h.geom AS geom_hospital, f.the_geom AS geom_nodeFROM poi_hospitals hLEFTJOIN LATERAL(SELECTid, the_geomFROM ghs_aoi_net_largest_vertices_pgr AS netORDERBY net.the_geom <-> h.geomLIMIT1) AS f ONtrue--- Time: 814.451 ms
5.2.2 Create Isochrones from PoI
The ORS API1 created the area that is reached in 10 minutes from each of the hospitals, where we took the regular OD.
Error: Openrouteservice API request failed [3004] Parameter ‘locations=22’ is out of range. Maximum possible value is 5. The function ors_api_key registered a personal token required to run the ORS services by the API. Then, the arguments of the function ors_isochrones took the first 5 hospitals location, the range and interval of 600 seconds (10 minutes) and the output format, a sf object. The reason to repeat this process five times was that the maximum possible values for location was 5. This approach is quite rudimentary, yet it worked. The last step was to remove the “center” column that resulted on an non-valid format to copy the R object into PostgreSQL using the function st_write.
[ACR-7] How to run ORS to create isochrones for each hospital
library(sf)library(openrouteservice)# Use the free API from ORS replacing "<>" for the key value.openrouteservice::ors_api_key("<https://openrouteservice.org/dev/#/login?redirect=https://openrouteservice.org/dev/%23/api-docs>")hospitals_poi <- sf::st_read(connection, DBI::Id(schema="agile_gis_2025_rs" ,"hospitals_poi"))library(tidyverse)## Part I: PoI (1-5) tic()isochrone_part_1 <-ors_isochrones(st_coordinates(hospitals_poi$geom_node[1:5]),range =600,interval =600,output ="sf") |>mutate(ds_cnes = hospitals_poi$ds_cnes[1:5],cd_cnes = hospitals_poi$cd_cnes[1:5],id = hospitals_poi$id[1:5])## Part II: PoI (6-10)isochrone_part_2 <-ors_isochrones(st_coordinates(hospitals_poi$geom_node[6:10]),range =600,interval =600,output ="sf") |>mutate(ds_cnes = hospitals_poi$ds_cnes[6:10],cd_cnes = hospitals_poi$cd_cnes[6:10],id = hospitals_poi$id[6:10])## Part III: PoI (11-15)isochrone_part_3 <-ors_isochrones(st_coordinates(hospitals_poi$geom_node[11:15]),range =600,interval =600,output ="sf") |>mutate(ds_cnes = hospitals_poi$ds_cnes[11:15],cd_cnes = hospitals_poi$cd_cnes[11:15],id = hospitals_poi$id[11:15])## Part IV: PoI (16-20)isochrone_part_4 <-ors_isochrones(st_coordinates(hospitals_poi$geom_node[16:20]),range =600,interval =600,output ="sf") |>mutate(ds_cnes = hospitals_poi$ds_cnes[16:20],cd_cnes = hospitals_poi$cd_cnes[16:20],id = hospitals_poi$id[16:20])## Part V: PoI (20-22)isochrone_part_5 <-ors_isochrones(st_coordinates(hospitals_poi$geom_node[21:22]),range =600,interval =600,output ="sf") |>mutate(ds_cnes = hospitals_poi$ds_cnes[21:22],cd_cnes = hospitals_poi$cd_cnes[21:22],id = hospitals_poi$id[21:22])## Merge 5 partsisochrones_poi_hospitals <-rbind(isochrone_part_1, isochrone_part_2, isochrone_part_3, isochrone_part_4, isochrone_part_5 ) |>select(-center) # the variable ""center"" is a list. Writting will return no applicable method for st_write.toc()# time: 1925 ms
5.2.3 Create regular sampling points
We used a function in PostGIS to sample regularly2 the OD to assesss the hospitals accessibility and lack of redundancy. In this case, the function is limited to the Spatial Reference IDentifier (SRID) 4326. Since this SRID use degrees and their distance depends on the distance from the equator, the accuracy could be improved by using another SRID adapted to the project. We used this SRID because the accuracy between the OD were not as important as using a SRID that could be used on a global scale.
The code chunk ACR-9 snapped the OD to the closest node of the road network of each hospital isochrone. We noticed that the final number of OD may be slightly different from the rmarkdown_data of the github repository. This difference was not determinant to draw different conclusions from ours, therefore we acknowledge them and do not consider them.
[ACR-9] How to snap the regular samples to calculate lack of redundancy of hospitals.
EXPLAINANALYZECREATETABLE regular_sampling_isochrones_snapped_ghs_aoi ASSELECTDISTINCTON (net.id) net.id, ds_cnes, cd_cnes, net.the_geom AS geomFROM regular_sampling_isochrones_ghs_aoi AS ptCROSSJOIN LATERAL (SELECT*FROM ghs_aoi_net_largest_vertices_pgr AS netORDERBY net.the_geom <-> pt.geomLIMIT1) AS net; --- Time: 166.008 ms
# Origin-Destination matrix```{r}#| eval: true#| echo: false#| message: falselibrary(DBI)library(dplyr)library(RPostgres)## connect to PostgreSQLconnection <- DBI::dbConnect(RPostgres::Postgres(),user="docker",password="docker",host="localhost",dbname="gis",port=25432)od_300_v2 <- sf::st_read(connection, Id(schema="agile_gis_2025_rs",table="od_300_v2"))od_200_each <- sf::st_read(connection, Id(schema="agile_gis_2025_rs",table="od_200_each"))od_300_snapped_v2 <- sf::st_read(connection, Id(schema="agile_gis_2025_rs",table="od_300_snapped_v2"))```We created Origin and Destination (OD) points to create shortest path in each of the combinations using the costs from ORS instead of the distance. For the centrality, we used the Global Human Settlement Built-up volume raster to obtain a <span class='font-color'>**weighted OD**</span>. Those regions with a higher built-up volume had a higher number of OD. The following map shows the weighted OD based on the building density that were used to calculate the core metropolitan centrality of the AoI (Core Metropolitan Area) and to calculate the intracity connectivity (Municipality).```{=html}<iframe width= "780" height="500" src="data/media/weighted_od_v2.html" title "Weighted OD"></iframe>``````{r}#| eval: false#| echo: true#| code-summary: "How to create the map that shows weighted OD sampling to calculate centrality of the road network"library(sf)library(DBI)## Import Data from PostgreSQL databasemunicipalities_ghs <-st_read(connection, DBI::Id(schema="agile_gis_2025_rs",table="municipalities_ghs"))build_masked <- terra::rast("~/agile-gscience-2024-rs-flood/data/source_data/build_masked.tif")build_masked_na <- terra::subst(build_masked, from=0, to=NA)core_metropolitan_area <-st_read(connection, DBI::Id(schema="agile_gis_2025_rs",table="core_metropolitan_area")) od_300_snapped_v2 <-st_read( connection, DBI::Id(schema="agile_gis_2025_rs",table="od_300_snapped_v2"))## Create interactive map### styleslibrary(raster)library(leaflet)pal <-colorNumeric(c("#faebdd", "#cd1c4e", "#110c24"), values(build_masked),na.color ="transparent")pal_fct <-colorFactor(palette =c('#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00','#ffff33','#a65628', '#f781bf','#999999' ),domain = od_200_each$shapename)### mapleaflet() |>addTiles() |>addProviderTiles("CartoDB.Positron", group ="CartoDB.Positron") |>addProviderTiles("Esri.WorldImagery", group ="Esri.WorldImagery") |>addPolygons(data=municipalities_ghs,fillColor ="yellow",fillOpacity =0.05,weight =1.5,color ="red",dashArray ="1",group="Municipalities") |>addPolygons(data=core_metropolitan_area,fillColor ="yellow",fillOpacity =0.05,weight =2,color ="red",dashArray ="1",group="Core Metropolitan Area") |>addCircleMarkers(data=od_300_snapped_v2,fillColor ="yellow",fillOpacity =0.8,weight =1.5,radius=3,color ="black",dashArray ="1",group="Weighted OD - C.Metropolitan Area") |>addCircleMarkers(data=od_200_each,color =~pal_fct(shapename),fillOpacity =0.8,weight =1.5,radius=3,dashArray ="1",group="Weighted OD - Municipality") |>addRasterImage(build_masked_na,opacity=0.7,colors = pal,group="Bulding density") |>addLegend(pal = pal, values =values(build_masked),title ="Building Density (m³)") |>addLayersControl(baseGroups =c("CartoDB.Positron", "Esri.WorldImagery"),overlayGroups =c("Municipalities","Core Metropolitan Area", "Weighted OD - C.Metropolitan Area","Weighted OD - Municipality","Bulding density"),options =layersControlOptions(collapsed =FALSE) ) |>hideGroup(c("Municipalities","Core Metropolitan Area","Weighted OD - Municipality")) |>setView( lng =-51.21566 , lat =-30.04176, zoom =12 ) ```For the the redundancy, a component of the resiliene, we used a <span class='font-color'>**regular OD**</span> sampling in isochrones of 10 minutes for the hospitals equiped with ICU beds in the Area of Interest. The following map shows these Isochornes, hospitals as Point of Interest (PoI) and the regular OD samples.```{=html}<iframe width="780" height=500 src="data/media/regular_od.html" title="Regular OD"></iframe>``````{r}#| echo: true#| eval: false#| code-summary: "How to create the map that shows regular OD sampling to calculate redundancy of the hospitals"## Import data:regular_sampling_isochrones_snapped_ghs_aoi <- sf::st_read(connection, Id(schema="agile_gis_2025_rs",table="regular_sampling_isochrones_snapped_ghs_aoi"))## style:factpal <-colorFactor(topo.colors(22), isochrones_poi_hospitals$ds_cnes)## Mapleaflet() |>addTiles() |>addProviderTiles("CartoDB.Positron", group ="CartoDB.Positron") |>addProviderTiles("Esri.WorldImagery", group ="Esri.WorldImagery") |>addProviderTiles("CartoDB.DarkMatter", group ="CartoDB.DarkMatter") |>addCircleMarkers(data=hospitals_poi,fillColor ="yellow",fillOpacity =0.6,weight =3,radius =5,color ="red",dashArray ="1",group="Hospitals PoI") |>addPolygons(data=isochrones_poi_hospitals,fillOpacity =0.2,weight =2,color =~factpal(ds_cnes),dashArray ="1",group="Isochrones") |>addCircleMarkers(data=regular_sampling_isochrones_snapped_ghs_aoi,fillOpacity =0.8,weight =1.5,radius=3,color =~factpal(ds_cnes),dashArray ="1",group="Regular OD") |>addLayersControl(baseGroups =c("CartoDB.Positron", "Esri.WorldImagery","CartoDB.DarkMatter"),overlayGroups =c("Hospitals PoI","Isochrones", "Regular OD"),options =layersControlOptions(collapsed =FALSE) ) |>setView( lng =-51.01134 , lat =-29.93259, zoom =11.5 ) ```**Troubleshootin 10** There were several tables when exporting _od_300_v2_ to GIS from PostgreSQL database. The reason is that unlike PostgreSQL, QGIS only admit one geometry type column. We solved this checking the attribute table in QGIS and choosing the table accordingly. ```{r}#| eval: false#| include: false#| echo: false# Snappingod_200_each <-st_read(connection, Id(schema="agile_gis_2025_rs", table="od_200_each" ))od_200_each_group_id <-st_read(connection, Id(schema="public", table="od_200_each_group_id")) |>mutate(dense_rank =as.character(dense_rank)) ```## Weighted OD### Create weighted sampling points#### For the Core Metropolitan AreaWe masked the global dataset GHS-Built-up volume to the area of interest named Core Metropolitan Area and took 300 samples based the building density. A higher number of points caused the `ERROR: invalid memory alloc request size 2450771848`, however, a possible solution is to iterate using batch queries. Although the limitation of the OD to 300 points could be an underrepresented of the phenomena, we did observed similar patterns in the centrality with visual inspections. The function spatSample from the library terra [@terra] created the table _od_300_v2_ that is used to calculate the core metropolitan centrality.```{r}#| eval: false#| echo: true#| code-summary: "[ACR-1] How to take weighted OD samples in the Core Metropolitan Area"# Import datalibrary(sf) # handle vector fileslibrary(terra) # handle raster filesbuildings <- terra::rast("/home/ricardors/agile-gscience-2024-rs-flood/data/source_data/GHS_BUILT_V_E2020_GLOBE_R2023A_4326_100_V1_0_RioGrandeDoSul.tif")pop_ghs <-300# set up number of sampling points## Select area of interest from the databasemunicipalities_ghs <- sf::read_sf(connection, DBI::Id(schema="agile_gis_2025_rs",table="municipalities_ghs"))municipalities_ghs_reproj <- municipalities_ghs |>st_transform(crs(buildings))build_cropped <-crop(buildings, municipalities_ghs_reproj)## Reproject for samplingbuild_4326 <- terra::project(build_cropped,crs(municipalities_ghs))build_masked <- terra::mask(build_4326, municipalities_ghs)## Weighted samplingset.seed(123)od <-spatSample(build_masked, pop_ghs,"weights",as.points =TRUE) |>st_as_sf() |>st_transform(4326)## Rename columnnames(od)[1] <-"building"## Upload the sampling points to the databasesf::dbWriteTable(connection, DBI::Id(schema ="agile_gis_2025_rs",table ="od_300_v2"), od)# time: 721 ms``````{r}#| echo: false#| message: falselibrary(kableExtra)kbl(slice(od_300_v2,1:5))```#### For the Intracity connectivityWe iterated through each municipality masking the GHS-Built-up volume and taking 200 weighted OD samples. The list that stored all the iterations is converted into a spatial dataframe (sf).```{r}#| eval: false#| echo: true#| code-summary: "[ACR-2] How to take weighted OD samples for each Municipality"# Load the municipalities municipalities_ghs <- sf::st_read(connection,Id(schema="agile_gis_2025_rs",table="municipalities_ghs"))# Select size of sample populationn_samples <-200# Create a list before looping for each municipality to generate sampling pointssampled_points_list_200 <-list()library(tidyverse)for (idx in1:nrow(municipalities_ghs)){ shapename <- municipalities_ghs$shapename[idx] municipality_geom <- municipalities_ghs$wkb_geometry[idx] n_od_samples <- n_samples masked_ghs_aoi <-mask(build_masked, vect(municipality_geom))od <-spatSample(masked_ghs_aoi, size = n_od_samples, method ="weights", as.points =TRUE) od_sf <-st_as_sf(od) |>st_transform(4326)# df with the point id and muncipality (shapename) od_sf <- od_sf |>mutate(id_sample =row_number(),shapename = shapename ) |>select(id_sample, shapename, geometry = geometry)# list that stores all the points sampled_points_list_200[[idx]] <- od_sf}# Create a dataframe from the previous list created during the loopsampled_points_df_200 <-bind_rows(sampled_points_list_200)# Export from R to PostgreSQLDBI::dbWriteTable(connection, Id(schema="agile_gis_2025_rs","od_200_each"),sampled_points_df_200)# time: 1468 ms```The following table shows the first 5 observations from the table _od_200_each_ where the municipality is indicated in the field shapename.```{r}#| echo: false#| message: falselibrary(kableExtra)kbl(slice(od_200_each,1:5))```### Snap weighted sampling points#### For the Core Metropolitan AreaThe sampling points of the OD matrix are not necessary located on the road network, therefore it is necessary to snap these points. Previous to snap the points, we made the largest component of the road network routable. (i.e. _ghs_ao_net_largest_). Subsequently, we indexed the geometries and ID variables to improve computational efficiency. We calculated the Euclidean distance between each sampling point and the nodes from the road network nodes. By limiting the results by one, we selected the closest node for each sampling point.```{sql}#| eval: false#| echo: true#| code-summary: "[ACR-3] How to snap the sampling points from the OD matrix to the road network"--- (optional) Create "id" column as a pre-requisite for snapping if it is not created before.ALTERTABLE od_300_v2ADDCOLUMN id INTEGERPRIMARYKEY GENERATED ALWAYS AS IDENTITY;--- Similar to [ORN-3], we make the largest component routable--- First we cast the variables according to pgrouting requirementsALTERTABLE ghs_aoi_net_largest ALTERCOLUMN"toId"typebigint,ALTERCOLUMN"fromId"typebigint,ALTERCOLUMN"id"typebigint;--- Second we create the auxiliary table ghs_aoi_net_largest_vertices_pgrSELECT pgr_createverticesTable('ghs_aoi_net_largest',the_geom:='geom',source:='fromId',target:='toId');--- Create indexes to improve queries performance.CREATEINDEX idx_od_300_v2_id ON od_300_v2 USING btree(id);CREATEINDEX idx_od_300_v2_geom ON od_300_v2 USING gist(geometry);CREATEINDEX idx_ghs_aoi_net_largest_vertices_pgr ON ghs_aoi_net_largest_vertices_pgr USING btree(fid);--- Snap the OD to the nodes of ghs_aoi_net_largest_vertices_pgrEXPLAINANALYZECREATETABLE od_300_snapped_v2 ASSELECTDISTINCTON (net.id) pt.id AS pt_id, --- original position of the Origin-Destination net.id AS snapped_id, --- snapped position of the Origin-Destination net.the_geomFROM(SELECT*FROM od_300_v2 AS pt) as ptCROSSJOINLATERAL (SELECT*FROM ghs_aoi_net_largest_vertices_pgr AS netORDERBY net.the_geom <->pt.geometryLIMIT1) AS net;---time: 35.474 ms```#### For the Intracity connectivityThe OD matrix is limited to each municipality instead of the core metropolitan area. We created a numeric column named shapename_id to iterate each municipalities' networks.```{sql}#| eval: false#| echo: true#| code-summary: "[ACR-4-1] How to snap the sampling points from the OD matrix to each municipality road network"--- Create a ID for each municipalityCREATETABLE od_200_each_group_id ASSELECT*,dense_rank() OVER (ORDERBY shapename) FROM od_200_each;---time: 13.858 ms```First, we create an empty table that contained the final columns. ```{r}#| echo: true#| eval: false#| code-summary: "[ACR-4-2] How to snap the sampling points from the OD matrix to each municipality road network"library(glue)library(DBI)od_200_each_group_id <-st_read(connection,"od_200_each_group_id")table_snapped <-glue_sql("CREATE TABLE table_snapped (pt_id numeric, snapped_id numeric, shapename_id varchar(200), shapename varchar(200), geom geometry)", .con =connection)table_snapped_db <-dbGetQuery(conn=connection, table_snapped )```Then, we snapped the sampling points to the closest vertice of the network. The table _net_largest_1_vertices_pgr_ was created in the loop shown in code chunk [ORN-6]. The final table table_snapped contained the sampling point weighted on the building density for each of the municipalities. ```{r}#| echo: true#| eval: false#| code-summary: "[ACR-5] How to snap all the weighted OD on each of the municipalities to calculate IC"tic() for(idx in1:9){tic(glue("run {idx}/9")) table_name_1_centrality <-glue("net_largest_{as.character(idx)}") temp_table_name <-glue("temp_centrality_{idx}") table_bidirect_id <-glue("t_bidirect_id_{idx}")dbExecute(glue_sql("DROP TABLE IF EXISTS {`temp_table_name`}", .con=connection),conn=connection) query_1_central <-glue_sql("CREATE TABLE {`temp_table_name`} AS SELECT b.id, b.geom, count(b.geom) AS centrality FROM pgr_dijkstra('SELECT id, fromid AS source, toid AS target, weight AS cost FROM {`table_name_1_centrality`}', ARRAY(SELECT snapped_id AS start_id FROM table_snapped WHERE shapename_id = {idx}::text), ARRAY(SELECT snapped_id AS end_id FROM table_snapped WHERE shapename_id = {idx}::text), directed := TRUE) j LEFT JOIN {`table_name_1_centrality`} AS b ON j.edge = b.id GROUP BY b.id, geom;", .con=connection)dbExecute(con=connection, query_1_central)toc()}toc()# Time: 53805 ms``````{r}#| eval: false#| echo: falsefor (idx in1:9) { table_name_result <-glue("od_200_each_snapped_{idx}") insert_query <-glue_sql(" INSERT INTO table_snapped (pt_id, snapped_id, shapename_id, shapename, geom) SELECT pt_id, snapped_id, shapename_id, shapename, the_geom FROM {`table_name_result`}; ", .con = connection)dbExecute(conn = connection, insert_query)}```**Troubleshooting 9** The query did not find the required tables. Since this iteration required the tables net_largest_x, where x is one of the municipalities id, we observed that these tables were in the public schema. We solved this by defining the search_path or the schema. ## Regular ODThe subject of the study that uses a regular OD is the hospital accessibility and their lack of redundancy. First we create the field location using isochrones generated with ORS. Then we create regular samples and finally we snap these OD to the road networks' nodes. ### Select Point of Interest (PoI)We used ORS to create 10 minutes ischrones for each of our point of interest, the hospitals. Before selecting running the PostgreSQL query, we created indexes in the geometry column and id column. The first common table expressin (CTE) _all_hospitals_ included the non-spatial information from the hospitals with their location joining both tables by the hospital code cne. Subsequently, the CTE _poi_hospitals_ filtered all the hospitals selecting only those that intersected with the core metropolitan area. Lastly, the table _ghs_aoi_net_largest_vertices_pgr_ created by pgrouting provided the nodes or vertices to which the hospitals are snapped.```{sql}#| eval: false#| echo: true#| code-summary: "[ACR-6] How to select the hospitals and snap them to the road network"CREATEINDEX idx_etlcnes_hospital_selected_cnes ON etlcnes_hospital_selected USING btree("CNES");CREATEINDEX idx_hospitals_cd_cnes ON hospitals USING btree(cd_cnes);EXPLAINANALYZECREATETABLE hospitals_poi AS--- Add ICU bed capacity from ETLCNES to the hospitalsWITH all_hospitals AS (SELECTDISTINCTON (cd_cnes) cd_cnes, ds_cnes, wkb_geometry AS geom,"QTLEITP1","QTLEITP2","QTLEITP3","NAT_JUR", tp_risco, nm_razao_s, nm_municipFROM hospitals AS geom_hospitalLEFTJOIN etlcnes_hospital_selected AS df_hospitalON df_hospital."CNES"=geom_hospital.cd_cnes),--- Define Point Of Interest (POI) using Area of Interest (AoI) poi_hospitals AS (SELECT h.*FROM all_hospitals AS h, core_metropolitan_area AS poiWHERE st_intersects(h.geom, poi.geom))--- Add the original geometry and the snapped positionSELECTDISTINCTON (h.cd_cnes) h.*, f.the_geom <-> h.geom AS distance, h.geom AS geom_hospital, f.the_geom AS geom_nodeFROM poi_hospitals hLEFTJOIN LATERAL(SELECT id, the_geomFROM ghs_aoi_net_largest_vertices_pgr AS netORDERBY net.the_geom <-> h.geomLIMIT1) AS f ON true--- Time: 814.451 ms```### Create Isochrones from PoIThe ORS API^[[https://openrouteservice.org/dev/#/login?redirect=https://openrouteservice.org/dev/%23/api-docs](https://openrouteservice.org/dev/#/login?redirect=https://openrouteservice.org/dev/%23/api-docs)] created the area that is reached in 10 minutes from each of the hospitals, where we took the regular OD.Error: Openrouteservice API request failed[3004] Parameter 'locations=22' is out of range. Maximum possible value is 5. The function _ors_api_key_ registered a personal token required to run the ORS services by the API. Then, the arguments of the function _ors_isochrones_ took the first 5 hospitals location, the range and interval of 600 seconds (10 minutes) and the output format, a sf object. The reason to repeat this process five times was that the maximum possible values for location was 5. This approach is quite rudimentary, yet it worked. The last step was to remove the "center" column that resulted on an non-valid format to copy the R object into PostgreSQL using the function _st_write_.```{r}#| eval: false#| echo: true#| code-summary: "[ACR-7] How to run ORS to create isochrones for each hospital"library(sf)library(openrouteservice)# Use the free API from ORS replacing "<>" for the key value.openrouteservice::ors_api_key("<https://openrouteservice.org/dev/#/login?redirect=https://openrouteservice.org/dev/%23/api-docs>")hospitals_poi <- sf::st_read(connection, DBI::Id(schema="agile_gis_2025_rs" ,"hospitals_poi"))library(tidyverse)## Part I: PoI (1-5) tic()isochrone_part_1 <-ors_isochrones(st_coordinates(hospitals_poi$geom_node[1:5]),range =600,interval =600,output ="sf") |>mutate(ds_cnes = hospitals_poi$ds_cnes[1:5],cd_cnes = hospitals_poi$cd_cnes[1:5],id = hospitals_poi$id[1:5])## Part II: PoI (6-10)isochrone_part_2 <-ors_isochrones(st_coordinates(hospitals_poi$geom_node[6:10]),range =600,interval =600,output ="sf") |>mutate(ds_cnes = hospitals_poi$ds_cnes[6:10],cd_cnes = hospitals_poi$cd_cnes[6:10],id = hospitals_poi$id[6:10])## Part III: PoI (11-15)isochrone_part_3 <-ors_isochrones(st_coordinates(hospitals_poi$geom_node[11:15]),range =600,interval =600,output ="sf") |>mutate(ds_cnes = hospitals_poi$ds_cnes[11:15],cd_cnes = hospitals_poi$cd_cnes[11:15],id = hospitals_poi$id[11:15])## Part IV: PoI (16-20)isochrone_part_4 <-ors_isochrones(st_coordinates(hospitals_poi$geom_node[16:20]),range =600,interval =600,output ="sf") |>mutate(ds_cnes = hospitals_poi$ds_cnes[16:20],cd_cnes = hospitals_poi$cd_cnes[16:20],id = hospitals_poi$id[16:20])## Part V: PoI (20-22)isochrone_part_5 <-ors_isochrones(st_coordinates(hospitals_poi$geom_node[21:22]),range =600,interval =600,output ="sf") |>mutate(ds_cnes = hospitals_poi$ds_cnes[21:22],cd_cnes = hospitals_poi$cd_cnes[21:22],id = hospitals_poi$id[21:22])## Merge 5 partsisochrones_poi_hospitals <-rbind(isochrone_part_1, isochrone_part_2, isochrone_part_3, isochrone_part_4, isochrone_part_5 ) |>select(-center) # the variable ""center"" is a list. Writting will return no applicable method for st_write.toc()# time: 1925 ms```### Create regular sampling pointsWe used a function in PostGIS to sample regularly^[[https://gis.stackexchange.com/questions/4663/creating-regular-point-grid-inside-polygon-in-postgis](https://gis.stackexchange.com/questions/4663/creating-regular-point-grid-inside-polygon-in-postgis)] the OD to assesss the hospitals accessibility and lack of redundancy. In this case, the function is limited to the Spatial Reference IDentifier (SRID) 4326. Since this SRID use degrees and their distance depends on the distance from the equator, the accuracy could be improved by using another SRID adapted to the project. We used this SRID because the accuracy between the OD were not as important as using a SRID that could be used on a global scale.```{sql}#| eval: false#| echo: true#| code-summary: Regular ODCREATEORREPLACEFUNCTION I_Grid_Point_Series(geom geometry, x_side decimal, y_side decimal, spheroid booleandefault false)RETURNS SETOF geometry AS $BODY$DECLAREx_max decimal;y_max decimal;x_min decimal;y_min decimal;srid integer:=4326;input_srid integer;x_series DECIMAL;y_series DECIMAL;BEGINCASE st_srid(geom) WHEN0THEN geom := ST_SetSRID(geom, srid); RAISE NOTICE 'SRID Not Found.';ELSE RAISE NOTICE 'SRID Found.';ENDCASE;CASE spheroid WHEN false THEN RAISE NOTICE 'Spheroid False';else srid :=900913; RAISE NOTICE 'Spheroid True';ENDCASE; input_srid:=st_srid(geom); geom := st_transform(geom, srid); x_max := ST_XMax(geom); y_max := ST_YMax(geom); x_min := ST_XMin(geom); y_min := ST_YMin(geom); x_series := CEIL ( @( x_max - x_min ) / x_side); y_series := CEIL ( @( y_max - y_min ) / y_side );RETURN QUERYSELECT st_collect(st_setsrid(ST_MakePoint(x * x_side + x_min, y*y_side + y_min), srid)) FROMgenerate_series(0, x_series) as x,generate_series(0, y_series) as yWHERE st_intersects(st_setsrid(ST_MakePoint(x*x_side + x_min, y*y_side + y_min), srid), geom);END;$BODY$ LANGUAGE plpgsql IMMUTABLE STRICT;```The following code created a bounding box required to run the created function in the previous step.```{sql}#| eval: false#| echo: true#| code-summary: How to create a bounding box that entails the core metropolitan area.CREATETABLE core_metropolitan_bbox ASSELECT ST_Extent(geom)::geometry AS geomFROM core_metropolitan_area;```Based on the isochrones of the healthcare facilities created from ORS, we regularly sampled the OD separated by 0.00912º.```{sql}#| eval: false#| echo: true#| code-summary: "[ACR-8] How to create regular samples to calculate lack of redundancy of hospitals."EXPLAINANALYZECREATETABLE regular_sampling_isochrones_ghs_aoi ASWITH multipoint_regular AS(SELECT I_Grid_Point_Series(geometry, 0.00912,0.00912, false) AS geom, ds_cnes, cd_cnesFROM isochrones_poi_hospitals),point_regular AS(SELECT st_setsrid((st_dump(geom)).geom, 4326)::geometry(Point, 4326) AS geom, ds_cnes, cd_cnesFROM multipoint_regular)SELECT cd_cnes, ds_cnes, geom, row_number() over() AS idFROM point_regular; --- Time: 6083.595 ms```### Snap regular sampling pointsThe code chunk ACR-9 snapped the OD to the closest node of the road network of each hospital isochrone. We noticed that the final number of OD may be slightly different from the _rmarkdown_data_ of the github repository. This difference was not determinant to draw different conclusions from ours, therefore we acknowledge them and do not consider them.```{sql}#| eval: false#| echo: true#| code-summary: "[ACR-9] How to snap the regular samples to calculate lack of redundancy of hospitals."EXPLAINANALYZECREATETABLE regular_sampling_isochrones_snapped_ghs_aoi ASSELECTDISTINCTON (net.id) net.id, ds_cnes, cd_cnes, net.the_geom AS geomFROM regular_sampling_isochrones_ghs_aoi AS ptCROSSJOIN LATERAL (SELECT*FROM ghs_aoi_net_largest_vertices_pgr AS netORDERBY net.the_geom <-> pt.geomLIMIT1) AS net; --- Time: 166.008 ms```