diff --git a/Dockerfile b/01_get_data/Dockerfile similarity index 100% rename from Dockerfile rename to 01_get_data/Dockerfile diff --git a/build.sh b/01_get_data/build.sh similarity index 100% rename from build.sh rename to 01_get_data/build.sh diff --git a/01_get_data/main.R b/01_get_data/main.R index 9d44813..1313789 100644 --- a/01_get_data/main.R +++ b/01_get_data/main.R @@ -69,5 +69,3 @@ compile_processed_data( mostRecent = TRUE ) - -browser() diff --git a/04_travel_time/project_create.R b/04_travel_time/project_create.R index dbbf453..4bddea5 100644 --- a/04_travel_time/project_create.R +++ b/04_travel_time/project_create.R @@ -3,7 +3,10 @@ source("global.R") source("/helpers/find_inaccessmod_layer.R") source("/helpers/get_location.R") -location <- get_location() +# Get location from argument or default +location_arg <- get_arg("--location") +location <- if (!is.null(location_arg)) location_arg else get_location() + location_path <- "/data/location" project_name <- sprintf("project_gpp_%s", location) project_db <- file.path("/data/dbgrass/", project_name) @@ -18,7 +21,6 @@ dem_path <- find_inaccessmod_layer( "rDEM_pr.tif", copy = TRUE ) -browser() amGrassNS( mapset = "demo", diff --git a/04_travel_time/travel_time.R b/04_travel_time/travel_time.R index 348868a..fb71a47 100644 --- a/04_travel_time/travel_time.R +++ b/04_travel_time/travel_time.R @@ -3,16 +3,31 @@ source("/helpers/find_inaccessmod_layer.R") source("/helpers/get_location.R") source("/helpers/pop_vs_traveltime.R") -location <- get_location() + +location <- get_arg("--location", default = get_location()) +scenario <- get_arg("--scenario", default = NULL) location_path <- "/data/location" project_name <- sprintf("project_gpp_%s", location) +conf <- amAnalysisReplayParseConf( + "/data/config/default.json" +) +conf$location <- project_name +conf$mapset <- project_name + +if (isNotEmpty(scenario)) { + conf$tableScenario <- jsonlite::fromJSON(scenario) +} + output_folder <- file.path(location_path, location, "output") -output_travel_time <- file.path(output_folder, "travel_time.tif") output_nearest <- file.path(output_folder, "travel_nearest.tif") +output_travel_time <- file.path(output_folder, "travel_time.tif") +output_travel_time_wgs84 <- file.path(output_folder, "travel_time_wgs84.tif") output_pop_vs_time_data <- file.path(output_folder, "pop_vs_traveltime.csv") output_pop_vs_time_plot <- file.path(output_folder, "pop_vs_traveltime.pdf") +proj_4 <- NULL +pop_vs_time <- data.frame() dir.create(output_folder, showWarnings = FALSE, recursive = TRUE) @@ -36,18 +51,6 @@ facilities_path <- find_inaccessmod_layer( "vFacilities_pr.shp", copy = TRUE ) - -# -# Default config should be updated -# - Empty HF table (default to all, no valdiation) -# - cucrent mapset/location -# -conf <- amAnalysisReplayParseConf( - "/data/config/default.json" -) -conf$location <- project_name -conf$mapset <- project_name - # # Start AccessMod Session # @@ -59,32 +62,38 @@ amGrassNS( # - match AccessMod classes in /www/dictionary/classes.json # - use two underscore to separate tags from class, and # one between tags e.g. vFacility__test_a - - execGRASS( - "r.in.gdal", - band = 1, - input = landcover_merged_path, - output = "rLandCoverMerged__pr", - title = "rLandCoverMerged__pr", - flags = c("overwrite", "quiet") - ) - - execGRASS( - "r.in.gdal", - band = 1, - input = population_path, - output = "rPopulation__pr", - title = "rPopulation__pr", - flags = c("overwrite", "quiet") - ) - - execGRASS("v.in.ogr", - flags = c("overwrite", "w", "2"), # overwrite, lowercase, 2d only, - input = facilities_path, - key = "cat", - output = "vFacility__pr", - snap = 0.0001 - ) + if (!amRastExsit("rLandCoverMerged__pr")) { + execGRASS( + "r.in.gdal", + band = 1, + input = landcover_merged_path, + output = "rLandCoverMerged__pr", + title = "rLandCoverMerged__pr", + flags = c("overwrite", "quiet") + ) + } + + if (!amRastExsit("rPopulation__pr")) { + execGRASS( + "r.in.gdal", + band = 1, + input = population_path, + output = "rPopulation__pr", + title = "rPopulation__pr", + flags = c("overwrite", "quiet") + ) + } + + + if (!amVectExsit("rLandCoverMerged__pr")) { + execGRASS("v.in.ogr", + flags = c("overwrite", "w", "2"), # overwrite, lowercase, 2d only, + input = facilities_path, + key = "cat", + output = "vFacility__pr", + snap = 0.0001 + ) + } exportedDirs <- amAnalysisReplayExec(conf, exportDirectory = output_folder @@ -96,14 +105,62 @@ amGrassNS( # - table -> output # - plot -> output # - popVsTime <- amGetRasterStatZonal( + pop_vs_time <- amGetRasterStatZonal( "rPopulation__pr", "rTravelTime__pr" ) - write.csv(popVsTime, output_pop_vs_time_data) - plot_cumulative_sum(popVsTime, output_pop_vs_time_plot) + write.csv(pop_vs_time, output_pop_vs_time_data) + plot_cumulative_sum(pop_vs_time, output_pop_vs_time_plot) + + + # + # Export tt as tif + # + proj_4 <- paste(execGRASS("g.proj", flags = c("j"), intern = TRUE), collapse = " ") + + execGRASS("r.colors", + flags = c("n"), + map = "rTravelTime__pr", + color = "viridis" + ) + + + execGRASS("r.out.gdal", + flags = c("overwrite", "f", "m"), + input = "rTravelTime__pr", + output = output_travel_time, + format = "GTiff", + createopt = "TFW=YES" + ) + execGRASS("r.out.gdal", + flags = c("overwrite", "f", "c", "m"), + input = "rNearest__pr", + output = output_nearest, + format = "GTiff", + createopt = "TFW=YES" + ) + + tmp_tif <- raster(output_travel_time) + tmp_tif_reproj <- projectRaster(tmp_tif, crs = "+init=epsg:4326") + writeRaster( + tmp_tif_reproj, + output_travel_time_wgs84, + format = "GTiff", + overwrite = TRUE + ) } ) -print(sprintf("Data exported in %s", output_folder)) +result <- list( + pop_vs_time_data = output_pop_vs_time_data, + pop_vs_time_plot = output_pop_vs_time_plot, + travel_time = output_travel_time, + travel_time_wgs84 = output_travel_time_wgs84, + nearest = output_nearest, + proj_4 = proj_4, + thresholds = pop_vs_time$zone +) + + +print(jsonlite::toJSON(result, auto_unbox = TRUE)) diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..d24bd4c --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,19 @@ +FROM fredmoser/accessmod:5.8.3-alpha.1 + +RUN apk add --no-cache nodejs npm + +# context = parent ! +COPY helpers /helpers +COPY 04_travel_time /run +COPY api /api + + +WORKDIR /api +RUN npm install + + +VOLUME /data + +EXPOSE 3000 + +CMD ["node", "app.js"] diff --git a/api/app.js b/api/app.js new file mode 100644 index 0000000..2bb963f --- /dev/null +++ b/api/app.js @@ -0,0 +1,72 @@ +import express from "express"; +import { getListLocations, computeTravelTime } from "./helpers.js"; +import { TifContour } from "./contour.js"; +const app = express(); +const port = process.env.NODE_ENV === "development" ? 3030 : 3000; + +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +app.use(express.json()); +app.use("/data/location", express.static("/data/location")); + +app.get("/data/location/*", (req, res) => { + const filePath = path.join(__dirname, "data/location", req.params[0]); + res.sendFile(filePath, (err) => { + if (err) { + res.status(404).send(`File not found at ${filePath}`); + } + }); +}); + +// GET endpoint to list available locations +app.get("/get_list_locations", async (_, res) => { + try { + const locations = await getListLocations(); + res.json(locations); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// POST endpoint to compute travel time +app.post("/compute_travel_time", async (req, res) => { + const { location, scenario, add_contours } = req.body; + + try { + const locations = await getListLocations(); + + if (!locations.includes(location)) { + throw new Error( + `Location ${location} is not yet available, come back later` + ); + } + + const output = await computeTravelTime(location, scenario); + + if (add_contours) { + const cc = new TifContour({ + thresholds: output.thresholds, + raster: output.travel_time, + proj4: output.proj_4, + }); + output.contours = await cc.render(); + } + + res.json({ + message: "Travel time computation completed", + data: output, + }); + } catch (error) { + debugger; + console.error("Error computing travel time:", error); + res.status(500).json({ error: "Error running AccessMod scripts" }); + } +}); + +app.listen(port, () => { + console.log(`Server running at http://localhost:${port}`); +}); diff --git a/api/build.sh b/api/build.sh new file mode 100755 index 0000000..af421d8 --- /dev/null +++ b/api/build.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +docker build --push \ + --platform linux/amd64,linux/arm64 \ + -t fredmoser/accessmod_api:latest \ + -f ./Dockerfile \ + .. + diff --git a/api/compose.yml b/api/compose.yml new file mode 100644 index 0000000..1bcd458 --- /dev/null +++ b/api/compose.yml @@ -0,0 +1,55 @@ +services: + accessmod_api: + image: fredmoser/accessmod_api:latest + container_name: accessmod_api + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--spider", "http://localhost:3000/get_list_locations"] + interval: 5s + timeout: 60s + retries: 10 + start_period: 10s + ports: + - "3000:3000" + volumes: + - /home/accessmod/data:/data + networks: + - traefik-network + + traefik: + image: traefik:3.0.4 + container_name: traefik + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - traefik-volume:/etc/traefik + command: + - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:443" + - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" + - "--certificatesresolvers.letsencrypt.acme.email=${EMAIL}" + - "--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme.json" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--log.level=INFO" + - "--accesslog=true" + - "--entrypoints.web.http.redirections.entrypoint.to=websecure" + - "--entrypoints.web.http.redirections.entrypoint.scheme=https" + + labels: + - "traefik.enable=true" + - "traefik.http.routers.accessmod.rule=Host(`accessmod.mapx.org`)" + - "traefik.http.routers.accessmod.entrypoints=websecure" + - "traefik.http.routers.accessmod.tls=true" + - "traefik.http.routers.accessmod.tls.certresolver=letsencrypt" + - "traefik.http.services.accessmod.loadbalancer.server.port=3000" + +networks: + traefik-network: + external: true + +volumes: + traefik-volume: diff --git a/api/contour.js b/api/contour.js new file mode 100644 index 0000000..3743500 --- /dev/null +++ b/api/contour.js @@ -0,0 +1,123 @@ +import { contours } from "d3-contour"; +import { fromArrayBuffer } from "geotiff"; +import proj4 from "proj4"; +import { promises as fs } from "fs"; + +const def = { + proj4: null, + raster: null, + thresholds: [], +}; + +class TifContour { + constructor(config) { + this._config = Object.assign({}, def, config); + this._w = 0; + this._h = 0; + this._xMin = 0; + this._xMax = 0; + this._yMin = 0; + this._yMax = 0; + this._res = [0, 0]; + } + + get proj4() { + return this._config.proj4; + } + + get raster() { + return this._config.raster; + } + get thresholds() { + return this._config.thresholds; + } + + toArrayBuffer(buf) { + const ab = new ArrayBuffer(buf.length); + const view = new Uint8Array(ab); + for (var i = 0; i < buf.length; ++i) { + view[i] = buf[i]; + } + return ab; + } + + async render() { + const exists = await fileExists(this.raster); + if (!exists) { + throw new Error(`File ${this.raster} not found`); + } + const tiffBuff = await fs.readFile(this.raster); + const tiffABuff = this.toArrayBuffer(tiffBuff); + const tiff = await fromArrayBuffer(tiffABuff); + const img = await tiff.getImage(); + const values = (await img.readRasters())[0]; + const bbox = img.getBoundingBox(); + + this._xMin = bbox[0]; + this._yMin = bbox[1]; + this._xMax = bbox[2]; + this._yMax = bbox[3]; + + this._res = img.getResolution(); + this._res_y = Math.abs(this._res[1]); + this._res_x = Math.abs(this._res[0]); + this._w = img.getWidth(); + this._h = img.getHeight(); + + this._dx = this._xMax - this._xMin; + this._dy = this._yMax - this._yMin; + + const contoursGenerator = contours() + .size([this._w, this._h]) + .smooth(true) + .thresholds(this.thresholds); + + const geoms = contoursGenerator(values); + const features = geoms.map((geom, i) => + this.createFeature(geom, this.thresholds[i]) + ); + return { + type: "FeatureCollection", + features: features, + }; + } + + createFeature(geom, threshold) { + const coords = geom.coordinates.map((group) => + group.map((coord) => + coord.map((c) => this.reprojectPoint(this.pixToCoord(c[0], c[1]))) + ) + ); + + return { + type: "Feature", + properties: { value: threshold }, + geometry: { ...geom, coordinates: coords }, + }; + } + + pixToCoord(x, y) { + const lng = x * this._res_x + this._xMin; + const lat = this._yMax - y * this._res_y; // Adjusted to start from _yMax + return [lng, lat]; + } + + reprojectPoint(point) { + return proj4(this.proj4, "EPSG:4326", point); + } +} + +export { TifContour }; + +async function fileExists(filePath) { + try { + await fs.access(filePath); + return true; // File exists + } catch (error) { + if (error.code === "ENOENT") { + return false; // File does not exist + } else { + throw error; // An unexpected error occurred + } + } +} diff --git a/api/docker-compose.yml b/api/docker-compose.yml new file mode 100644 index 0000000..c90641e --- /dev/null +++ b/api/docker-compose.yml @@ -0,0 +1,31 @@ +services: + accessmod-api: + image: fredmoser/accessmod_api:latest + build: + context: ../ + dockerfile: ./api/Dockerfile + platforms: + - linux/amd64 + - linux/arm64 + ports: + - "3000:3000" + volumes: + - .:/api + - ../shared:/data + - ../04_travel_time:/run + accessmod-api-dev: + image: fredmoser/accessmod_api:latest + build: + context: ../ + dockerfile: ./api/Dockerfile + ports: + - "3030:3030" + volumes: + - .:/api + - ../shared:/data + - ../04_travel_time:/run + environment: + - NODE_ENV=development + command: ["tail", "-f", "/dev/null"] + + diff --git a/api/helpers.js b/api/helpers.js new file mode 100644 index 0000000..0fa48b0 --- /dev/null +++ b/api/helpers.js @@ -0,0 +1,77 @@ +import { spawn } from "child_process"; +import { promises as fs } from "fs"; + +//import path from "path"; +/*import { fileURLToPath } from "url";*/ + +/*const __filename = fileURLToPath(import.meta.url);*/ +/*const __dirname = path.dirname(__filename);*/ + +// Helper function to compute travel time +export async function computeTravelTime(location, scenario) { + try { + const scenarioArg = JSON.stringify(scenario); + + // Run project_create.R + //await runRScript("/run/project_create.R", ["--location", location], "/app"); + + const tt_args = ["--location", location]; + + if (scenarioArg) { + tt_args.push(...["--scenario", scenarioArg]); + } + + const result = await runRScript("/run/travel_time.R", tt_args, "/app"); + return result[0]; + } catch (error) { + console.error("Error running R scripts:", error); + throw error; + } +} +// Helper function to list locations +export async function getListLocations() { + try { + const locationPath = "/data/location"; + const directories = await fs.readdir(locationPath, { withFileTypes: true }); + const locations = directories + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name); + return locations; + } catch (error) { + console.error("Error reading locations:", error); + throw new Error("Unable to retrieve locations"); + } +} + +// Helper function to run R scripts +function runRScript(scriptPath, args, wd) { + return new Promise((resolve, reject) => { + const process = spawn("Rscript", [scriptPath, ...args], { cwd: wd }); + + let stdout = ""; + let stderr = ""; + + process.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + process.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + process.on("close", (code) => { + if (code !== 0) { + reject(new Error(`R script exited with code ${code}\n${stderr}`)); + } else { + const res = stdout.match(/\{(?:[^{}]|"[^"]*"|')*\}/g); + + if (res) { + return resolve(res.map(JSON.parse)); + } + + resolve([stdout]); + } + }); + }); +} + diff --git a/api/package.json b/api/package.json new file mode 100644 index 0000000..1a4f406 --- /dev/null +++ b/api/package.json @@ -0,0 +1,27 @@ +{ + "name": "accessmod-api", + "version": "0.0.1", + "description": "AccessMod API (prototype)", + "main": "app.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/unige-geohealth/accessmod_gpp/api" + }, + "keywords": [ + "accessmod", + "gpp", + "cities" + ], + "author": "F. Moser", + "license": "MIT", + "dependencies": { + "d3-contour": "^4.0.2", + "express": "^4.19.2", + "geotiff": "^2.1.3", + "proj4": "^2.11.0" + } +} diff --git a/api/test_curl.sh b/api/test_curl.sh new file mode 100755 index 0000000..213b2e7 --- /dev/null +++ b/api/test_curl.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +curl -X POST http://localhost:3030/compute_travel_time -H "Content-Type: application/json" -d '{"location":"Dubai"}' diff --git a/helpers/get_location.R b/helpers/get_location.R index 0a42586..d3c7230 100644 --- a/helpers/get_location.R +++ b/helpers/get_location.R @@ -14,3 +14,14 @@ get_location <- function() { return(gpp_location) } + +# Function to get command line arguments +get_arg <- function(arg_name, default = NULL) { + args <- commandArgs(trailingOnly = TRUE) + arg_index <- match(arg_name, args) + if (!is.na(arg_index) && arg_index < length(args)) { + return(args[arg_index + 1]) + } + return(default) +} + diff --git a/shared/config/default.json b/shared/config/default.json index 7e7f05d..8f5ae9b 100644 --- a/shared/config/default.json +++ b/shared/config/default.json @@ -83,143 +83,43 @@ "outputTravelTime": "rTravelTime__pr", "outputNearest": "rNearest__pr", "typeAnalysis": "anisotropic", - "knightMove": false, + "knightMove": true, "addNearest": true, "joinField": "cat", "towardsFacilities": true, - "maxTravelTime": 120, + "maxTravelTime": 60, "useMaxSpeedMask": false, "timeoutValue": -1, "tableScenario": [ - { "class": 30, "label": "no_label_30", "speed": 5, "mode": "WALKING" }, - { "class": 40, "label": "no_label_40", "speed": 5, "mode": "WALKING" }, - { "class": 50, "label": "no_label_50", "speed": 5, "mode": "WALKING" }, - { "class": 111, "label": "no_label_111", "speed": 5, "mode": "WALKING" }, - { "class": 114, "label": "no_label_114", "speed": 5, "mode": "WALKING" }, - { "class": 115, "label": "no_label_115", "speed": 5, "mode": "WALKING" }, - { "class": 125, "label": "no_label_125", "speed": 5, "mode": "WALKING" }, - { "class": 126, "label": "no_label_126", "speed": 5, "mode": "WALKING" }, - { - "class": 1001, - "label": "no_label_1001", - "speed": 5, - "mode": "WALKING" - }, - { - "class": 1002, - "label": "no_label_1002", - "speed": 5, - "mode": "WALKING" - }, - { - "class": 1003, - "label": "no_label_1003", - "speed": 5, - "mode": "WALKING" - }, - { - "class": 1004, - "label": "no_label_1004", - "speed": 5, - "mode": "WALKING" - }, - { - "class": 1005, - "label": "no_label_1005", - "speed": 5, - "mode": "WALKING" - }, - { - "class": 1006, - "label": "no_label_1006", - "speed": 5, - "mode": "WALKING" - }, - { - "class": 1007, - "label": "no_label_1007", - "speed": 5, - "mode": "WALKING" - }, - { - "class": 1008, - "label": "no_label_1008", - "speed": 5, - "mode": "WALKING" - }, - { - "class": 1009, - "label": "no_label_1009", - "speed": 5, - "mode": "WALKING" - }, - { - "class": 1010, - "label": "no_label_1010", - "speed": 5, - "mode": "WALKING" - }, - { - "class": 1013, - "label": "no_label_1013", - "speed": 5, - "mode": "WALKING" - }, - { - "class": 1014, - "label": "no_label_1014", - "speed": 5, - "mode": "WALKING" - }, - { - "class": 1015, - "label": "no_label_1015", - "speed": 5, - "mode": "WALKING" - }, - { - "class": 1016, - "label": "no_label_1016", - "speed": 5, - "mode": "WALKING" - }, - { - "class": 1017, - "label": "no_label_1017", - "speed": 5, - "mode": "WALKING" - }, - { - "class": 1018, - "label": "no_label_1018", - "speed": 5, - "mode": "WALKING" - }, - { - "class": 1019, - "label": "no_label_1019", - "speed": 5, - "mode": "WALKING" - }, - { - "class": 1021, - "label": "no_label_1021", - "speed": 5, - "mode": "WALKING" - }, - { - "class": 1022, - "label": "no_label_1022", - "speed": 5, - "mode": "WALKING" - }, - { - "class": 1023, - "label": "no_label_1023", - "speed": 5, - "mode": "WALKING" - }, - { "class": 1024, "label": "no_label_1024", "speed": 5, "mode": "WALKING" } + { "class": 30, "label": "herbaceous vegetation", "speed": 3, "mode": "WALKING" }, + { "class": 40, "label": "cultivated/agriculture", "speed": 4, "mode": "WALKING" }, + { "class": 50, "label": "urban/built up", "speed": 5, "mode": "WALKING" }, + { "class": 111, "label": "closed forest, evergreen needle leaf", "speed": 2, "mode": "WALKING" }, + { "class": 114, "label": "closed forest, deciduous broad leaf", "speed": 2, "mode": "WALKING" }, + { "class": 115, "label": "closed forest, mixed", "speed": 2, "mode": "WALKING" }, + { "class": 125, "label": "open forest, mixed", "speed": 3, "mode": "WALKING" }, + { "class": 126, "label": "open forest, not matching other definitions", "speed": 3, "mode": "WALKING" }, + { "class": 1001, "label": "trunk", "speed": 80, "mode": "MOTORIZED" }, + { "class": 1002, "label": "trunk_link", "speed": 60, "mode": "MOTORIZED" }, + { "class": 1003, "label": "primary", "speed": 70, "mode": "MOTORIZED" }, + { "class": 1004, "label": "primary_link", "speed": 50, "mode": "MOTORIZED" }, + { "class": 1005, "label": "motorway", "speed": 120, "mode": "MOTORIZED" }, + { "class": 1006, "label": "motorway_link", "speed": 80, "mode": "MOTORIZED" }, + { "class": 1007, "label": "secondary", "speed": 60, "mode": "MOTORIZED" }, + { "class": 1008, "label": "secondary_link", "speed": 40, "mode": "MOTORIZED" }, + { "class": 1009, "label": "tertiary", "speed": 50, "mode": "MOTORIZED" }, + { "class": 1010, "label": "tertiary_link", "speed": 30, "mode": "MOTORIZED" }, + { "class": 1013, "label": "residential", "speed": 30, "mode": "MOTORIZED" }, + { "class": 1014, "label": "living_street", "speed": 20, "mode": "MOTORIZED" }, + { "class": 1015, "label": "service", "speed": 30, "mode": "MOTORIZED" }, + { "class": 1016, "label": "track", "speed": 20, "mode": "BICYCLING" }, + { "class": 1017, "label": "pedestrian", "speed": 5, "mode": "WALKING" }, + { "class": 1018, "label": "path", "speed": 10, "mode": "BICYCLING" }, + { "class": 1019, "label": "footway", "speed": 5, "mode": "WALKING" }, + { "class": 1021, "label": "bridleway", "speed": 15, "mode": "BICYCLING" }, + { "class": 1022, "label": "cycleway", "speed": 20, "mode": "BICYCLING" }, + { "class": 1023, "label": "steps", "speed": 2, "mode": "WALKING" }, + { "class": 1024, "label": "unclassified", "speed": 40, "mode": "MOTORIZED" } ], "tableFacilities": [], "roundingMethod": "ceil" diff --git a/shared/sync.sh b/shared/sync.sh new file mode 100755 index 0000000..91297ac --- /dev/null +++ b/shared/sync.sh @@ -0,0 +1,27 @@ +#!/bin/bash + + +# Set source and destination +SRC_DIR="." +REMOTE_SERVER="root@srv560729.hstgr.cloud" +REMOTE_DEST="/data/" + +# Run rsync with specific include and exclude patterns +rsync -avz --delete \ + --include='config/' \ + --include='config/**' \ + --include='location/' \ + --include='location/*/' \ + --include='location/*/data/' \ + --include='location/*/data/zToAccessMod/' \ + --include='location/*/data/zToAccessMod/**' \ + --include='location/*/facilities/' \ + --include='location/*/facilities/**' \ + --include='location/*/landcover/' \ + --include='location/*/landcover/**' \ + --include='location/*/output/' \ + --include='location/*/output/**' \ + --exclude='*' \ + "$SRC_DIR" "${REMOTE_SERVER}:${REMOTE_DEST}" + +echo "Rsync completed."