├── .dockerignore ├── .github └── workflows │ └── build.yml ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── build.sh ├── client ├── README.md ├── dev │ └── src │ │ ├── build.clj │ │ ├── build │ │ └── sass.clj │ │ └── dev │ │ └── user.cljs ├── package-lock.json ├── package.json ├── resources │ ├── css │ │ ├── data-table.css │ │ └── mapbox.css │ ├── js │ │ └── externs.js │ └── sass │ │ ├── _global.scss │ │ ├── _grid.scss │ │ ├── _layout.scss │ │ ├── _reset.scss │ │ ├── _responsive.scss │ │ ├── _variables.scss │ │ ├── landing.scss │ │ └── site2.scss ├── shadow-cljs.edn ├── src │ ├── leaflet │ │ ├── controls.cljs │ │ ├── core.cljs │ │ └── layers.cljs │ └── planwise │ │ └── client │ │ ├── api.cljs │ │ ├── asdf.cljs │ │ ├── components │ │ ├── common.cljs │ │ └── common2.cljs │ │ ├── config.cljs │ │ ├── core.cljs │ │ ├── coverage.cljs │ │ ├── db.cljs │ │ ├── effects.cljs │ │ ├── handlers.cljs │ │ ├── mapping.cljs │ │ ├── modal │ │ ├── modal.cljs │ │ └── modal.svg │ │ ├── projects2 │ │ ├── api.cljs │ │ ├── components │ │ │ ├── common.cljs │ │ │ ├── create.cljs │ │ │ ├── dashboard.cljs │ │ │ ├── listings.cljs │ │ │ └── settings.cljs │ │ ├── core.cljs │ │ ├── db.cljs │ │ ├── handlers.cljs │ │ ├── subs.cljs │ │ └── views.cljs │ │ ├── providers_set │ │ ├── api.cljs │ │ ├── components │ │ │ ├── dropdown.cljs │ │ │ └── new_provider_set.cljs │ │ ├── db.cljs │ │ ├── handlers.cljs │ │ ├── subs.cljs │ │ └── views.cljs │ │ ├── regions │ │ ├── api.cljs │ │ ├── handlers.cljs │ │ └── subs.cljs │ │ ├── routes.cljs │ │ ├── scenarios │ │ ├── api.cljs │ │ ├── changeset.cljs │ │ ├── db.cljs │ │ ├── edit.cljs │ │ ├── handlers.cljs │ │ ├── subs.cljs │ │ └── views.cljs │ │ ├── sources │ │ ├── api.cljs │ │ ├── components │ │ │ └── dropdown.cljs │ │ ├── db.cljs │ │ ├── handlers.cljs │ │ ├── sources.svg │ │ ├── subs.cljs │ │ └── views.cljs │ │ ├── styles.cljs │ │ ├── subs.cljs │ │ ├── ui │ │ ├── common.cljs │ │ ├── dialog.cljs │ │ ├── filter_select.cljs │ │ ├── macros.clj │ │ └── rmwc.cljs │ │ ├── utils.cljs │ │ └── views.cljs └── test │ └── planwise │ └── client │ ├── scenarios_test.cljs │ └── utils_test.cljs ├── common ├── README.md └── src │ └── planwise │ ├── common.cljc │ └── model │ ├── coverage.cljc │ └── project.cljc ├── cpp ├── .gitignore ├── CMakeLists.txt ├── aggregate-population ├── aggregate-population.cpp ├── bin-trampoline.sh ├── walking-coverage └── walking-coverage.cpp ├── data ├── populations │ └── .gitkeep ├── regions │ └── .gitkeep └── scenarios │ └── .gitkeep ├── dev ├── resources │ └── dev.edn └── src │ ├── dev.clj │ ├── planwise │ ├── repl.clj │ └── virgil.clj │ └── user.clj ├── doc ├── coverage_algo.md └── polygon_clipping_algorithms.md ├── docker-cloud.sample.yml ├── docker-compose.ci.yml ├── docker-compose.production.yml ├── docker-compose.yml ├── docker-env ├── env ├── java └── planwise │ └── engine │ └── Algorithm.java ├── mapserver ├── .dockerignore ├── .gitignore ├── Dockerfile.mapcache ├── Dockerfile.mapserver ├── README.md ├── docker-compose.yml ├── mapcache.xml ├── partition.py └── planwise.map ├── project.clj ├── resources ├── migrations │ ├── 001-extensions.up.sql │ ├── 002-ways_nodes.down.sql │ ├── 002-ways_nodes.up.sql │ ├── 003-facilities.down.sql │ ├── 003-facilities.up.sql │ ├── 004-preprocess_functions.down.sql │ ├── 004-preprocess_functions.up.sql │ ├── 005-projects.down.sql │ ├── 005-projects.up.sql │ ├── 006-facilities_type_column.down.sql │ ├── 006-facilities_type_column.up.sql │ ├── 007-users.down.sql │ ├── 007-users.up.sql │ ├── 008-regions.down.sql │ ├── 008-regions.up.sql │ ├── 009-add_region_to_projects.down.sql │ ├── 009-add_region_to_projects.up.sql │ ├── 010-tokens.down.sql │ ├── 010-tokens.up.sql │ ├── 011-facility_types.down.sql │ ├── 011-facility_types.up.sql │ ├── 012-add_facilities_count_to_projects.down.sql │ ├── 012-add_facilities_count_to_projects.up.sql │ ├── 013-process_single_facility.down.sql │ ├── 013-process_single_facility.up.sql │ ├── 014-projects_filters_and_stats.down.sql │ ├── 014-projects_filters_and_stats.up.sql │ ├── 015-apply_traffic_factor.down.sql │ ├── 015-apply_traffic_factor.up.sql │ ├── 016-add_preview_geom_to_regions.down.sql │ ├── 016-add_preview_geom_to_regions.up.sql │ ├── 017-facilities_polygons_index.down.sql │ ├── 017-facilities_polygons_index.up.sql │ ├── 018-add_owner_user_to_projects.down.sql │ ├── 018-add_owner_user_to_projects.up.sql │ ├── 019-add_id_to_facilities_polygons.down.sql │ ├── 019-add_id_to_facilities_polygons.up.sql │ ├── 020-facility_polygons_regions.down.sql │ ├── 020-facility_polygons_regions.up.sql │ ├── 021-facility_polygons_data.down.sql │ ├── 021-facility_polygons_data.up.sql │ ├── 022-add_population_to_regions.down.sql │ ├── 022-add_population_to_regions.up.sql │ ├── 023-add_facilities_polygon_constraint.down.sql │ ├── 023-add_facilities_polygon_constraint.up.sql │ ├── 024-add_facilities_processing_status.down.sql │ ├── 024-add_facilities_processing_status.up.sql │ ├── 025-add_max_population_to_regions.down.sql │ ├── 025-add_max_population_to_regions.up.sql │ ├── 026-datasets.down.sql │ ├── 026-datasets.up.sql │ ├── 027-dataset_references.down.sql │ ├── 027-dataset_references.up.sql │ ├── 028-project_share.down.sql │ ├── 028-project_share.up.sql │ ├── 029-add_raster_pixel_area_to_regions.down.sql │ ├── 029-add_raster_pixel_area_to_regions.up.sql │ ├── 030-facilities-type-id-contraint.down.sql │ ├── 030-facilities-type-id-contraint.up.sql │ ├── 031-add_import_job_to_datasets.down.sql │ ├── 031-add_import_job_to_datasets.up.sql │ ├── 032-add_projects_state.down.sql │ ├── 032-add_projects_state.up.sql │ ├── 033-add_capacity_to_facilities.down.sql │ ├── 033-add_capacity_to_facilities.up.sql │ ├── 034-add_code_to_facility_types.down.sql │ ├── 034-add_code_to_facility_types.up.sql │ ├── 035-datasets2.down.sql │ ├── 035-datasets2.up.sql │ ├── 036-projects2.down.sql │ ├── 036-projects2.up.sql │ ├── 037-add-config-to-projects2.down.sql │ ├── 037-add-config-to-projects2.up.sql │ ├── 038-add_coverage_to_datasets2.down.sql │ ├── 038-add_coverage_to_datasets2.up.sql │ ├── 039-sites2_coverage.down.sql │ ├── 039-sites2_coverage.up.sql │ ├── 040-population_sources.down.sql │ ├── 040-population_sources.up.sql │ ├── 041-populations.down.sql │ ├── 041-populations.up.sql │ ├── 042-add-dataset-id-to-projects2.down.sql │ ├── 042-add-dataset-id-to-projects2.up.sql │ ├── 043-add-population-source-id-to-projects2.down.sql │ ├── 043-add-population-source-id-to-projects2.up.sql │ ├── 044-add_projects2_state.down.sql │ ├── 044-add_projects2_state.up.sql │ ├── 045-scenarios.down.sql │ ├── 045-scenarios.up.sql │ ├── 046-add-region-id-to-projects2.down.sql │ ├── 046-add-region-id-to-projects2.up.sql │ ├── 047-add_dataset_version_to_projects2.down.sql │ ├── 047-add_dataset_version_to_projects2.up.sql │ ├── 048-add_state_to_scenarios.down.sql │ ├── 048-add_state_to_scenarios.up.sql │ ├── 049-add_engine_config_to_projects2.down.sql │ ├── 049-add_engine_config_to_projects2.up.sql │ ├── 050-add_deleted_at_to_projects2.down.sql │ ├── 050-add_deleted_at_to_projects2.up.sql │ ├── 051-rename_datasets_to_providers_set.down.sql │ ├── 051-rename_datasets_to_providers_set.up.sql │ ├── 052-add_sources_set.down.sql │ ├── 052-add_sources_set.up.sql │ ├── 053-add_sources.down.sql │ ├── 053-add_sources.up.sql │ ├── 054-add_providers_data_to_scenarios.down.sql │ ├── 054-add_providers_data_to_scenarios.up.sql │ ├── 055-update-population-source-id-to-projects2.down.sql │ ├── 055-update-population-source-id-to-projects2.up.sql │ ├── 056-add_owner_to_sources.down.sql │ ├── 056-add_owner_to_sources.up.sql │ ├── 057-nullable_raster_file_sources.down.sql │ ├── 057-nullable_raster_file_sources.up.sql │ ├── 058-add_sources_data_to_scenarios.down.sql │ ├── 058-add_sources_data_to_scenarios.up.sql │ ├── 059-add_new_providers_geom_to_scenarios.down.sql │ ├── 059-add_new_providers_geom_to_scenarios.up.sql │ ├── 060-add_error_message_to_scenarios.down.sql │ ├── 060-add_error_message_to_scenarios.up.sql │ ├── 061-remove_deleted_at_from_projects2.down.sql │ ├── 061-remove_deleted_at_from_projects2.up.sql │ ├── 062-coverage_contexts.down.sql │ ├── 062-coverage_contexts.up.sql │ ├── 063-add_geo_coverage_to_scenarios.down.sql │ ├── 063-add_geo_coverage_to_scenarios.up.sql │ ├── 064-add_coverage_to_projects2.down.sql │ ├── 064-add_coverage_to_projects2.up.sql │ ├── 065-add_population_under_coverage_to_scenarios.down.sql │ ├── 065-add_population_under_coverage_to_scenarios.up.sql │ ├── 066-rename_investment_to_effort.down.sql │ ├── 066-rename_investment_to_effort.up.sql │ ├── 067-add_providers_set_version_index.down.sql │ ├── 067-add_providers_set_version_index.up.sql │ ├── 068-add_regions_name_index.down.sql │ └── 068-add_regions_name_index.up.sql ├── planwise │ ├── cli.edn │ ├── config.edn │ ├── errors │ │ ├── 403.html │ │ ├── 404.html │ │ └── 500.html │ ├── plpgsql │ │ ├── coverage.sql │ │ ├── fix-isolated.sql │ │ ├── functions.sql │ │ ├── isochrones.sql │ │ ├── patches.sql │ │ └── regions-preview.sql │ ├── prod.edn │ ├── public │ │ ├── favicon.ico │ │ ├── images │ │ │ ├── illustration.svg │ │ │ ├── logo-transparent.png │ │ │ ├── logo.png │ │ │ ├── planwise-icon.svg │ │ │ ├── project-background1@2x.png │ │ │ ├── project-thumbnail1@2x.png │ │ │ ├── project-thumbnail2@2x.png │ │ │ └── project-thumbnail3@2x.png │ │ ├── js │ │ │ └── leaflet.ext.js │ │ ├── providers-sample.csv │ │ ├── robots.txt │ │ └── sources-sample.csv │ └── sql │ │ ├── coverage │ │ ├── coverage.sql │ │ ├── friction.sql │ │ ├── pgrouting.sql │ │ └── simple.sql │ │ ├── projects2.sql │ │ ├── providers_set.sql │ │ ├── regions.sql │ │ ├── scenarios.sql │ │ ├── sources.sql │ │ └── users.sql └── svg │ ├── README.md │ ├── icons.svg │ └── icons │ ├── ambulance.svg │ ├── arrow-back.svg │ ├── arrow-down.svg │ ├── arrow-up.svg │ ├── box.svg │ ├── bulb.svg │ ├── bus.svg │ ├── car.svg │ ├── check-circle-wizard.svg │ ├── check-circle.svg │ ├── close.svg │ ├── cross-circle.svg │ ├── delete.svg │ ├── demographics.svg │ ├── download.svg │ ├── error.svg │ ├── exit.svg │ ├── filter.svg │ ├── hospital.svg │ ├── key-arrow-down.svg │ ├── key-arrow-left.svg │ ├── key-arrow-right.svg │ ├── location.svg │ ├── logo.svg │ ├── logo2.svg │ ├── mail-outline.svg │ ├── pickup.svg │ ├── portfolio.svg │ ├── refresh.svg │ ├── remove-circle.svg │ ├── remove.svg │ ├── repair.svg │ ├── scenario.svg │ ├── search.svg │ ├── share.svg │ ├── signout.svg │ ├── transport-means.svg │ ├── walk.svg │ └── warning.svg ├── scripts ├── Dockerfile ├── bootstrap-dev.sh ├── build-binaries ├── crop-source-raster ├── dev ├── friction │ ├── README.md │ └── load-friction-raster ├── geojson │ ├── README.md │ ├── gadm2geojson │ ├── gadm2geojson.js │ ├── package-lock.json │ └── package.json ├── migrate ├── population │ ├── README.md │ ├── load-regions │ └── scripts-design.svg ├── raster-pixel-size ├── resize-raster └── tools │ ├── README.md │ ├── docker-entrypoint.sh │ ├── readme │ ├── tools │ ├── update-region-previews │ └── update-source-sets ├── src └── planwise │ ├── auth.clj │ ├── auth │ └── guisso.clj │ ├── boundary │ ├── auth.clj │ ├── coverage.clj │ ├── engine.clj │ ├── file_store.clj │ ├── jobrunner.clj │ ├── mailer.clj │ ├── maps.clj │ ├── projects2.clj │ ├── providers_set.clj │ ├── regions.clj │ ├── runner.clj │ ├── scenarios.clj │ ├── sources.clj │ └── users.clj │ ├── component │ ├── auth.clj │ ├── coverage.clj │ ├── coverage │ │ ├── friction.clj │ │ ├── greedy_search.clj │ │ ├── rasterize.clj │ │ └── simple.clj │ ├── engine.clj │ ├── file_store.clj │ ├── jobrunner.clj │ ├── mailer.clj │ ├── maps.clj │ ├── projects2.clj │ ├── providers_set.clj │ ├── regions.clj │ ├── runner.clj │ ├── scenarios.clj │ ├── sources.clj │ ├── taskmaster.clj │ └── users.clj │ ├── config.clj │ ├── configuration │ └── templates.clj │ ├── database.clj │ ├── endpoint │ ├── auth.clj │ ├── coverage.clj │ ├── home.clj │ ├── monitor.clj │ ├── projects2.clj │ ├── providers_set.clj │ ├── regions.clj │ ├── scenarios.clj │ └── sources.clj │ ├── engine │ ├── common.clj │ ├── demand.clj │ ├── raster.clj │ └── suggestions.clj │ ├── main.clj │ ├── middleware.clj │ ├── model │ ├── ident.clj │ ├── project_review.cljc │ ├── projects2.clj │ ├── providers.clj │ ├── scenarios.clj │ └── users.clj │ ├── router.clj │ ├── tasks │ ├── build_icons.clj │ └── recompute_scenarios.clj │ └── util │ ├── collections.clj │ ├── files.clj │ ├── geo.clj │ ├── hash.clj │ ├── numbers.clj │ ├── ring.clj │ └── str.clj └── test ├── planwise ├── boundary │ └── coverage_test.clj ├── component │ ├── jobrunner_test.clj │ ├── providers_set_test.clj │ ├── regions_test.clj │ ├── scenarios_test.clj │ ├── taskmaster_test.clj │ └── users_test.clj ├── endpoint │ └── home_test.clj ├── engine │ └── raster_test.clj ├── model │ └── scenarios_test.clj ├── test_system.clj └── test_utils.clj └── resources ├── other-sites.csv ├── pointe-noire-byte.tif ├── pointe-noire-float.tif ├── sites.csv └── test.edn /.dockerignore: -------------------------------------------------------------------------------- 1 | data 2 | mapserver 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-22.04 8 | steps: 9 | - uses: actions/checkout@v4 10 | 11 | - name: Set environment up 12 | run: | 13 | mv docker-compose.ci.yml docker-compose.override.yml 14 | docker compose pull 15 | docker compose up --wait db 16 | docker compose build 17 | docker compose exec db createdb planwise-test -U postgres 18 | docker compose run --rm client npm install 19 | 20 | - name: Run specs 21 | env: 22 | JVM_OPTS: -Xmx3200m 23 | run: | 24 | docker compose up --wait db 25 | docker compose run --rm -e JVM_OPTS app lein test 26 | docker compose run --rm client npm run test 27 | docker compose run --rm app lein check-format 28 | 29 | build: 30 | needs: test 31 | runs-on: ubuntu-22.04 32 | env: 33 | DOCKER_REPOSITORY: 'instedd/planwise' 34 | DOCKER_USER: ${{ secrets.DOCKER_USER }} 35 | DOCKER_PASS: ${{ secrets.DOCKER_PASS }} 36 | steps: 37 | - uses: actions/checkout@v4 38 | - name: Build image & push to Docker Hub 39 | run: ./build.sh 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /logs 3 | /classes 4 | /checkouts 5 | /data 6 | pom.xml 7 | pom.xml.asc 8 | *.jar 9 | *.class 10 | /.env 11 | /.docker-env 12 | /.lein-* 13 | /.nrepl-port 14 | 15 | /.dir-locals.el 16 | /client/.dir-locals.el 17 | /profiles.clj 18 | /dev/src/local.clj 19 | /dev/resources/local.edn 20 | 21 | *.pyc 22 | .idea 23 | resources/planwise/public/data/ 24 | /mapserver/data/* 25 | 26 | /cpp/*.dSYM 27 | /cpp/*.o 28 | /cpp/build-*/ 29 | 30 | /docker-compose.override.yml 31 | 32 | node_modules 33 | /scripts/population/target 34 | /scripts/population/data 35 | /client/.shadow-cljs 36 | /client/target 37 | .clj-kondo/ 38 | .lsp/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM clojure:lein-2.8.1 AS base 2 | 3 | RUN echo 'deb http://archive.debian.org/debian stretch main\n\ 4 | deb http://archive.debian.org/debian-security stretch/updates main' > /etc/apt/sources.list 5 | 6 | RUN apt update && \ 7 | apt install -y build-essential cmake \ 8 | libboost-timer-dev libboost-program-options-dev \ 9 | libboost-filesystem-dev \ 10 | libpq-dev libgdal-dev postgresql-client libpq-dev \ 11 | gdal-bin python-gdal libgdal-java \ 12 | && \ 13 | curl -sL https://deb.nodesource.com/setup_9.x | bash - && \ 14 | apt-get install -y nodejs 15 | 16 | WORKDIR /app 17 | 18 | FROM base as build 19 | 20 | COPY . /app 21 | 22 | RUN cd client && npm install && npm run release 23 | RUN lein uberjar 24 | RUN scripts/build-binaries --release 25 | 26 | FROM openjdk:8u242-jre-stretch 27 | 28 | RUN echo 'deb http://archive.debian.org/debian stretch main\n\ 29 | deb http://archive.debian.org/debian-security stretch/updates main' > /etc/apt/sources.list 30 | 31 | # Install package dependencies and add precompiled binary 32 | RUN apt-get update && \ 33 | apt-get -y install postgresql-client gdal-bin python-gdal libgdal-java && \ 34 | apt-get clean && \ 35 | rm -rf /var/lib/apt/lists/* 36 | 37 | # Add scripts 38 | COPY --from=build /app/scripts/ /app/scripts/ 39 | ENV SCRIPTS_PATH /app/scripts/ 40 | 41 | # Add project compiled binaries 42 | COPY --from=build /app/cpp/build-linux-x86_64/aggregate-population /app/bin/aggregate-population 43 | COPY --from=build /app/cpp/build-linux-x86_64/walking-coverage /app/bin/walking-coverage 44 | ENV BIN_PATH /app/bin/ 45 | 46 | # Add uberjar with app 47 | COPY --from=build /app/target/uberjar/planwise-standalone.jar /app/lib/ 48 | ENV JAR_PATH /app/lib/planwise-standalone.jar 49 | 50 | # Add app version file 51 | COPY --from=build /app/resources/planwise/version /app/VERSION 52 | 53 | # Expose JNI libs to app 54 | ENV LD_LIBRARY_PATH=/usr/lib/jni 55 | 56 | # Exposed port 57 | ENV PORT 80 58 | EXPOSE $PORT 59 | 60 | # Data and tmp folders 61 | ENV DATA_PATH /data/ 62 | ENV TMP_PATH /tmp/ 63 | 64 | CMD ["java", "-jar", "/app/lib/planwise-standalone.jar"] 65 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | source <(curl -s https://raw.githubusercontent.com/manastech/ci-docker-builder/d3406587def914918666ef41c0637d6b739fdf7d/build.sh) 5 | 6 | dockerSetup 7 | echo $VERSION > VERSION 8 | echo $VERSION > resources/planwise/version 9 | 10 | if [[ -z "$DOCKER_TAG" ]]; then 11 | echo "Not building because DOCKER_TAG is undefined" 12 | exit 0 13 | fi 14 | 15 | dockerBuildAndPush 16 | dockerBuildAndPush -d mapserver -s "-mapserver" -o "-f mapserver/Dockerfile.mapserver" 17 | dockerBuildAndPush -d mapserver -s "-mapcache" -o "-f mapserver/Dockerfile.mapcache" 18 | dockerBuildAndPush -d scripts -s "-tools" 19 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | 1. Run `npm install` 4 | 2. To start a watch process with Hot Code Reload, run `npm run watch` 5 | 6 | * Running `npm run build` will rebuild both CSS and JS files for development 7 | * Running `npm run build-sass` will rebuild the CSS only 8 | 9 | 3. (optional) Connect CIDER with `cider-connect-cljs`, selecting connection type `shadow` and build `:app` 10 | -------------------------------------------------------------------------------- /client/dev/src/build.clj: -------------------------------------------------------------------------------- 1 | (ns build 2 | (:require [hawk.core :as hawk] 3 | [shadow.cljs.devtools.api :as shadow] 4 | [build.sass :as sass])) 5 | 6 | (def sass-options 7 | {:source-paths ["resources/sass"] 8 | :output-path "target/planwise/public/css" 9 | :include-paths ["node_modules"] 10 | :output-style :nested 11 | :source-map? true}) 12 | 13 | (defn sass 14 | [] 15 | (sass/build-all 16 | (merge sass-options {:output-style :nested}))) 17 | 18 | (defn sass-release 19 | [] 20 | (sass/build-all 21 | (merge sass-options {:output-style :compressed}))) 22 | 23 | (defn watch-sass 24 | {:shadow/requires-server true} 25 | [] 26 | (hawk/watch! [{:paths ["resources/sass"] 27 | :filter hawk/file? 28 | :handler (fn [ctx e] (sass))}])) 29 | 30 | (defn watch 31 | {:shadow/requires-server true} 32 | [] 33 | (sass) 34 | (watch-sass) 35 | (shadow/watch :app)) 36 | 37 | (defn build 38 | [] 39 | (sass) 40 | (shadow/compile :app)) 41 | 42 | (defn release 43 | [] 44 | (sass-release) 45 | (shadow/release :app)) 46 | -------------------------------------------------------------------------------- /client/dev/src/dev/user.cljs: -------------------------------------------------------------------------------- 1 | (ns dev.user 2 | (:require [schema.core :as s] 3 | [re-frame.core :as rf] 4 | [planwise.client.core :as client])) 5 | 6 | ;; Enable Schema validations client-side 7 | (s/set-fn-validation! true) 8 | 9 | (js/console.info "Starting in development mode") 10 | 11 | (enable-console-print!) 12 | 13 | (defn ^:dev/after-load remount 14 | [] 15 | (rf/clear-subscription-cache!) 16 | (client/mount-root)) 17 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build-sass": "shadow-cljs clj-run build/sass", 4 | "build": "shadow-cljs clj-run build/build", 5 | "watch": "shadow-cljs clj-run build/watch", 6 | "release": "shadow-cljs clj-run build/release", 7 | "test": "shadow-cljs compile test && node target/test/node-tests.js" 8 | }, 9 | "dependencies": { 10 | "create-react-class": "^15.7.0", 11 | "highlight.js": "10.4.1", 12 | "react": "16.8.6", 13 | "react-dom": "16.8.6", 14 | "react-flip-move": "^3.0.3", 15 | "react-highlight.js": "1.0.7", 16 | "react-intercom": "^1.0.15", 17 | "rmwc": "1.4.1" 18 | }, 19 | "devDependencies": { 20 | "shadow-cljs": "^2.8.58" 21 | }, 22 | "license": "GPL-3.0" 23 | } 24 | -------------------------------------------------------------------------------- /client/resources/sass/_grid.scss: -------------------------------------------------------------------------------- 1 | /* ============================================================================= 2 | GRID 3 | ============================================================================= */ 4 | 5 | .grid { 6 | display: grid; 7 | position: relative; 8 | grid-gap: 3rem; 9 | &.x2 { 10 | grid-template-columns: 1fr 1fr; 11 | grid-template-areas: "c1 c2"; 12 | } 13 | &.x3 { 14 | grid-template-columns: repeat(3, 1fr); 15 | grid-template-areas: "c1 c2 c3"; 16 | } 17 | &.x4 { 18 | grid-template-columns: repeat(4, 1fr); 19 | grid-template-areas: "c1 c2 c3 c4"; 20 | } 21 | &.centered { 22 | justify-content: center; 23 | } 24 | &.v-centered { 25 | align-items: center; 26 | height: 100%; 27 | } 28 | .section { 29 | padding-left: 0; 30 | padding-right: 0; 31 | } 32 | } 33 | 34 | .cell { 35 | padding: .5rem 0; 36 | &.span2 { 37 | grid-area: auto / auto / auto / span 2; 38 | } 39 | &.span3 { 40 | grid-area: auto / auto / auto / span 3; 41 | } 42 | &.span4 { 43 | grid-area: auto / auto / auto / span 4; 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /client/resources/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | /* ============================================================================= 2 | VARIABLES 3 | ============================================================================= */ 4 | 5 | @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500,700'); 6 | @import url('https://fonts.googleapis.com/icon?family=Material+Icons'); 7 | 8 | 9 | 10 | $sans-family: "Roboto", sans-serif; 11 | 12 | $primary-color: #FF5722; 13 | $secondary-color: #5D5D5D; 14 | 15 | $black: #000000; 16 | $white: #FFFFFF; 17 | 18 | $gray: #9E9E9E; 19 | $gray-medium: #cdcdcd; 20 | $gray-light: #E5E5E5; 21 | $gray-lighten: #f1f1f1; 22 | $gray-dark: #424242; 23 | $gray-darker: #333333; 24 | $blue-link: #2196F3; 25 | 26 | $btn-outline: $primary-color; 27 | $btn-outline-dark: darken($primary-color, 10%); 28 | 29 | $header: 100px; 30 | $side-padd: 5%; 31 | 32 | $error-red: #F1453D; 33 | -------------------------------------------------------------------------------- /client/resources/sass/landing.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import 'reset'; 3 | @import 'grid'; 4 | @import 'layout'; 5 | @import 'global'; 6 | @import 'responsive'; -------------------------------------------------------------------------------- /client/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | ;; shadow-cljs configuration 2 | {:source-paths 3 | ["dev/src" 4 | "src" 5 | "../common/src" 6 | "test"] 7 | 8 | :dependencies 9 | [[reagent "0.8.1" 10 | :exclusions [cljsjs/react cljsjs/react-dom]] 11 | [reagent-forms "0.5.43"] 12 | [reagent-utils "0.3.3"] 13 | [re-frame "0.10.9"] 14 | [re-com "2.1.0"] 15 | [day8.re-frame/http-fx "0.1.5"] 16 | [crate "0.2.4"] 17 | [cljs-ajax "0.7.3"] 18 | [clj-commons/secretary "1.2.4"] 19 | [venantius/accountant "0.2.4"] 20 | [prismatic/schema "1.1.7"] 21 | [day8.re-frame/re-frame-10x "0.5.2" 22 | :exclusions [cljsjs/react cljsjs/react-dom]] 23 | [binaryage/devtools "0.9.10"] 24 | 25 | [io.bit3/jsass "5.5.6"] 26 | [org.slf4j/slf4j-nop "1.7.25"] 27 | 28 | [cider/cider-nrepl "0.26.0"]] 29 | 30 | :http 31 | {:port 9630} 32 | 33 | :nrepl 34 | {:middleware [cider.nrepl/cider-middleware 35 | cider.piggieback/wrap-cljs-repl]} 36 | 37 | :builds 38 | {:app {:target :browser 39 | :output-dir "target/planwise/public/js" 40 | :asset-path "/js" 41 | :modules {:main {:entries [planwise.client.core]}} 42 | :compiler-options {:externs ["resources/js/externs.js"]} 43 | :dev {:closure-defines {"re_frame.trace.trace_enabled_QMARK_" true}} 44 | :devtools {:watch-dir "target/planwise/public/css" 45 | :watch-path "/css" 46 | :preloads [dev.user 47 | day8.re-frame-10x.preload]}} 48 | 49 | :test {:target :node-test 50 | :output-to "target/test/node-tests.js"}}} 51 | -------------------------------------------------------------------------------- /client/src/leaflet/controls.cljs: -------------------------------------------------------------------------------- 1 | (ns leaflet.controls 2 | (:require [crate.core :as crate])) 3 | 4 | 5 | (def ^:private MapboxLogo 6 | (js/L.Control.extend 7 | #js {:onAdd #(crate/html [:a.mapbox-logo 8 | {:href "https://www.mapbox.com/about/maps" 9 | :target "_blank"} 10 | "MapBox"])})) 11 | 12 | (defn- mapbox-logo 13 | [options] 14 | (new MapboxLogo (clj->js (merge {:position "topright"} options)))) 15 | 16 | (defn- reference-table-content 17 | [{:keys [hide-actions?] :as options}] 18 | (crate/html 19 | [:div.map-reference-table 20 | (when-not hide-actions? 21 | [:div 22 | [:h1 "Actions"] 23 | [:ul 24 | [:li [:i.material-icons "domain"] "New provider"] 25 | [:li [:i.material-icons "arrow_upward"] "Upgrade provider"] 26 | [:li [:i.material-icons "add"] "Increase capacity"]] 27 | [:hr]]) 28 | [:h1 "Provider capacity"] 29 | [:ul 30 | [:li [:div.marker-provider-icon.idle-capacity] "Excess"] 31 | [:li [:div.marker-provider-icon.at-capacity] "Covered"] 32 | [:li [:div.marker-provider-icon.unsatisfied] "Shortage"]] 33 | [:hr] 34 | [:h1 "Unsatisfied demand"] 35 | [:ul.scale 36 | [:li.q1 "Q1"] 37 | [:li.q2 "Q2"] 38 | [:li.q3 "Q3"] 39 | [:li.q4 "Q4"]]])) 40 | 41 | (def ^:private ReferenceTable 42 | (js/L.Control.extend 43 | #js {:onAdd (fn [] 44 | (let [options (js->clj (.-options (js-this)) :keywordize-keys true)] 45 | (reference-table-content options)))})) 46 | 47 | (defn- reference-table 48 | [options] 49 | (new ReferenceTable (clj->js (merge {:position "bottomright"} options)))) 50 | 51 | 52 | (defn create-control 53 | [control-def] 54 | (let [[type props] (if (vector? control-def) control-def [control-def {}])] 55 | (case type 56 | :zoom (.zoom js/L.control) 57 | :attribution (.attribution js/L.control #js {:prefix false}) 58 | :mapbox-logo (mapbox-logo props) 59 | :reference-table (reference-table props) 60 | (throw (ex-info "Invalid control type" control-def))))) 61 | -------------------------------------------------------------------------------- /client/src/planwise/client/api.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.api 2 | (:require [ajax.core :as ajax] 3 | [re-frame.core :as rf] 4 | [clojure.string :as string] 5 | [planwise.client.config :as config])) 6 | 7 | ;; Set default interceptor for adding CSRF token to all non-GET requests 8 | 9 | (def csrf-token 10 | (atom (.-value (.getElementById js/document "__anti-forgery-token")))) 11 | 12 | (def csrf-token-interceptor 13 | (ajax/to-interceptor {:name "CSRF Token Interceptor" 14 | :request (fn [req] 15 | (if (not= "GET" (:method req)) 16 | (assoc-in req [:headers "X-CSRF-Token"] @csrf-token) 17 | req))})) 18 | 19 | ;; Add interceptor to send JWT encrypted token authentication with all requests 20 | 21 | (def jwe-token 22 | (atom config/jwe-token)) 23 | 24 | (def jwe-token-interceptor 25 | (ajax/to-interceptor {:name "JWE Token Interceptor" 26 | :request (fn [req] 27 | (let [auth-value (str "Token " @jwe-token)] 28 | (assoc-in req [:headers "Authorization"] auth-value)))})) 29 | 30 | (swap! ajax/default-interceptors into [csrf-token-interceptor jwe-token-interceptor]) 31 | 32 | 33 | ;; Default event handlers for http-xhrio/api effects 34 | 35 | (rf/reg-event-fx 36 | :http-no-on-success 37 | (fn [_ [_ data]] 38 | (rf/console :log "Unhandled successful API response: " data))) 39 | 40 | (rf/reg-event-fx 41 | :http-no-on-failure 42 | (fn [_ [_ {:keys [status] :as response}]] 43 | (rf/console :error (str "Got HTTP response " status ":") response))) 44 | 45 | 46 | ;; Debugging utility functions 47 | 48 | (defn truncate 49 | ([s] 50 | (truncate s 200)) 51 | ([s max-length] 52 | (let [s (str s)] 53 | (if (> (count s) max-length) 54 | (str (subs s 0 (- max-length 3)) "...") 55 | s)))) 56 | 57 | 58 | ;; Authentication APIs 59 | 60 | (def signout 61 | {:method :delete 62 | :uri "/logout"}) 63 | -------------------------------------------------------------------------------- /client/src/planwise/client/asdf.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.asdf 2 | [:refer-clojure :exclude [reset! swap! update]] 3 | [:require [schema.core :as s]]) 4 | 5 | (s/defschema AsdfState (s/enum :invalid :reloading :valid)) 6 | 7 | (s/defrecord AsdfBase 8 | [state :- AsdfState 9 | value :- s/Any]) 10 | 11 | (defn Asdf 12 | [value-schema] 13 | {:state AsdfState 14 | :value value-schema}) 15 | 16 | (defn new 17 | [value] 18 | (map->AsdfBase {:state :invalid :value value})) 19 | 20 | (defn value 21 | [asdf] 22 | (:value asdf)) 23 | 24 | (defn state 25 | [asdf] 26 | (:state asdf)) 27 | 28 | (defn valid? 29 | [asdf] 30 | (= :valid (:state asdf))) 31 | 32 | (defn reloading? 33 | [asdf] 34 | (= :reloading (:state asdf))) 35 | 36 | (defn should-reload? 37 | [asdf] 38 | (= :invalid (:state asdf))) 39 | 40 | (defn reset! 41 | [asdf value] 42 | (assoc asdf 43 | :state :valid 44 | :value value)) 45 | 46 | (defn swap! 47 | [asdf swap-fn & args] 48 | (assoc asdf 49 | :state :valid 50 | :value (apply swap-fn (:value asdf) args))) 51 | 52 | (defn invalidate! 53 | ([asdf] 54 | (assoc asdf :state :invalid)) 55 | ([asdf update-fn & args] 56 | (assoc asdf 57 | :state :invalid 58 | :value (apply update-fn (:value asdf) args)))) 59 | 60 | (defn reload! 61 | [asdf] 62 | (assoc asdf :state :reloading)) 63 | 64 | (defn update 65 | "Updates the value in the asdf preserving its current state" 66 | [asdf update-fn & args] 67 | (assoc asdf :value (apply update-fn (:value asdf) args))) 68 | -------------------------------------------------------------------------------- /client/src/planwise/client/components/common.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.components.common) 2 | 3 | ;; SVG based icons 4 | 5 | (defn icon [icon-name & [icon-class]] 6 | (let [icon-name (some-> icon-name name) 7 | icon-class (or icon-class "icon")] 8 | [:svg {:class icon-class 9 | ; Support for :xlinkHref is present in React 0.14+, but reagent 0.5.1 is bundled with 0.13 10 | ; Change the dangerouslySetInnerHTML call to the following content after upgrading 11 | ; [:use {:xlinkHref (str "#icon-" icon-name)}]]) 12 | :dangerouslySetInnerHTML {:__html (str "")}}])) 13 | 14 | -------------------------------------------------------------------------------- /client/src/planwise/client/config.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.config 2 | (:require [planwise.model.project])) 3 | 4 | (def global-config 5 | (aget js/window "_CONFIG")) 6 | 7 | (def jwe-token 8 | (aget global-config "jwe-token")) 9 | 10 | (def user-email 11 | (aget global-config "identity")) 12 | 13 | (def mapserver-url 14 | (aget global-config "mapserver-url")) 15 | 16 | (def app-version 17 | (aget global-config "app-version")) 18 | 19 | (def intercom-app-id 20 | (aget global-config "intercom-app-id")) 21 | -------------------------------------------------------------------------------- /client/src/planwise/client/core.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.core 2 | (:require-macros [secretary.core :refer [defroute]]) 3 | (:require [reagent.core :as reagent :refer [atom]] 4 | [re-frame.core :as rf] 5 | [secretary.core :as secretary] 6 | [accountant.core :as accountant] 7 | 8 | [planwise.client.effects] 9 | [planwise.client.routes] 10 | [planwise.client.handlers] 11 | [planwise.client.subs] 12 | [planwise.client.views :as views])) 13 | 14 | 15 | ;; ------------------------- 16 | ;; Initialize app 17 | 18 | (defn mount-root [] 19 | (reagent/render [views/planwise-app] 20 | (.getElementById js/document "app"))) 21 | 22 | (defn- ^:export main [] 23 | (rf/dispatch-sync [:initialise-db]) 24 | (accountant/configure-navigation! 25 | {:nav-handler 26 | (fn [path] 27 | (secretary/dispatch! path)) 28 | :path-exists? 29 | (fn [path] 30 | (secretary/locate-route path))}) 31 | (accountant/dispatch-current!) 32 | (mount-root)) 33 | -------------------------------------------------------------------------------- /client/src/planwise/client/db.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.db 2 | (:require [planwise.client.sources.db :as sources] 3 | [planwise.client.projects2.db :as projects2] 4 | [planwise.client.scenarios.db :as scenarios] 5 | [planwise.client.providers-set.db :as providers-set])) 6 | 7 | (def initial-db 8 | {;; Navigation (current page) 9 | :current-page :home 10 | ;; Map of navigation params (ie. :page, :id, :section, etc) 11 | :page-params {} 12 | 13 | ;; Projects2 14 | :projects2 projects2/initial-db 15 | 16 | ;; Regions id => {:keys [id name admin-level & [geojson preview-geojson]]} 17 | :regions {} 18 | 19 | ;; Providers Set 20 | :providers-set providers-set/initial-db 21 | 22 | :coverage {} 23 | 24 | :sources sources/initial-db 25 | 26 | :scenarios scenarios/initial-db}) 27 | -------------------------------------------------------------------------------- /client/src/planwise/client/handlers.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.handlers 2 | (:require [planwise.client.db :as db] 3 | [planwise.client.api :as api] 4 | [planwise.client.routes :as routes] 5 | [planwise.client.projects2.handlers] 6 | [planwise.client.providers-set.handlers] 7 | [planwise.client.sources.handlers] 8 | [planwise.client.scenarios.handlers] 9 | [planwise.client.coverage] 10 | [planwise.client.regions.handlers :as regions] 11 | [re-frame.core :as rf])) 12 | 13 | ;; Event handlers 14 | ;; ----------------------------------------------------------------------- 15 | 16 | (rf/reg-event-fx 17 | :initialise-db 18 | (fn [_ _] 19 | {:dispatch-n [[:regions/load-regions] 20 | [:coverage/load-algorithms]] 21 | :db db/initial-db})) 22 | 23 | (rf/reg-event-fx 24 | :navigate 25 | (fn [{:keys [db]} [_ {page :page, :as params}]] 26 | {:db (assoc db :current-page page :page-params params) 27 | :route-change params})) 28 | 29 | (rf/reg-event-fx 30 | :signout 31 | (fn [_ [_]] 32 | {:api (assoc api/signout 33 | :on-success [:after-signout]) 34 | :intercom-logout true})) 35 | 36 | (rf/reg-event-fx 37 | :after-signout 38 | (fn [_ [_ data]] 39 | (let [url (or (:redirect-to data) (routes/home))] 40 | {:location url}))) 41 | 42 | (rf/reg-event-fx 43 | :message-posted 44 | (fn [_ [_ message]] 45 | (cond 46 | (#{"react-devtools-content-script" 47 | "react-devtools-bridge" 48 | "react-devtools-detector"} 49 | (aget message "source")) 50 | nil ; ignore React dev tools messages 51 | 52 | true 53 | (rf/console :warn "Invalid message received " message)))) 54 | -------------------------------------------------------------------------------- /client/src/planwise/client/modal/modal.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.modal.modal 2 | (:require [re-frame.core :as rf] 3 | [planwise.client.ui.common :as ui] 4 | [planwise.client.components.common2 :as common2] 5 | [planwise.client.ui.rmwc :as m])) 6 | 7 | (def in-modal-path (rf/path [:modal-window])) 8 | 9 | ;; ---------------------------------------------------------------------------- 10 | ;; Subscription 11 | (rf/reg-sub 12 | :modal/state 13 | (fn [db _] 14 | (get-in db [:modal-window :state]))) 15 | 16 | ;; ---------------------------------------------------------------------------- 17 | ;; Handlers 18 | (rf/reg-event-db 19 | :modal/show 20 | in-modal-path 21 | (fn [db _] 22 | (assoc-in db [:state] {:open? true}))) 23 | 24 | (rf/reg-event-db 25 | :modal/hide 26 | in-modal-path 27 | (fn [db _] 28 | (assoc-in db [:state] {:open? false}))) 29 | 30 | ;; ---------------------------------------------------------------------------- 31 | ;; View 32 | (defn modal-view 33 | [attrs content] 34 | (fn [attrs _] 35 | (let [state @(rf/subscribe [:modal/state])] 36 | [m/Dialog {:open (:open? state)} 37 | [m/DialogSurface 38 | [m/DialogHeader 39 | [m/DialogHeaderTitle (:title attrs)]] 40 | [m/DialogBody content] 41 | [m/DialogFooter 42 | [m/DialogFooterButton {:on-click (fn [] ((:cancel-fn attrs)))} 43 | "Cancel"] 44 | [m/DialogFooterButton {:on-click (fn [] ((:accept-fn attrs))) 45 | :disabled (not (:accept-enabled? attrs))} 46 | (:accept-label attrs)]]]]))) 47 | -------------------------------------------------------------------------------- /client/src/planwise/client/projects2/api.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.projects2.api) 2 | 3 | ;; ---------------------------------------------------------------------------- 4 | ;; API methods 5 | 6 | (defn- create-project! 7 | [defaults] 8 | {:method :post 9 | :section :index 10 | :params {:project defaults} 11 | :uri "/api/projects2"}) 12 | 13 | (defn- list-projects 14 | [] 15 | {:method :get 16 | :section :index 17 | :uri (str "/api/projects2")}) 18 | 19 | (defn- list-templates 20 | [] 21 | {:method :get 22 | :section :index 23 | :uri (str "/api/projects2/templates")}) 24 | 25 | (defn- update-project 26 | [project-id project] 27 | {:method :put 28 | :section :show 29 | :params {:project project} 30 | :uri (str "/api/projects2/" project-id)}) 31 | 32 | (defn- get-project 33 | [project-id] 34 | {:method :get 35 | :uri (str "/api/projects2/" project-id)}) 36 | 37 | (defn- start-project! 38 | [project-id] 39 | {:method :post 40 | :uri (str "/api/projects2/" project-id "/start")}) 41 | 42 | (defn- reset-project! 43 | [project-id] 44 | {:method :post 45 | :uri (str "/api/projects2/" project-id "/reset")}) 46 | 47 | (defn- delete-project! 48 | [project-id] 49 | {:method :delete 50 | :uri (str "/api/projects2/" project-id)}) 51 | -------------------------------------------------------------------------------- /client/src/planwise/client/projects2/components/common.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.projects2.components.common 2 | (:require [planwise.client.ui.dialog :refer [dialog]])) 3 | 4 | (defn delete-project-dialog 5 | [{:keys [open? id cancel-fn delete-fn]}] 6 | [dialog {:open? open? 7 | :title "Delete Project" 8 | :class "narrow" 9 | :delete-fn delete-fn 10 | :cancel-fn cancel-fn 11 | :content [:p.dialog-prompt 12 | "Do you want to delete this project?" 13 | [:br] 14 | [:strong "This action cannot be undone."]]}]) 15 | 16 | (defn reset-project-dialog 17 | [{:keys [open? id cancel-fn accept-fn]}] 18 | [dialog {:open? open? 19 | :title "Reset Project" 20 | :class "narrow" 21 | :acceptable? true 22 | :accept-fn accept-fn 23 | :cancel-fn cancel-fn 24 | :content [:p.dialog-prompt 25 | "Do you want to reset this project? " 26 | "This will delete all your current scenarios, but will allow changes in the project settings." 27 | [:br] 28 | [:strong "This action cannot be undone."]]}]) 29 | -------------------------------------------------------------------------------- /client/src/planwise/client/projects2/components/create.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.projects2.components.create 2 | (:require [re-frame.core :refer [subscribe dispatch] :as rf] 3 | [planwise.client.asdf :as asdf] 4 | [reagent.core :as r] 5 | [re-com.core :as rc] 6 | [planwise.client.components.common2 :as common2] 7 | [planwise.client.routes :as routes] 8 | [planwise.client.utils :as utils] 9 | [planwise.client.ui.common :as ui] 10 | [planwise.client.ui.rmwc :as m] 11 | [planwise.client.mapping :refer [static-image fullmap-region-geo]] 12 | [planwise.client.components.common :as common])) 13 | (def project-templates 14 | [{:description "Plan facilities based on ground access" 15 | :icon "directions_walk" 16 | :key "plan" 17 | :defaults {:name "ground"}} 18 | {:description "Plan diagnostic services & sample referrals" 19 | :icon "call_split" 20 | :key "diagnosis" 21 | :defaults {:name "sample"}}]) 22 | 23 | (defn project-section-template-selector 24 | [] 25 | [ui/fixed-width (common2/nav-params) 26 | (let [templates (subscribe [:projects2/templates]) 27 | scratch-template (first (filter #(not (contains? % :description)) @templates)) 28 | sample-templates (filter #(contains? % :description) @templates)] 29 | (dispatch [:projects2/get-templates-list]) 30 | [:div.template-container 31 | (if (some? sample-templates) 32 | [:h2 "Start from a template"]) 33 | (if (some? sample-templates) 34 | [:div.row 35 | (map (fn [template] 36 | [:a.action {:key (:key template) :onClick #(dispatch [:projects2/new-project (:defaults template)])} 37 | [m/Icon {} (:icon template)] 38 | [:div (:description template)]]) 39 | sample-templates)]) 40 | [:hr] 41 | [:h2 "Start from scratch"] 42 | [:div.row 43 | [:a.action {:onClick #(dispatch [:projects2/new-project (:defaults scratch-template)])} 44 | [m/Icon {} "folder_open"] 45 | [:div "Follow a wizard through all available settings"]]]])]) 46 | -------------------------------------------------------------------------------- /client/src/planwise/client/projects2/core.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.projects2.core) 2 | 3 | (def sections-base 4 | [{:step "goal" :title "Goal" :spec :planwise.model.project/goal-step} 5 | {:step "consumers" :title "Consumers" :spec :planwise.model.project/consumers-step} 6 | {:step "providers" :title "Providers" :spec :planwise.model.project/providers-step} 7 | {:step "coverage" :title "Coverage" :spec :planwise.model.project/coverage-step} 8 | {:step "actions" :title "Actions" :spec :planwise.model.project/actions-step} 9 | {:step "review" :title "Review" :spec :planwise.model.project/review-step}]) 10 | 11 | (def sections 12 | (->> sections-base 13 | (#(list % (concat (drop 1 %) (repeat nil)))) 14 | (apply mapv (fn [step next] 15 | (assoc step :next-step (:step next)))))) 16 | -------------------------------------------------------------------------------- /client/src/planwise/client/projects2/db.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.projects2.db 2 | (:require [schema.core :as s] 3 | [planwise.client.asdf :as asdf])) 4 | 5 | (def initial-db 6 | {:current-project nil 7 | :list nil 8 | :open-dialog nil 9 | :source-types #{"raster" "points"}}) 10 | -------------------------------------------------------------------------------- /client/src/planwise/client/projects2/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.projects2.subs 2 | (:require [re-frame.core :as rf] 3 | [clojure.string :as string] 4 | [planwise.client.asdf :as asdf] 5 | [planwise.client.utils :as utils])) 6 | 7 | (rf/reg-sub 8 | :projects2/current-project 9 | (fn [db _] 10 | (get-in db [:projects2 :current-project]))) 11 | 12 | (rf/reg-sub 13 | :projects2/templates 14 | (fn [db _] 15 | (get-in db [:projects2 :templates]))) 16 | 17 | (rf/reg-sub 18 | :projects2/list 19 | (fn [db _] 20 | (some->> (get-in db [:projects2 :list]) 21 | (sort-by (comp string/lower-case :name))))) 22 | 23 | (rf/reg-sub 24 | :projects2/source-types 25 | (fn [db _] 26 | (get-in db [:projects2 :source-types]))) 27 | 28 | (rf/reg-sub 29 | :projects2/tags :<- [:projects2/current-project] 30 | (fn [current-project [_]] 31 | (get-in current-project [:config :providers :tags]))) 32 | 33 | (rf/reg-sub 34 | :projects2/build-actions :<- [:projects2/current-project] 35 | (fn [current-project [_]] 36 | (get-in current-project [:config :actions :build]))) 37 | 38 | (rf/reg-sub 39 | :projects2/upgrade-actions :<- [:projects2/current-project] 40 | (fn [current-project [_]] 41 | (get-in current-project [:config :actions :upgrade]))) 42 | 43 | (rf/reg-sub 44 | :projects2/new-project-coverage :<- [:projects2/current-project] 45 | (fn [current-project [_]] 46 | (get-in current-project [:coverage-algorithm]))) 47 | 48 | (rf/reg-sub 49 | :projects2/open-dialog 50 | (fn [db _] 51 | (get-in db [:projects2 :open-dialog]))) 52 | -------------------------------------------------------------------------------- /client/src/planwise/client/providers_set/api.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.providers-set.api) 2 | 3 | ;; ---------------------------------------------------------------------------- 4 | ;; API methods 5 | 6 | (def load-providers-set 7 | {:method :get 8 | :uri "/api/providers"}) 9 | 10 | (defn create-provider-set-with-csv 11 | [{:keys [name csv-file]}] 12 | (let [form-data (doto (js/FormData.) 13 | (.append "name" name) 14 | (.append "file" csv-file))] 15 | 16 | {:method :post 17 | :uri "/api/providers" 18 | :body form-data})) 19 | 20 | (defn delete-provider-set 21 | [id] 22 | {:method :delete 23 | :params {:id id} 24 | :uri "/api/providers"}) 25 | -------------------------------------------------------------------------------- /client/src/planwise/client/providers_set/components/dropdown.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.providers-set.components.dropdown 2 | (:require [re-frame.core :as rf] 3 | [planwise.client.asdf :as asdf] 4 | [planwise.client.ui.filter-select :as filter-select])) 5 | 6 | (defn providers-set-dropdown-component 7 | [attrs] 8 | (let [list (rf/subscribe [:providers-set/list]) 9 | props (merge {:choices @(rf/subscribe [:providers-set/dropdown-options]) 10 | :label-fn :label 11 | :render-fn (fn [provider-set] [:div.option-row 12 | [:span (:label provider-set)]])} 13 | attrs)] 14 | (when (asdf/should-reload? @list) 15 | (rf/dispatch [:providers-set/load-providers-set])) 16 | (into [filter-select/single-dropdown] (mapcat identity props)))) 17 | -------------------------------------------------------------------------------- /client/src/planwise/client/providers_set/db.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.providers-set.db 2 | (:require [planwise.client.asdf :as asdf] 3 | [planwise.client.utils :refer [remove-by-id]] 4 | [clojure.spec.alpha :as s])) 5 | 6 | (s/def ::view-state #{:list :create-dialog :creating}) 7 | (s/def ::last-error (s/nilable string?)) 8 | (s/def ::name string?) 9 | (s/def ::coverage keyword?) 10 | 11 | (def initial-new-provider-set 12 | {:name "" 13 | :js-file nil 14 | :coverage nil}) 15 | 16 | (def initial-db 17 | {:view-state :list 18 | :last-error nil 19 | :list (asdf/new nil) 20 | :new-provider-set initial-new-provider-set}) 21 | 22 | (defn show-dialog? 23 | [state] 24 | (#{:create-dialog :creating} state)) 25 | -------------------------------------------------------------------------------- /client/src/planwise/client/providers_set/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.providers-set.subs 2 | (:require [re-frame.core :as rf] 3 | [planwise.client.asdf :as asdf] 4 | [planwise.client.utils :as utils])) 5 | 6 | (rf/reg-sub 7 | :providers-set/list 8 | (fn [db _] 9 | (get-in db [:providers-set :list]))) 10 | 11 | (rf/reg-sub 12 | :providers-set/dropdown-options 13 | (fn [db _] 14 | (let [providers (get-in db [:providers-set :list :value])] 15 | (->> providers 16 | (map (fn [provider-set] 17 | (let [{:keys [id name]} provider-set] 18 | {:value id :label name :id id}))) 19 | (sort-by :label) 20 | (into [{:value nil :label "None" :id nil}]))))) 21 | 22 | (rf/reg-sub 23 | :providers-set/view-state 24 | (fn [db _] 25 | (get-in db [:providers-set :view-state]))) 26 | 27 | (rf/reg-sub 28 | :providers-set/last-error 29 | (fn [db _] 30 | (get-in db [:providers-set :last-error]))) 31 | 32 | (rf/reg-sub 33 | :providers-set/new-provider-set-state 34 | (fn [db _] 35 | (get-in db [:providers-set :new-provider-set :state]))) 36 | 37 | (rf/reg-sub 38 | :providers-set/new-provider-set-name 39 | (fn [db _] 40 | (get-in db [:providers-set :new-provider-set :name]))) 41 | 42 | (rf/reg-sub 43 | :providers-set/new-provider-set-js-file 44 | (fn [db _] 45 | (get-in db [:providers-set :new-provider-set :js-file]))) 46 | 47 | 48 | (rf/reg-sub 49 | :providers-set/delete-selected-provider-set 50 | (fn [db _] 51 | (get-in db [:providers-set :selected-provider]))) 52 | -------------------------------------------------------------------------------- /client/src/planwise/client/regions/api.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.regions.api 2 | (:require [clojure.string :as str])) 3 | 4 | (def load-regions 5 | {:method :get 6 | :uri "/api/regions"}) 7 | 8 | (defn load-regions-with-preview 9 | [ids] 10 | {:method :get 11 | :uri (str "/api/regions/" (str/join "," ids) "/with-preview")}) 12 | 13 | (defn load-regions-with-geo 14 | [ids] 15 | {:method :get 16 | :uri (str "/api/regions/" (str/join "," ids) "/with-geo")}) 17 | -------------------------------------------------------------------------------- /client/src/planwise/client/regions/handlers.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.regions.handlers 2 | (:require [re-frame.core :as rf] 3 | [planwise.client.regions.api :as api])) 4 | 5 | (def in-regions (rf/path [:regions])) 6 | 7 | (rf/reg-event-fx 8 | :regions/load-regions 9 | (fn [_ _] 10 | {:api (assoc api/load-regions 11 | :on-success [:regions/regions-loaded])})) 12 | 13 | (defn ids-for-regions-without 14 | [field regions ids] 15 | (->> ids 16 | (filter some?) 17 | (remove (fn [id] (get-in regions [id field]))) 18 | seq)) 19 | 20 | (rf/reg-event-fx 21 | :regions/load-regions-with-preview 22 | in-regions 23 | (fn [{:keys [db]} [_ region-ids]] 24 | (when-some [missing-region-ids (ids-for-regions-without :preview-geojson db region-ids)] 25 | {:api (assoc (api/load-regions-with-preview missing-region-ids) 26 | :on-success [:regions/regions-loaded])}))) 27 | 28 | (rf/reg-event-fx 29 | :regions/load-regions-with-geo 30 | in-regions 31 | (fn [{:keys [db]} [_ region-ids]] 32 | (when-some [missing-region-ids (ids-for-regions-without :geojson db region-ids)] 33 | {:api (assoc (api/load-regions-with-geo missing-region-ids) 34 | :on-success [:regions/regions-loaded])}))) 35 | 36 | (rf/reg-event-db 37 | :regions/regions-loaded 38 | in-regions 39 | (fn [db [_ regions-data]] 40 | (reduce 41 | (fn [db {id :id :as region}] 42 | (update db id #(merge % region))) 43 | db regions-data))) 44 | -------------------------------------------------------------------------------- /client/src/planwise/client/regions/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.regions.subs 2 | (:require [re-frame.core :as rf])) 3 | 4 | (rf/reg-sub 5 | :regions/list 6 | (fn [db _] 7 | (sort-by :name (vals (:regions db))))) 8 | 9 | (rf/reg-sub 10 | :regions/preview-geojson 11 | (fn [db [_ region-id]] 12 | (get-in db [:regions region-id :preview-geojson]))) 13 | 14 | (rf/reg-sub 15 | :regions/geojson 16 | (fn [db [_ region-id]] 17 | (get-in db [:regions region-id :geojson]))) 18 | 19 | -------------------------------------------------------------------------------- /client/src/planwise/client/scenarios/api.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.scenarios.api) 2 | 3 | (defn load-scenario 4 | [id] 5 | {:method :get 6 | :uri (str "/api/scenarios/" id)}) 7 | 8 | (defn copy-scenario 9 | [id] 10 | {:method :post 11 | :uri (str "/api/scenarios/" id "/copy")}) 12 | 13 | (defn update-scenario 14 | [id scenario] 15 | {:method :put 16 | :params {:scenario scenario} 17 | :uri (str "/api/scenarios/" id)}) 18 | 19 | (defn load-scenarios 20 | [id] 21 | {:method :get 22 | :uri (str "/api/projects2/" id "/scenarios")}) 23 | 24 | (defn suggested-locations-for-new-provider 25 | [id] 26 | {:method :get 27 | :timeout 90000 28 | :uri (str "/api/scenarios/" id "/suggested-locations")}) 29 | 30 | (defn suggested-providers-to-improve 31 | [id] 32 | {:method :get 33 | :timeout 90000 34 | :uri (str "/api/scenarios/" id "/suggested-providers")}) 35 | 36 | (defn get-provider-geom 37 | [id provider-id] 38 | {:method :get 39 | :uri (str "/api/scenarios/" id "/geometry/" provider-id)}) 40 | 41 | (defn get-suggestion-geom 42 | [id iteration] 43 | {:method :get 44 | :uri (str "/api/scenarios/" id "/coverage/suggestion/" iteration)}) 45 | 46 | (defn- delete-scenario 47 | [id] 48 | {:method :delete 49 | :uri (str "/api/scenarios/" id)}) 50 | 51 | (defn download-scenario-sources 52 | [id] 53 | (str "/api/scenarios/" id "/sources")) 54 | 55 | (defn download-scenario-providers 56 | [id] 57 | (str "/api/scenarios/" id "/providers")) 58 | -------------------------------------------------------------------------------- /client/src/planwise/client/sources/api.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.sources.api) 2 | 3 | ;; ---------------------------------------------------------------------------- 4 | ;; API methods 5 | 6 | (def load-sources 7 | {:method :get 8 | :uri "/api/sources"}) 9 | 10 | (defn create-source-with-csv 11 | [{:keys [name unit csv-file]}] 12 | (let [form-data (doto (js/FormData.) 13 | (.append "name" name) 14 | (.append "unit" unit) 15 | (.append "csvfile" csv-file))] 16 | {:method :post 17 | :uri "/api/sources" 18 | :body form-data})) 19 | -------------------------------------------------------------------------------- /client/src/planwise/client/sources/components/dropdown.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.sources.components.dropdown 2 | (:require [re-frame.core :as rf] 3 | [re-frame.core :refer [dispatch subscribe]] 4 | [planwise.client.asdf :as asdf] 5 | [planwise.client.components.common2 :as common2] 6 | [planwise.client.utils :as utils] 7 | [planwise.client.ui.filter-select :as filter-select] 8 | [planwise.client.ui.rmwc :as m])) 9 | 10 | (def in-sources (rf/path [:sources])) 11 | 12 | (rf/reg-sub 13 | :sources/dropdown-options 14 | (fn [_] 15 | [(subscribe [:sources/list]) 16 | (subscribe [:projects2/source-types])]) 17 | (fn [[list types] _] 18 | (filter (fn [{:keys [type]}] (types type)) list))) 19 | 20 | ;; ---------------------------------------------------------------------------- 21 | ;; Views 22 | 23 | (defn sources-dropdown-component 24 | [attrs] 25 | (let [props (merge {:choices @(rf/subscribe [:sources/dropdown-options]) 26 | :label-fn :name 27 | :render-fn (fn [source] [:div.option-row 28 | [:span (:name source)] 29 | [:span.option-context (:value source)]])} 30 | attrs)] 31 | (into [filter-select/single-dropdown] (mapcat identity props)))) 32 | -------------------------------------------------------------------------------- /client/src/planwise/client/sources/db.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.sources.db 2 | (:require [planwise.client.asdf :as asdf])) 3 | 4 | (def initial-db 5 | {:list (asdf/new nil)}) 6 | -------------------------------------------------------------------------------- /client/src/planwise/client/sources/handlers.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.sources.handlers 2 | (:require [re-frame.core :refer [register-handler dispatch] :as rf] 3 | [planwise.client.asdf :as asdf] 4 | [planwise.client.sources.api :as api])) 5 | 6 | (def in-sources (rf/path [:sources])) 7 | 8 | (rf/reg-event-fx 9 | :sources/load 10 | in-sources 11 | (fn [{:keys [db]} [_]] 12 | {:api (assoc api/load-sources 13 | :on-success [:sources/loaded]) 14 | :db (update db :list asdf/reload!)})) 15 | 16 | (rf/reg-event-db 17 | :sources/loaded 18 | in-sources 19 | (fn [db [_ sources]] 20 | (update db :list asdf/reset! sources))) 21 | 22 | (rf/reg-event-db 23 | :sources.new/update 24 | in-sources 25 | (fn [db [_ changes]] 26 | (update db :new #(merge % changes)))) 27 | 28 | (rf/reg-event-fx 29 | :sources.new/create 30 | in-sources 31 | (fn [{:keys [db]}] 32 | (let [new-source (get db :new)] 33 | {:api (assoc (api/create-source-with-csv new-source) 34 | :on-success [:sources.new/created] 35 | :on-failure [:sources.new/failed])}))) 36 | 37 | (rf/reg-event-db 38 | :sources.new/discard 39 | in-sources 40 | (fn [db] 41 | (rf/dispatch [:modal/hide]) 42 | (dissoc db :new))) 43 | 44 | (rf/reg-event-db 45 | :sources.new/created 46 | in-sources 47 | (fn [db [_ created-source]] 48 | (rf/dispatch [:modal/hide]) 49 | (-> db 50 | (dissoc :new) 51 | (update :list #(asdf/swap! % conj created-source))))) 52 | 53 | (rf/reg-event-db 54 | :sources.new/failed 55 | in-sources 56 | (fn [db [_ err]] 57 | (assoc-in db [:new :current-error] (:status-text err)))) 58 | -------------------------------------------------------------------------------- /client/src/planwise/client/sources/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.sources.subs 2 | (:require [re-frame.core :as rf] 3 | [planwise.client.asdf :as asdf] 4 | [clojure.string :as str])) 5 | 6 | (rf/reg-sub 7 | :sources/list-as-asdf 8 | (fn [db _] 9 | (let [sources (get-in db [:sources :list])] 10 | (when (asdf/should-reload? sources) 11 | (rf/dispatch [:sources/load])) 12 | sources))) 13 | 14 | (rf/reg-sub 15 | :sources/list 16 | (fn [_] 17 | (rf/subscribe [:sources/list-as-asdf])) 18 | (fn [sources] 19 | (asdf/value sources))) 20 | 21 | (rf/reg-sub 22 | :sources.new/data 23 | (fn [db _] 24 | (get-in db [:sources :new]))) 25 | 26 | (rf/reg-sub 27 | :sources.new/valid? 28 | (fn [_] 29 | (rf/subscribe [:sources.new/data])) 30 | (fn [new-source] 31 | (let [name (:name new-source) 32 | csv-file (:csv-file new-source)] 33 | (not (or (str/blank? name) 34 | (nil? csv-file)))))) 35 | 36 | (rf/reg-sub 37 | :sources.new/current-error 38 | (fn [db _] 39 | (get-in db [:sources :new :current-error]))) 40 | -------------------------------------------------------------------------------- /client/src/planwise/client/styles.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.styles) 2 | 3 | (def orange "#ff8400") 4 | (def dark-orange "#ac5900") 5 | (def green "#819f51") 6 | (def black "#111111") 7 | (def light-grey "#b7b7b7") 8 | 9 | ; See https://bl.ocks.org/mbostock/5577023 10 | (def facility-types-palette 11 | ["#377eb8" "#4daf4a" "#984ea3" "#ff7f00" "#a65628" "#f781bf"]) 12 | 13 | (def invalid-facility-type "#999999") 14 | -------------------------------------------------------------------------------- /client/src/planwise/client/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.subs 2 | (:require [re-frame.core :as rf] 3 | [planwise.client.coverage] 4 | [planwise.client.projects2.subs] 5 | [planwise.client.sources.subs] 6 | [planwise.client.providers-set.subs] 7 | [planwise.client.regions.subs] 8 | [planwise.client.scenarios.subs])) 9 | 10 | 11 | ;; Subscriptions 12 | ;; ------------------------------------------------------- 13 | 14 | (rf/reg-sub 15 | :current-page 16 | (fn [db _] 17 | (:current-page db))) 18 | 19 | (rf/reg-sub 20 | :page-params 21 | (fn [db _] 22 | (:page-params db))) 23 | -------------------------------------------------------------------------------- /client/src/planwise/client/ui/dialog.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.ui.dialog 2 | (:require [reagent.core :as r] 3 | [planwise.client.ui.rmwc :as m] 4 | [planwise.client.ui.common :as ui] 5 | [planwise.client.utils :as utils])) 6 | 7 | (defn dialog 8 | [{:keys [open? class title content accept-fn cancel-fn delete-fn acceptable?]}] 9 | (r/with-let [] 10 | [m/Dialog {:open open? 11 | :on-accept accept-fn 12 | :on-close cancel-fn 13 | :className class} 14 | [m/DialogSurface 15 | [m/DialogHeader 16 | [m/DialogHeaderTitle title] 17 | [ui/close-button {:on-click cancel-fn}]] 18 | [m/DialogBody 19 | [:form.vertical {:on-submit (utils/prevent-default accept-fn)} 20 | content]] 21 | [m/DialogFooter 22 | (when (some? delete-fn) 23 | [:div {:class (when (some? accept-fn) "flex-spacer")} 24 | [m/Button {:on-click delete-fn} "Delete"]]) 25 | (when (some? cancel-fn) [m/DialogFooterButton {:cancel true} "Cancel"]) 26 | (when (some? accept-fn) [m/DialogFooterButton {:accept true 27 | :unelevated true 28 | :disabled (not acceptable?)} "OK"])]]] 29 | (finally 30 | ;; NB. this is a hack/fix to avoid the scroll lock when a RMWC Dialog is 31 | ;; destroyed while running the hide transition. In that case the HTML body 32 | ;; had the `mdc-dialog-scroll-lock` class added because the dialog was 33 | ;; open, but MDC fails to remove it if the dialog is destroyed before the 34 | ;; transition end for the closing animation event occurs 35 | (js/document.body.classList.remove "mdc-dialog-scroll-lock")))) 36 | -------------------------------------------------------------------------------- /client/src/planwise/client/ui/macros.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.client.ui.macros) 2 | 3 | (def rmwc-tags 4 | '[Button 5 | Checkbox 6 | Chip 7 | ChipIcon 8 | ChipSet 9 | ChipText 10 | Dialog 11 | DialogSurface 12 | DialogHeader 13 | DialogHeaderTitle 14 | DialogBody 15 | DialogFooter 16 | DialogFooterButton 17 | DialogBackdrop 18 | Elevation 19 | Fab 20 | FormField 21 | Grid 22 | GridCell 23 | GridInner 24 | GridList 25 | GridTile 26 | GridTileIcon 27 | GridTilePrimary 28 | GridTilePrimaryContent 29 | GridTileSecondary 30 | GridTileTitle 31 | Icon 32 | List 33 | ListDivider 34 | ListItem 35 | ListItemGraphic 36 | ListItemMeta 37 | ListItemSecondaryText 38 | ListItemText 39 | Menu 40 | MenuAnchor 41 | MenuItem 42 | Radio 43 | Ripple 44 | Select 45 | SimpleDialog 46 | SimpleMenu 47 | Slider 48 | Switch 49 | Tab 50 | TabBar 51 | TabIcon 52 | TextField 53 | TextFieldHelperText 54 | TextFieldIcon 55 | Theme 56 | Toolbar 57 | ToolbarRow 58 | ToolbarSection 59 | ToolbarTitle 60 | Typography]) 61 | 62 | (defn rmwc-ui-react-import [tname] 63 | `(def ~tname 64 | (reagent/adapt-react-class ~(symbol (str "rmwc/" (name tname)))))) 65 | 66 | (defmacro export-rmwc [] 67 | `(do 68 | ~@(map rmwc-ui-react-import rmwc-tags))) 69 | -------------------------------------------------------------------------------- /client/src/planwise/client/ui/rmwc.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.ui.rmwc 2 | (:refer-clojure :exclude [List]) 3 | (:require-macros [planwise.client.ui.macros :refer [export-rmwc]]) 4 | (:require rmwc 5 | [reagent.core :as reagent])) 6 | 7 | (export-rmwc) 8 | -------------------------------------------------------------------------------- /client/test/planwise/client/scenarios_test.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.scenarios-test 2 | (:require [planwise.client.scenarios.db :as sut] 3 | [cljs.test :as t :refer-macros [deftest is]])) 4 | 5 | (deftest new-provider-from-change-test 6 | (let [change {:id "foo" 7 | :name "New Provider 1" 8 | :action "create-provider" 9 | :location {:lat 0 :lon 0}}] 10 | (is (= {:id "foo" 11 | :name "New Provider 1" 12 | :matches-filters true 13 | :location {:lat 0 :lon 0} 14 | :change change} 15 | (sut/new-provider-from-change change))))) 16 | -------------------------------------------------------------------------------- /client/test/planwise/client/utils_test.cljs: -------------------------------------------------------------------------------- 1 | (ns planwise.client.utils-test 2 | (:require [cljs.test :as t :refer-macros [deftest is]] 3 | [clojure.string :as str] 4 | [planwise.client.utils :as sut] 5 | [planwise.common :as common])) 6 | 7 | 8 | (deftest pluralize-test 9 | (is (= "1 site" (common/pluralize 1 "site"))) 10 | (is (= "2 sites" (common/pluralize 2 "site"))) 11 | (is (= "1 person" (common/pluralize 1 "person" "people"))) 12 | (is (= "2 people" (common/pluralize 2 "person" "people")))) 13 | 14 | (deftest update-by-id-test 15 | (let [coll [{:id 1 :name "foo"} 16 | {:id 2 :name "bar"} 17 | {:id 3 :name "quux"}]] 18 | (is (= [{:id 1 :name "foo"} 19 | {:id 2 :name "BAR"} 20 | {:id 3 :name "quux"}] 21 | (sut/update-by-id coll 2 update :name str/upper-case))))) 22 | -------------------------------------------------------------------------------- /common/README.md: -------------------------------------------------------------------------------- 1 | This directory contains Clojure code common to both the server and client applications. 2 | 3 | -------------------------------------------------------------------------------- /common/src/planwise/common.cljc: -------------------------------------------------------------------------------- 1 | (ns planwise.common 2 | (:require [clojure.string :as string :refer [lower-case]])) 3 | 4 | (defn is-budget 5 | [analysis-type] 6 | (= analysis-type "budget")) 7 | 8 | (def is-action (complement is-budget)) 9 | 10 | (defn project-has-budget? 11 | [project] 12 | (= "budget" (get-in project [:config :analysis-type]))) 13 | 14 | (def currency-symbol "$") 15 | 16 | (defn pluralize 17 | ([count singular] 18 | (pluralize count singular (str singular "s"))) 19 | ([count singular plural] 20 | (let [noun (if (= 1 count) singular plural)] 21 | (str count " " noun)))) 22 | 23 | (defn- get-project-unit 24 | [project path value lowercase?] 25 | ((if lowercase? lower-case identity) (or (not-empty (get-in project path)) value))) 26 | 27 | (defn get-consumer-unit 28 | ([project] 29 | (get-consumer-unit project true)) 30 | ([project lowercase?] 31 | (get-project-unit project [:config :demographics :unit-name] "total population" lowercase?))) 32 | 33 | (defn get-demand-unit 34 | ([project] 35 | (get-demand-unit project true)) 36 | ([project lowercase?] 37 | (get-project-unit project [:config :demographics :demand-unit] "targets" lowercase?))) 38 | 39 | (defn get-provider-unit 40 | ([project] 41 | (get-provider-unit project true)) 42 | ([project lowercase?] 43 | (get-project-unit project [:config :providers :provider-unit] "providers" lowercase?))) 44 | 45 | (defn get-capacity-unit 46 | ([project] 47 | (get-capacity-unit project true)) 48 | ([project lowercase?] 49 | (get-project-unit project [:config :providers :capacity-unit] "units" lowercase?))) 50 | 51 | (defn sanitize-tag 52 | [tag] 53 | (-> tag 54 | string/trim 55 | (string/replace #"\s+" "-") 56 | (string/replace #"[^a-zA-Z0-9.-]" ""))) 57 | -------------------------------------------------------------------------------- /common/src/planwise/model/coverage.cljc: -------------------------------------------------------------------------------- 1 | (ns planwise.model.coverage 2 | (:require [clojure.spec.alpha :as s])) 3 | 4 | (s/def ::algorithm keyword?) 5 | (s/def ::base-criteria (s/keys :req-un [::algorithm])) 6 | 7 | (defmulti criteria-algo :algorithm) 8 | (s/def ::coverage-criteria (s/multi-spec criteria-algo :algorithm)) 9 | 10 | (def buffer-distance-values 11 | (range 0 301 5)) 12 | 13 | 14 | ;; Specs ===================================================================== 15 | ;; 16 | 17 | (s/def ::driving-time #{30 60 90 120}) 18 | (s/def ::driving-friction-criteria (s/keys :req-un [::driving-time])) 19 | 20 | (s/def ::distance (set buffer-distance-values)) 21 | (s/def ::simple-buffer-criteria (s/keys :req-un [::distance])) 22 | 23 | (s/def ::walking-time #{60 120 180}) 24 | (s/def ::walking-friction-criteria (s/keys :req-un [::walking-time])) 25 | 26 | (s/def ::drive-walk-friction-criteria (s/or :walking-friction-criteria ::walking-friction-criteria 27 | :driving-friction-criteria ::driving-friction-criteria)) 28 | 29 | (defmethod criteria-algo :simple-buffer [_] 30 | (s/merge ::base-criteria ::simple-buffer-criteria)) 31 | (defmethod criteria-algo :walking-friction [_] 32 | (s/merge ::base-criteria ::walking-friction-criteria)) 33 | (defmethod criteria-algo :driving-friction [_] 34 | (s/merge ::base-criteria ::driving-friction-criteria)) 35 | (defmethod criteria-algo :drive-walk-friction [_] 36 | (s/merge ::base-criteria ::drive-walk-friction-criteria)) 37 | 38 | 39 | (defn valid-coverage-criteria? 40 | [algorithm filter-options] 41 | (s/valid? ::coverage-criteria 42 | (assoc filter-options :algorithm (keyword algorithm)))) 43 | -------------------------------------------------------------------------------- /cpp/.gitignore: -------------------------------------------------------------------------------- 1 | *.tif 2 | -------------------------------------------------------------------------------- /cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.7) 2 | project(planwise) 3 | 4 | include(FindBoost) 5 | set(Boost_USE_STATIC_LIBS ON) 6 | find_package(Boost COMPONENTS timer program_options filesystem) 7 | if(NOT Boost_FOUND) 8 | message(FATAL_ERROR "Boost not found") 9 | endif() 10 | 11 | find_program(GDAL_CONFIG gdal-config) 12 | if(NOT GDAL_CONFIG) 13 | message(FATAL_ERROR "GDAL not found") 14 | endif() 15 | 16 | exec_program(${GDAL_CONFIG} ARGS --cflags OUTPUT_VARIABLE GDAL_CFLAGS) 17 | exec_program(${GDAL_CONFIG} ARGS --libs OUTPUT_VARIABLE GDAL_LIBS) 18 | 19 | option(BENCHMARK "add timing benchmarks" OFF) 20 | 21 | if(BENCHMARK) 22 | add_definitions(-DBENCHMARK) 23 | endif() 24 | 25 | add_compile_options(${GDAL_CFLAGS}) 26 | include_directories(${Boost_INCLUDE_DIRS}) 27 | 28 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++11") 29 | 30 | add_executable(aggregate-population aggregate-population.cpp) 31 | target_link_libraries(aggregate-population ${GDAL_LIBS} ${Boost_LIBRARIES}) 32 | 33 | add_executable(walking-coverage walking-coverage.cpp) 34 | target_link_libraries(walking-coverage ${GDAL_LIBS} ${Boost_LIBRARIES}) 35 | -------------------------------------------------------------------------------- /cpp/aggregate-population: -------------------------------------------------------------------------------- 1 | bin-trampoline.sh -------------------------------------------------------------------------------- /cpp/bin-trampoline.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | base_dir=$(dirname $0) 4 | base_name=$(basename $0) 5 | bin_dir=$base_dir/build-`uname -s | tr '[A-Z]' '[a-z]'`-`uname -m` 6 | 7 | binary=$bin_dir/$base_name 8 | 9 | if [ ! -x $binary ]; then 10 | echo "ERROR: Binary executable $binary is missing!" 11 | echo "Compile it using scripts/build-binaries." 12 | exit 1 13 | fi 14 | 15 | exec $binary $* 16 | -------------------------------------------------------------------------------- /cpp/walking-coverage: -------------------------------------------------------------------------------- 1 | bin-trampoline.sh -------------------------------------------------------------------------------- /data/populations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/instedd/planwise/6da765a09481c6b86169dab585bbea71aabe120e/data/populations/.gitkeep -------------------------------------------------------------------------------- /data/regions/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/instedd/planwise/6da765a09481c6b86169dab585bbea71aabe120e/data/regions/.gitkeep -------------------------------------------------------------------------------- /data/scenarios/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/instedd/planwise/6da765a09481c6b86169dab585bbea71aabe120e/data/scenarios/.gitkeep -------------------------------------------------------------------------------- /dev/resources/dev.edn: -------------------------------------------------------------------------------- 1 | {:duct.core/environment :development 2 | :duct.core/include ["planwise/config"] 3 | 4 | :duct.module/sql 5 | {:database-url #duct/env ["DATABASE_URL" Str :or "jdbc:postgresql://localhost:5433/planwise?user=planwise&password=planwise"]} 6 | 7 | :planwise.auth/base-secret 8 | "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" 9 | 10 | :planwise.component/runner 11 | {:bin #duct/env ["BIN_PATH" Str :or "cpp/"]} 12 | 13 | :planwise.component/mailer 14 | {:config {:mock? true}} 15 | 16 | :planwise.component/maps 17 | {:config {}}} 18 | -------------------------------------------------------------------------------- /dev/src/dev.clj: -------------------------------------------------------------------------------- 1 | (ns dev 2 | (:refer-clojure :exclude [test]) 3 | (:require [clojure.repl :refer :all] 4 | [fipp.edn :refer [pprint]] 5 | [clojure.tools.namespace.repl :refer [refresh]] 6 | [clojure.java.io :as io] 7 | [duct.core :as duct] 8 | [duct.core.repl :as duct-repl] 9 | [eftest.runner :as eftest] 10 | [clojure.spec.alpha :as s] 11 | [integrant.core :as ig] 12 | [integrant.repl :refer [clear halt init prep]] 13 | [integrant.repl.state :refer [config system]] 14 | [taoensso.timbre :as timbre] 15 | [planwise.config] 16 | [planwise.repl :refer :all] 17 | [planwise.virgil])) 18 | 19 | (duct/load-hierarchy) 20 | 21 | ;; Logging configuration for development 22 | (timbre/merge-config! {:level :debug 23 | :ns-blacklist ["com.zaxxer.hikari.*" 24 | "org.apache.http.*" 25 | "org.eclipse.jetty.*" 26 | "org.openid4java.*"]}) 27 | 28 | (defn read-config [] 29 | (duct/read-config (io/resource "dev.edn"))) 30 | 31 | (clojure.tools.namespace.repl/set-refresh-dirs "dev/src" "src" "test") 32 | (planwise.virgil/set-java-source-dirs! ["java"]) 33 | 34 | (when (io/resource "local.clj") 35 | (load "local")) 36 | 37 | (integrant.repl/set-prep! (comp duct/prep read-config)) 38 | 39 | (s/check-asserts true) 40 | -------------------------------------------------------------------------------- /dev/src/planwise/repl.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.repl 2 | (:refer-clojure :exclude [test]) 3 | (:require [integrant.core :as ig] 4 | [integrant.repl :as igr] 5 | [integrant.repl.state :refer [config system]] 6 | [eftest.runner :as eftest] 7 | [planwise.database :as database] 8 | [planwise.tasks.build-icons :as build-icons] 9 | [ragtime.core :as ragtime] 10 | [ragtime.jdbc :as rag-jdbc] 11 | [duct.migrator.ragtime :as dmr] 12 | [buddy.core.nonce :as nonce] 13 | [planwise.virgil]) 14 | (:import org.apache.commons.codec.binary.Hex)) 15 | 16 | (defn db 17 | [] 18 | (let [[_ database] (ig/find-derived-1 system :duct.database/sql)] 19 | database)) 20 | 21 | (defn run-tests 22 | [tests] 23 | (eftest/run-tests tests {:multithread? false})) 24 | 25 | (defn test 26 | ([] 27 | (run-tests (eftest/find-tests "test"))) 28 | ([pattern] 29 | (let [pattern (re-pattern pattern) 30 | filterer (fn [var-name] 31 | (->> (str var-name) 32 | (re-find pattern))) 33 | tests (->> (eftest/find-tests "test") 34 | (filter filterer))] 35 | (run-tests tests)))) 36 | 37 | (defn load-sql 38 | [] 39 | (database/load-sql-functions (db))) 40 | 41 | (defn rollback-1 42 | [] 43 | (let [[_ index] (ig/find-derived-1 system :duct.migrator/ragtime) 44 | [_ logger] (ig/find-derived-1 system :duct/logger) 45 | store (rag-jdbc/sql-database (:spec (db))) 46 | options {:reporter (dmr/logger-reporter logger)}] 47 | (ragtime/rollback-last store index 1 options))) 48 | 49 | (defn build-icons 50 | [] 51 | (build-icons/process-svgs)) 52 | 53 | (defn gen-base-secret 54 | [] 55 | (-> (nonce/random-bytes 32) 56 | Hex/encodeHex 57 | String.)) 58 | 59 | (defn go 60 | [] 61 | (igr/prep) 62 | (igr/init)) 63 | 64 | (defn compile-java 65 | [] 66 | (planwise.virgil/recompile-all-java)) 67 | 68 | (defn reset 69 | [] 70 | (planwise.virgil/refresh) 71 | (igr/reset)) 72 | -------------------------------------------------------------------------------- /dev/src/user.clj: -------------------------------------------------------------------------------- 1 | (ns user) 2 | 3 | (defn dev 4 | "Load and switch to the 'dev' namespace." 5 | [] 6 | (require 'dev) 7 | (in-ns 'dev) 8 | :loaded) 9 | -------------------------------------------------------------------------------- /doc/coverage_algo.md: -------------------------------------------------------------------------------- 1 | # Coverage algorithms 2 | 3 | Coverage algorithms should calculate the coverage area of a site in a geographic 4 | point given some criteria. Each algorithm can apply a different strategy to 5 | compute the resulting coverage and accept different criteria parameters. 6 | 7 | For example, the coverage for car driving would accept a maximum driving time 8 | and use OSM road network data. The coverage for walking distance would also 9 | accept a maximum travel time but use a capped raster friction layer. A 10 | hipotetical FM radio coverage algorithm would take a minimum signal strength as 11 | a cut criteria and use the digital elevation model for the computation. 12 | 13 | The expected output format for the coverage is both in vector format as a 14 | polygon and in raster format. The algorithms should accept parameters 15 | controlling the resolution and/or quality of the output. Eg. a simplification 16 | threshold for vector responses, grid resolution and origin/snap coordinates for 17 | raster responses and spatial reference system for both. 18 | 19 | Several instances of a single algorithm can be declared, each with a different 20 | data set, for example to compute coverages in different areas, or with different 21 | precision. Hence, each instance of an algorithm is only aplicable to a 22 | restricted geographical area. The choice of data set may also constrain the 23 | allowed criteria parameters, depending on the algorithm implementation. 24 | -------------------------------------------------------------------------------- /docker-cloud.sample.yml: -------------------------------------------------------------------------------- 1 | db: 2 | image: starefossen/pgrouting:9.4-2.1-2.1 3 | autorestart: always 4 | environment: 5 | POSTGRES_PASSWORD: PLANWISE_POSTGRES_PASSWORD 6 | POSTGRES_USER: planwise 7 | POSTGRES_DB: routing 8 | 9 | web: 10 | image: instedd/planwise:latest 11 | autorestart: always 12 | autoredeploy: true 13 | command: /bin/sh -c "/app/scripts/migrate && java -jar $JAR_PATH" 14 | environment: 15 | DATABASE_URL: "jdbc:postgresql://db/routing?user=planwise&password=PLANWISE_POSTGRES_PASSWORD" 16 | POSTGRES_PASSWORD: PLANWISE_POSTGRES_PASSWORD 17 | POSTGRES_USER: planwise 18 | POSTGRES_DB: routing 19 | POSTGRES_HOST: db 20 | volumes_from: 21 | - mapserver-data 22 | 23 | mapcache: 24 | image: instedd/planwise-mapcache:kenya 25 | autorestart: always 26 | autoredeploy: true 27 | 28 | mapserver: 29 | image: instedd/planwise-mapserver:kenya 30 | autorestart: always 31 | autoredeploy: true 32 | volumes_from: 33 | - mapserver-data 34 | 35 | mapserver-data: 36 | image: instedd/planwise-mapserver:kenya-data 37 | autoredeploy: true 38 | command: /bin/true 39 | -------------------------------------------------------------------------------- /docker-compose.ci.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | app: 5 | environment: 6 | DATABASE_URL: "jdbc:postgresql://db/planwise?user=postgres&password=planwise" 7 | TEST_DATABASE_URL: "jdbc:postgresql://db/planwise-test?user=postgres&password=planwise" 8 | POSTGRES_USER: postgres 9 | 10 | db: 11 | environment: 12 | POSTGRES_USER: postgres 13 | -------------------------------------------------------------------------------- /docker-compose.production.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | app: 5 | container_name: planwise-prod-app 6 | image: instedd/planwise:latest 7 | environment: 8 | CALCULATE_DEMAND: 'true' 9 | DATABASE_URL: "jdbc:postgresql://db/planwise?user=planwise&password=planwise" 10 | MAIL_SENDER: planwise-test@instedd.org 11 | PORT: 3000 12 | POSTGRES_DB: planwise 13 | POSTGRES_HOST: db 14 | POSTGRES_PASSWORD: planwise 15 | POSTGRES_USER: planwise 16 | RASTER_ISOCHRONES: 'true' 17 | env_file: ".docker-env" 18 | volumes: 19 | - data:/data 20 | depends_on: 21 | - db 22 | ports: 23 | - "3000:3000" 24 | 25 | db: 26 | container_name: planwise-prod-db 27 | image: starefossen/pgrouting:10.1-2.4-2.5 28 | environment: 29 | POSTGRES_PASSWORD: planwise 30 | POSTGRES_USER: planwise 31 | POSTGRES_DB: routing 32 | volumes: 33 | - db:/var/lib/postgresql/data 34 | 35 | mapcache: 36 | container_name: planwise-prod-mapcache 37 | image: instedd/planwise-mapcache:kenya 38 | pid: host 39 | 40 | mapserver: 41 | container_name: planwise-prod-mapserver 42 | image: instedd/planwise-mapserver:kenya 43 | pid: host 44 | volumes: 45 | - data:/data 46 | 47 | volumes: 48 | db: 49 | data: 50 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | target: base 8 | platform: linux/amd64 9 | volumes: 10 | - ".:/app" 11 | - m2:/root/.m2 12 | - "lein:/root/.lein" 13 | depends_on: 14 | - db 15 | env_file: 16 | - ./docker-env 17 | command: "lein repl :headless" 18 | ports: 19 | - "47480:47480" 20 | - "3000:3000" 21 | 22 | client: 23 | build: 24 | context: . 25 | target: base 26 | platform: linux/amd64 27 | volumes: 28 | - ".:/app" 29 | - m2:/root/.m2 30 | - node_modules:/app/client/node_modules 31 | working_dir: "/app/client" 32 | command: "sh -c 'npm install && npm run watch'" 33 | ports: 34 | - "9630:9630" 35 | 36 | db: 37 | image: starefossen/pgrouting:10.1-2.4-2.5 38 | environment: 39 | POSTGRES_PASSWORD: planwise 40 | POSTGRES_USER: planwise 41 | POSTGRES_DB: planwise 42 | volumes: 43 | - db:/var/lib/postgresql/data 44 | ports: 45 | - "5433:5432" 46 | 47 | mapcache: 48 | image: camptocamp/mapcache:1.4 49 | volumes: 50 | - "./mapserver/mapcache.xml:/mapcache/mapcache.xml:ro" 51 | depends_on: 52 | - mapserver 53 | ports: 54 | - "5002:80" 55 | 56 | mapserver: 57 | image: camptocamp/mapserver:7.0 58 | volumes: 59 | - "./data:/data:ro" 60 | - "./mapserver/planwise.map:/etc/mapserver/planwise.map:ro" 61 | ports: 62 | - "5001:80" 63 | 64 | tools: 65 | image: instedd/planwise-tools 66 | platform: linux/amd64 67 | build: 68 | context: scripts 69 | env_file: 70 | - ./docker-env 71 | volumes: 72 | - "./data:/data" 73 | depends_on: 74 | - db 75 | 76 | volumes: 77 | db: 78 | lein: 79 | m2: 80 | node_modules: 81 | -------------------------------------------------------------------------------- /docker-env: -------------------------------------------------------------------------------- 1 | DATABASE_URL="jdbc:postgresql://db/planwise?user=planwise&password=planwise" 2 | TEST_DATABASE_URL="jdbc:postgresql://db/planwise-test?user=planwise&password=planwise" 3 | POSTGRES_HOST=db 4 | POSTGRES_PORT=5432 5 | POSTGRES_USER=planwise 6 | POSTGRES_PASSWORD=planwise 7 | POSTGRES_DB=planwise 8 | BIN_PATH=cpp/ 9 | -------------------------------------------------------------------------------- /env: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export POSTGRES_HOST=localhost 4 | export POSTGRES_PORT=5433 5 | export POSTGRES_USER=planwise 6 | export POSTGRES_PASSWORD=planwise 7 | export POSTGRES_DB=planwise 8 | export RASTER_ISOCHRONES=true 9 | 10 | -------------------------------------------------------------------------------- /mapserver/.dockerignore: -------------------------------------------------------------------------------- 1 | data 2 | -------------------------------------------------------------------------------- /mapserver/.gitignore: -------------------------------------------------------------------------------- 1 | *.png 2 | -------------------------------------------------------------------------------- /mapserver/Dockerfile.mapcache: -------------------------------------------------------------------------------- 1 | FROM camptocamp/mapcache:1.4 2 | 3 | # Add mapcache.xml 4 | ADD mapcache.xml /mapcache/mapcache.xml 5 | 6 | EXPOSE 80 7 | -------------------------------------------------------------------------------- /mapserver/Dockerfile.mapserver: -------------------------------------------------------------------------------- 1 | FROM camptocamp/mapserver:7.0 2 | 3 | ADD planwise.map /etc/mapserver/planwise.map 4 | 5 | VOLUME /data 6 | 7 | EXPOSE 80 8 | -------------------------------------------------------------------------------- /mapserver/docker-compose.yml: -------------------------------------------------------------------------------- 1 | mapcache: 2 | image: instedd/planwise-mapcache 3 | # build: . 4 | # dockerfile: Dockerfile.mapcache 5 | volumes: 6 | - "./mapcache.xml:/mapcache/mapcache.xml:ro" 7 | links: 8 | - mapserver 9 | ports: 10 | - "5002:80" 11 | 12 | mapserver: 13 | image: instedd/planwise-mapserver 14 | # build: . 15 | # dockerfile: Dockerfile.mapserver 16 | volumes: 17 | - "../data:/data:ro" 18 | - "./planwise.map:/etc/mapserver/mapserver.map:ro" 19 | ports: 20 | - "5001:80" 21 | -------------------------------------------------------------------------------- /mapserver/partition.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | width = 9601 4 | height = 12179 5 | tilesize = 2048 6 | filename = 'KEN_popmap15_v2b' 7 | 8 | os.chdir('./data') 9 | 10 | for i in range(0, width, tilesize): 11 | for j in range(0, height, tilesize): 12 | size_x = (width - i) if i + tilesize > width else tilesize 13 | size_y = (height - j) if j + tilesize > height else tilesize 14 | gdaltranString = "gdal_translate -of GTIFF -srcwin {0} {1} {2} {3} {4}.tif {4}_{0}_{1}.tif".format(i, j, size_x, size_y, filename) 15 | os.system(gdaltranString) 16 | os.system("gdaladdo {0}_{1}_{2}.tif -r average 2 4 8 16 32".format(filename, i, j)) 17 | 18 | os.system("gdaltindex {0}.shp {0}_*".format(filename)) 19 | -------------------------------------------------------------------------------- /resources/migrations/001-extensions.up.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS postgis; 2 | CREATE EXTENSION IF NOT EXISTS pgrouting; 3 | -------------------------------------------------------------------------------- /resources/migrations/002-ways_nodes.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS ways_nodes; 2 | -------------------------------------------------------------------------------- /resources/migrations/002-ways_nodes.up.sql: -------------------------------------------------------------------------------- 1 | -- Creates a new table `ways_nodes` to contain all the points for all the 2 | -- `ways` table. Used for alpha shape computation. 3 | 4 | CREATE TABLE ways_nodes ( 5 | id SERIAL, 6 | gid BIGINT NOT NULL, 7 | lon NUMERIC(11,8), 8 | lat NUMERIC(11,8)); 9 | 10 | CREATE INDEX ways_nodes_gid ON ways_nodes(gid); 11 | -------------------------------------------------------------------------------- /resources/migrations/003-facilities.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS facilities; 2 | -------------------------------------------------------------------------------- /resources/migrations/003-facilities.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE facilities ( 2 | id BIGINT PRIMARY KEY, 3 | name VARCHAR(255) NOT NULL, 4 | lat NUMERIC(11,8) NOT NULL, 5 | lon NUMERIC(11,8) NOT NULL, 6 | the_geom GEOMETRY(Point, 4326) NOT NULL); 7 | 8 | CREATE INDEX facilities_the_geom_idx ON facilities USING gist (the_geom); 9 | -------------------------------------------------------------------------------- /resources/migrations/004-preprocess_functions.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS facilities_polygons; 2 | -------------------------------------------------------------------------------- /resources/migrations/004-preprocess_functions.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE facilities_polygons ( 2 | facility_id integer not null, 3 | threshold integer not null, 4 | method varchar not null, 5 | the_geom geometry(polygon, 4326) 6 | ); 7 | -------------------------------------------------------------------------------- /resources/migrations/005-projects.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS projects; 2 | -------------------------------------------------------------------------------- /resources/migrations/005-projects.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE projects ( 2 | id BIGSERIAL PRIMARY KEY, 3 | goal VARCHAR(255) NOT NULL); 4 | -------------------------------------------------------------------------------- /resources/migrations/006-facilities_type_column.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE facilities DROP COLUMN type; 2 | -------------------------------------------------------------------------------- /resources/migrations/006-facilities_type_column.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE facilities ADD COLUMN type varchar(255); 2 | -------------------------------------------------------------------------------- /resources/migrations/007-users.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users; 2 | -------------------------------------------------------------------------------- /resources/migrations/007-users.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id BIGSERIAL PRIMARY KEY, 3 | email VARCHAR(255) UNIQUE NOT NULL, 4 | full_name VARCHAR(255), 5 | last_login TIMESTAMP WITH TIME ZONE, 6 | created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT current_timestamp 7 | ); 8 | 9 | CREATE INDEX users_email_idx ON users (email); 10 | -------------------------------------------------------------------------------- /resources/migrations/008-regions.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS regions; 2 | -------------------------------------------------------------------------------- /resources/migrations/008-regions.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE regions ( 2 | id BIGSERIAL PRIMARY KEY, 3 | country VARCHAR(255) NOT NULL, 4 | name VARCHAR(255) NOT NULL, 5 | admin_level INT, 6 | the_geom geometry(MultiPolygon,4326) 7 | ); 8 | 9 | CREATE INDEX regions_country_idx ON regions (country); 10 | -------------------------------------------------------------------------------- /resources/migrations/009-add_region_to_projects.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects DROP COLUMN region_id; 2 | -------------------------------------------------------------------------------- /resources/migrations/009-add_region_to_projects.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects ADD COLUMN region_id BIGINT NULL REFERENCES regions(id); 2 | -------------------------------------------------------------------------------- /resources/migrations/010-tokens.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS tokens; 2 | -------------------------------------------------------------------------------- /resources/migrations/010-tokens.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE tokens ( 2 | id BIGSERIAL PRIMARY KEY, 3 | user_id BIGINT NOT NULL REFERENCES users(id), 4 | scope VARCHAR(255) NOT NULL, 5 | token VARCHAR(255) NOT NULL, 6 | refresh_token VARCHAR(255) NOT NULL, 7 | expires TIMESTAMP WITH TIME ZONE 8 | ); 9 | -------------------------------------------------------------------------------- /resources/migrations/011-facility_types.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE facility_types; 2 | 3 | ALTER TABLE facilities DROP COLUMN type_id; 4 | ALTER TABLE facilities ADD COLUMN type TYPE varchar(255); 5 | -------------------------------------------------------------------------------- /resources/migrations/011-facility_types.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE facility_types ( 2 | id SERIAL PRIMARY KEY, 3 | name VARCHAR(255) NOT NULL); 4 | 5 | ALTER TABLE facilities DROP COLUMN type; 6 | ALTER TABLE facilities ADD COLUMN type_id INTEGER REFERENCES facility_types(id); 7 | 8 | -------------------------------------------------------------------------------- /resources/migrations/012-add_facilities_count_to_projects.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects DROP COLUMN facilities_count; 2 | -------------------------------------------------------------------------------- /resources/migrations/012-add_facilities_count_to_projects.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects ADD COLUMN facilities_count integer; 2 | -------------------------------------------------------------------------------- /resources/migrations/013-process_single_facility.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE facilities_polygons DROP COLUMN starting_node; 2 | -------------------------------------------------------------------------------- /resources/migrations/014-projects_filters_and_stats.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects ADD COLUMN facilities_count integer; 2 | ALTER TABLE projects DROP COLUMN stats; 3 | ALTER TABLE projects DROP COLUMN filters; 4 | -------------------------------------------------------------------------------- /resources/migrations/014-projects_filters_and_stats.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects ADD COLUMN filters text; 2 | ALTER TABLE projects ADD COLUMN stats text; 3 | ALTER TABLE projects DROP COLUMN facilities_count; 4 | -------------------------------------------------------------------------------- /resources/migrations/015-apply_traffic_factor.down.sql: -------------------------------------------------------------------------------- 1 | DO $$ 2 | BEGIN 3 | IF EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ways') THEN 4 | UPDATE ways 5 | SET cost_s = length_m / (maxspeed_forward * 5 / 18) * 1.0; 6 | END IF; 7 | END; 8 | $$ 9 | -------------------------------------------------------------------------------- /resources/migrations/015-apply_traffic_factor.up.sql: -------------------------------------------------------------------------------- 1 | DO $$ 2 | BEGIN 3 | IF EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ways') THEN 4 | UPDATE ways 5 | SET cost_s = length_m / (maxspeed_forward * 5.0 / 18) * 1.5; 6 | END IF; 7 | END; 8 | $$ 9 | -------------------------------------------------------------------------------- /resources/migrations/016-add_preview_geom_to_regions.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE regions 2 | DROP COLUMN IF EXISTS preview_geom; 3 | -------------------------------------------------------------------------------- /resources/migrations/016-add_preview_geom_to_regions.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE regions 2 | ADD COLUMN preview_geom geometry(MultiPolygon,4326); 3 | 4 | DO $$ 5 | BEGIN 6 | PERFORM calculate_regions_previews(); 7 | END; 8 | $$ 9 | -------------------------------------------------------------------------------- /resources/migrations/017-facilities_polygons_index.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX facilities_polygons_facility_method_threshold; 2 | -------------------------------------------------------------------------------- /resources/migrations/017-facilities_polygons_index.up.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX facilities_polygons_facility_method_threshold 2 | ON facilities_polygons(facility_id, method, threshold); 3 | -------------------------------------------------------------------------------- /resources/migrations/018-add_owner_user_to_projects.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects DROP COLUMN owner_id; 2 | -------------------------------------------------------------------------------- /resources/migrations/018-add_owner_user_to_projects.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects ADD COLUMN owner_id BIGINT REFERENCES users(id); 2 | -------------------------------------------------------------------------------- /resources/migrations/019-add_id_to_facilities_polygons.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE facilities_polygons 2 | DROP COLUMN IF EXISTS id; 3 | -------------------------------------------------------------------------------- /resources/migrations/019-add_id_to_facilities_polygons.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE facilities_polygons ADD COLUMN id BIGSERIAL PRIMARY KEY; 2 | -------------------------------------------------------------------------------- /resources/migrations/020-facility_polygons_regions.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS facilities_polygons_regions; 2 | -------------------------------------------------------------------------------- /resources/migrations/020-facility_polygons_regions.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE facilities_polygons_regions ( 2 | facility_polygon_id BIGINT REFERENCES facilities_polygons(id) ON DELETE CASCADE, 3 | region_id INT REFERENCES regions(id) ON DELETE CASCADE, 4 | area FLOAT, 5 | population INT); 6 | 7 | CREATE INDEX facilities_polygons_regions_facility_polygon_id 8 | ON facilities_polygons_regions(facility_polygon_id); 9 | 10 | CREATE INDEX facilities_polygons_regions_region_id 11 | ON facilities_polygons_regions(region_id); 12 | 13 | INSERT INTO facilities_polygons_regions(facility_polygon_id, region_id, area) 14 | SELECT fp.id, r.id, ST_Area(ST_Intersection(fp.the_geom, r.the_geom)) 15 | FROM regions AS r INNER JOIN facilities_polygons AS fp 16 | ON ST_Intersects(fp.the_geom, r.the_geom); 17 | -------------------------------------------------------------------------------- /resources/migrations/021-facility_polygons_data.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE facilities_polygons 2 | DROP COLUMN IF EXISTS area; 3 | 4 | ALTER TABLE facilities_polygons 5 | DROP COLUMN IF EXISTS population; 6 | -------------------------------------------------------------------------------- /resources/migrations/021-facility_polygons_data.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE facilities_polygons 2 | ADD COLUMN area FLOAT; 3 | 4 | ALTER TABLE facilities_polygons 5 | ADD COLUMN population INT; 6 | -------------------------------------------------------------------------------- /resources/migrations/022-add_population_to_regions.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE regions 2 | DROP COLUMN IF EXISTS total_population; 3 | -------------------------------------------------------------------------------- /resources/migrations/022-add_population_to_regions.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE regions 2 | ADD COLUMN total_population INT NULL; 3 | -------------------------------------------------------------------------------- /resources/migrations/023-add_facilities_polygon_constraint.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE facilities_polygons 2 | DROP CONSTRAINT facilities_polygons_facility_id_fkey; 3 | 4 | -------------------------------------------------------------------------------- /resources/migrations/023-add_facilities_polygon_constraint.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE facilities_polygons 2 | ADD CONSTRAINT facilities_polygons_facility_id_fkey 3 | FOREIGN KEY (facility_id) 4 | REFERENCES facilities(id) 5 | ON DELETE CASCADE; 6 | -------------------------------------------------------------------------------- /resources/migrations/024-add_facilities_processing_status.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE facilities 2 | DROP COLUMN IF EXISTS processing_status; 3 | -------------------------------------------------------------------------------- /resources/migrations/024-add_facilities_processing_status.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE facilities 2 | ADD COLUMN processing_status VARCHAR; 3 | 4 | UPDATE facilities AS f 5 | SET processing_status = 6 | CASE 7 | WHEN EXISTS (SELECT 1 FROM facilities_polygons fp WHERE fp.facility_id = f.id LIMIT 1) 8 | THEN 'ok' 9 | ELSE 10 | 'no-road-network' 11 | END 12 | WHERE f.processing_status IS NULL; 13 | 14 | -- The following version indeed calculates if the facility is too far from the 15 | -- road network, but takes too long to run. So we use the version above, that 16 | -- assumes that, if we are running migrations, there are no facilities pending 17 | -- processing, so they have polygons iif they are close to the road network. 18 | 19 | -- UPDATE facilities AS f 20 | -- SET processing_status = 21 | -- CASE 22 | -- WHEN ST_Distance( 23 | -- ST_GeogFromWKB(f.the_geom), 24 | -- ST_GeogFromWKB(( 25 | -- SELECT wvp.the_geom 26 | -- FROM ways_vertices_pgr AS wvp 27 | -- WHERE wvp.id = closest_node(f.the_geom) 28 | -- LIMIT 1 29 | -- ))) > 1000 THEN 'no-road-network' 30 | -- ELSE 'ok' 31 | -- END 32 | -- WHERE f.processing_status IS NULL; 33 | -------------------------------------------------------------------------------- /resources/migrations/025-add_max_population_to_regions.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE regions 2 | DROP COLUMN IF EXISTS max_population; 3 | -------------------------------------------------------------------------------- /resources/migrations/025-add_max_population_to_regions.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE regions 2 | ADD COLUMN max_population INT NULL; 3 | -------------------------------------------------------------------------------- /resources/migrations/026-datasets.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS datasets; 2 | -------------------------------------------------------------------------------- /resources/migrations/026-datasets.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS datasets ( 2 | id BIGSERIAL PRIMARY KEY, 3 | name VARCHAR(255) NOT NULL, 4 | description VARCHAR(255), 5 | owner_id BIGINT NOT NULL REFERENCES users(id), 6 | collection_id BIGINT, 7 | import_mappings TEXT, 8 | import_result TEXT 9 | ); 10 | -------------------------------------------------------------------------------- /resources/migrations/027-dataset_references.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE facilities DROP COLUMN id CASCADE; 2 | ALTER TABLE facilities RENAME COLUMN site_id TO id; 3 | ALTER TABLE facilities DROP COLUMN IF EXISTS dataset_id; 4 | ALTER TABLE facility_types DROP COLUMN IF EXISTS dataset_id; 5 | ALTER TABLE projects DROP COLUMN IF EXISTS dataset_id; 6 | -------------------------------------------------------------------------------- /resources/migrations/027-dataset_references.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE facility_types 2 | ADD COLUMN dataset_id BIGINT NOT NULL 3 | REFERENCES datasets(id) ON DELETE CASCADE; 4 | 5 | ALTER TABLE facilities 6 | DROP CONSTRAINT IF EXISTS facilities_pkey CASCADE; 7 | 8 | ALTER TABLE facilities 9 | RENAME COLUMN id TO site_id; 10 | 11 | ALTER TABLE facilities 12 | ADD COLUMN id SERIAL PRIMARY KEY; 13 | 14 | ALTER TABLE facilities 15 | ADD COLUMN dataset_id BIGINT NOT NULL 16 | REFERENCES datasets(id) ON DELETE CASCADE; 17 | 18 | ALTER TABLE facilities_polygons 19 | ADD CONSTRAINT facilities_polygons_facility_id_pkey 20 | FOREIGN KEY (facility_id) 21 | REFERENCES facilities(id) ON DELETE CASCADE; 22 | 23 | ALTER TABLE projects 24 | ADD COLUMN dataset_id BIGINT NOT NULL 25 | REFERENCES datasets(id) ON DELETE RESTRICT; 26 | 27 | CREATE INDEX facilities_dataset_id_idx ON facilities(dataset_id); 28 | CREATE INDEX projects_dataset_id_idx ON projects(dataset_id); 29 | -------------------------------------------------------------------------------- /resources/migrations/028-project_share.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS project_shares; 2 | 3 | ALTER TABLE projects 4 | DROP COLUMN IF EXISTS share_token; 5 | -------------------------------------------------------------------------------- /resources/migrations/028-project_share.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS project_shares ( 2 | project_id BIGINT REFERENCES projects(id) ON DELETE CASCADE, 3 | user_id BIGINT REFERENCES users(id) ON DELETE CASCADE, 4 | PRIMARY KEY (project_id, user_id) 5 | ); 6 | 7 | CREATE INDEX project_shares_project_id_idx ON project_shares(project_id); 8 | CREATE INDEX project_shares_user_id_idx ON project_shares(user_id); 9 | 10 | ALTER TABLE projects 11 | ADD COLUMN share_token VARCHAR(60) NULL; 12 | 13 | CREATE EXTENSION IF NOT EXISTS pgcrypto; 14 | 15 | UPDATE projects SET share_token = gen_random_uuid() 16 | WHERE share_token IS NULL; 17 | 18 | CREATE INDEX proje_sharing_token_idx ON projects(share_token); 19 | -------------------------------------------------------------------------------- /resources/migrations/029-add_raster_pixel_area_to_regions.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE regions 2 | DROP COLUMN IF EXISTS raster_pixel_area; 3 | -------------------------------------------------------------------------------- /resources/migrations/029-add_raster_pixel_area_to_regions.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE regions 2 | ADD COLUMN raster_pixel_area INT NULL; 3 | -------------------------------------------------------------------------------- /resources/migrations/030-facilities-type-id-contraint.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE facilities ALTER COLUMN type_id DROP NOT NULL; 2 | -------------------------------------------------------------------------------- /resources/migrations/030-facilities-type-id-contraint.up.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM facilities WHERE type_id IS NULL; 2 | ALTER TABLE facilities ALTER COLUMN type_id SET NOT NULL; 3 | -------------------------------------------------------------------------------- /resources/migrations/031-add_import_job_to_datasets.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE datasets 2 | DROP COLUMN IF EXISTS import_job; 3 | -------------------------------------------------------------------------------- /resources/migrations/031-add_import_job_to_datasets.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE datasets 2 | ADD COLUMN import_job TEXT; 3 | -------------------------------------------------------------------------------- /resources/migrations/032-add_projects_state.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects DROP COLUMN state; 2 | -------------------------------------------------------------------------------- /resources/migrations/032-add_projects_state.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects ADD COLUMN state TEXT; 2 | -------------------------------------------------------------------------------- /resources/migrations/033-add_capacity_to_facilities.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE facilities DROP COLUMN capacity; 2 | -------------------------------------------------------------------------------- /resources/migrations/033-add_capacity_to_facilities.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE facilities ADD COLUMN capacity INTEGER DEFAULT 0; 2 | -------------------------------------------------------------------------------- /resources/migrations/034-add_code_to_facility_types.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE facility_types DROP COLUMN IF EXISTS code; 2 | -------------------------------------------------------------------------------- /resources/migrations/034-add_code_to_facility_types.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE facility_types ADD COLUMN code VARCHAR(255); 2 | -------------------------------------------------------------------------------- /resources/migrations/035-datasets2.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS sites2; 2 | 3 | DROP TABLE IF EXISTS datasets2; 4 | -------------------------------------------------------------------------------- /resources/migrations/035-datasets2.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS datasets2 ( 2 | id BIGSERIAL PRIMARY KEY, 3 | name VARCHAR(255) NOT NULL, 4 | "last-version" INT, 5 | "owner-id" BIGINT NOT NULL REFERENCES users(id) 6 | ); 7 | 8 | CREATE TABLE IF NOT EXISTS sites2 ( 9 | id INT NOT NULL, 10 | version INT, 11 | "dataset-id" BIGINT NOT NULL REFERENCES datasets2(id), 12 | name VARCHAR(255) NOT NULL, 13 | lat NUMERIC(11,8) NOT NULL, 14 | lon NUMERIC(11,8) NOT NULL, 15 | the_geom GEOMETRY(Point, 4326) NOT NULL, 16 | capacity INT, 17 | type TEXT, 18 | tags TEXT 19 | ); 20 | -------------------------------------------------------------------------------- /resources/migrations/036-projects2.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS projects2; 2 | -------------------------------------------------------------------------------- /resources/migrations/036-projects2.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS projects2 ( 2 | id BIGSERIAL PRIMARY KEY, 3 | "owner-id" BIGINT NOT NULL REFERENCES users(id), 4 | name VARCHAR(255) NOT NULL 5 | ); 6 | -------------------------------------------------------------------------------- /resources/migrations/037-add-config-to-projects2.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects2 DROP COLUMN config; 2 | -------------------------------------------------------------------------------- /resources/migrations/037-add-config-to-projects2.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects2 ADD COLUMN config TEXT; 2 | -------------------------------------------------------------------------------- /resources/migrations/038-add_coverage_to_datasets2.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE datasets2 DROP COLUMN "coverage-algorithm"; 2 | -------------------------------------------------------------------------------- /resources/migrations/038-add_coverage_to_datasets2.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE datasets2 ADD COLUMN "coverage-algorithm" VARCHAR(100); 2 | -------------------------------------------------------------------------------- /resources/migrations/039-sites2_coverage.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "sites2_coverage"; 2 | 3 | ALTER TABLE "sites2" DROP COLUMN "processing-status"; 4 | ALTER TABLE "sites2" DROP COLUMN "id"; 5 | ALTER TABLE "sites2" RENAME COLUMN "source-id" TO "id"; 6 | -------------------------------------------------------------------------------- /resources/migrations/039-sites2_coverage.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "sites2" RENAME COLUMN "id" TO "source-id"; 2 | ALTER TABLE "sites2" ADD COLUMN "id" SERIAL PRIMARY KEY; 3 | ALTER TABLE "sites2" ADD COLUMN "processing-status" VARCHAR; 4 | 5 | CREATE TABLE "sites2_coverage" ( 6 | "id" SERIAL PRIMARY KEY, 7 | "site-id" INTEGER REFERENCES sites2(id), 8 | "algorithm" VARCHAR(100) NOT NULL, 9 | "options" VARCHAR NOT NULL, 10 | "geom" GEOMETRY(Polygon,4326), 11 | "raster" VARCHAR 12 | ); 13 | 14 | -------------------------------------------------------------------------------- /resources/migrations/040-population_sources.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS population_sources; 2 | -------------------------------------------------------------------------------- /resources/migrations/040-population_sources.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS population_sources ( 2 | id BIGSERIAL PRIMARY KEY, 3 | name VARCHAR(255) NOT NULL, 4 | tif_file VARCHAR(255) NOT NULL 5 | ); 6 | -------------------------------------------------------------------------------- /resources/migrations/041-populations.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS populations; 2 | -------------------------------------------------------------------------------- /resources/migrations/041-populations.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS populations ( 2 | id BIGSERIAL PRIMARY KEY, 3 | source_id BIGINT NOT NULL REFERENCES population_sources(id), 4 | region_id BIGINT NOT NULL REFERENCES regions(id), 5 | total_population INT NULL, 6 | max_population INT NULL, 7 | raster_pixel_area INT NULL 8 | ); 9 | -------------------------------------------------------------------------------- /resources/migrations/042-add-dataset-id-to-projects2.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects2 DROP COLUMN "dataset-id"; 2 | -------------------------------------------------------------------------------- /resources/migrations/042-add-dataset-id-to-projects2.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects2 ADD COLUMN "dataset-id" BIGINT REFERENCES datasets2(id) DEFAULT NULL; 2 | -------------------------------------------------------------------------------- /resources/migrations/043-add-population-source-id-to-projects2.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects2 DROP COLUMN "population-source-id""; 2 | -------------------------------------------------------------------------------- /resources/migrations/043-add-population-source-id-to-projects2.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects2 ADD COLUMN "population-source-id" BIGINT REFERENCES population_sources(id) DEFAULT NULL; 2 | -------------------------------------------------------------------------------- /resources/migrations/044-add_projects2_state.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects DROP COLUMN state; 2 | -------------------------------------------------------------------------------- /resources/migrations/044-add_projects2_state.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects2 2 | ADD COLUMN state VARCHAR(255); 3 | -------------------------------------------------------------------------------- /resources/migrations/045-scenarios.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS scenarios; 2 | -------------------------------------------------------------------------------- /resources/migrations/045-scenarios.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS scenarios ( 2 | id BIGSERIAL PRIMARY KEY, 3 | name VARCHAR(255) NOT NULL, 4 | "project-id" BIGINT NOT NULL REFERENCES projects2(id), 5 | label VARCHAR(255) NULL, 6 | investment NUMERIC(12, 2) NULL, 7 | "demand-coverage" BIGINT NULL, 8 | changeset TEXT NOT NULL 9 | ); 10 | -------------------------------------------------------------------------------- /resources/migrations/046-add-region-id-to-projects2.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "projects2" DROP COLUMN "region-id"; 2 | -------------------------------------------------------------------------------- /resources/migrations/046-add-region-id-to-projects2.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "projects2" ADD COLUMN "region-id" BIGINT REFERENCES regions(id); 2 | -------------------------------------------------------------------------------- /resources/migrations/047-add_dataset_version_to_projects2.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "projects2" DROP COLUMN "dataset-version"; 2 | -------------------------------------------------------------------------------- /resources/migrations/047-add_dataset_version_to_projects2.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "projects2" ADD COLUMN "dataset-version" INTEGER; 2 | -------------------------------------------------------------------------------- /resources/migrations/048-add_state_to_scenarios.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "scenarios" DROP COLUMN "updated-at"; 2 | ALTER TABLE "scenarios" DROP COLUMN "state"; 3 | ALTER TABLE "scenarios" DROP COLUMN "raster"; 4 | -------------------------------------------------------------------------------- /resources/migrations/048-add_state_to_scenarios.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "scenarios" ADD COLUMN "raster" VARCHAR(255); 2 | ALTER TABLE "scenarios" ADD COLUMN "state" VARCHAR(100); 3 | ALTER TABLE "scenarios" ADD COLUMN "updated-at" TIMESTAMP; 4 | -------------------------------------------------------------------------------- /resources/migrations/049-add_engine_config_to_projects2.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "projects2" DROP COLUMN "engine-config"; 2 | -------------------------------------------------------------------------------- /resources/migrations/049-add_engine_config_to_projects2.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "projects2" ADD COLUMN "engine-config" TEXT; 2 | -------------------------------------------------------------------------------- /resources/migrations/050-add_deleted_at_to_projects2.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "projects2" DROP COLUMN "deleted-at"; 2 | -------------------------------------------------------------------------------- /resources/migrations/050-add_deleted_at_to_projects2.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "projects2" ADD COLUMN "deleted-at" TIMESTAMP DEFAULT NULL; 2 | -------------------------------------------------------------------------------- /resources/migrations/051-rename_datasets_to_providers_set.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE providers_coverage 2 | RENAME COLUMN "provider-id" TO "site-id"; 3 | 4 | ALTER TABLE "providers_coverage" 5 | RENAME TO "sites2_coverage"; 6 | 7 | ALTER TABLE providers 8 | RENAME TO sites2; 9 | 10 | ALTER TABLE projects2 11 | RENAME COLUMN "provider-set-id" TO "dataset-id"; 12 | 13 | ALTER TABLE projects2 14 | RENAME COLUMN "provider-set-version" TO "dataset-version"; 15 | 16 | AlTER TABLE sites2 17 | RENAME COLUMN "provider-set-id" TO "dataset-id"; 18 | 19 | ALTER TABLE providers_set 20 | RENAME TO datasets2; -------------------------------------------------------------------------------- /resources/migrations/051-rename_datasets_to_providers_set.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE datasets2 2 | RENAME TO providers_set; 3 | 4 | AlTER TABLE sites2 5 | RENAME COLUMN "dataset-id" TO "provider-set-id"; 6 | 7 | ALTER TABLE projects2 8 | RENAME COLUMN "dataset-version" TO "provider-set-version"; 9 | 10 | ALTER TABLE projects2 11 | RENAME COLUMN "dataset-id" TO "provider-set-id"; 12 | 13 | ALTER TABLE sites2 14 | RENAME TO providers; 15 | 16 | ALTER TABLE "sites2_coverage" 17 | RENAME TO "providers_coverage"; 18 | 19 | ALTER TABLE providers_coverage 20 | RENAME COLUMN "site-id" TO "provider-id"; 21 | -------------------------------------------------------------------------------- /resources/migrations/052-add_sources_set.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS source_set; 2 | -------------------------------------------------------------------------------- /resources/migrations/052-add_sources_set.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS source_set ( 2 | id BIGSERIAL PRIMARY KEY, 3 | name VARCHAR(255) NOT NULL, 4 | type TEXT NOT NULL, 5 | unit VARCHAR(255) NOT NULL, 6 | raster_file VARCHAR(255) NOT NULL 7 | ); 8 | -------------------------------------------------------------------------------- /resources/migrations/053-add_sources.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS sources; -------------------------------------------------------------------------------- /resources/migrations/053-add_sources.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS sources ( 2 | id BIGSERIAL PRIMARY KEY, 3 | set_id BIGINT NOT NULL REFERENCES source_set(id), 4 | name VARCHAR(255) NOT NULL, 5 | type TEXT NOT NULL, 6 | the_geom geometry(POINT, 4326) NOT NULL, 7 | quantity INT NULL 8 | ); 9 | -------------------------------------------------------------------------------- /resources/migrations/054-add_providers_data_to_scenarios.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE scenarios DROP COLUMN "providers-data"; -------------------------------------------------------------------------------- /resources/migrations/054-add_providers_data_to_scenarios.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE scenarios 2 | ADD COLUMN "providers-data" TEXT; 3 | -------------------------------------------------------------------------------- /resources/migrations/055-update-population-source-id-to-projects2.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects2 2 | DROP COLUMN IF EXISTS "source-set-id", 3 | ADD COLUMN "population-source-id" BIGINT REFERENCES population_sources(id) DEFAULT NULL; 4 | -------------------------------------------------------------------------------- /resources/migrations/055-update-population-source-id-to-projects2.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects2 2 | DROP COLUMN IF EXISTS "population-source-id", 3 | ADD COLUMN "source-set-id" BIGINT REFERENCES source_set(id) DEFAULT NULL; 4 | -------------------------------------------------------------------------------- /resources/migrations/056-add_owner_to_sources.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE source_set 2 | DROP COLUMN "owner-id"; 3 | -------------------------------------------------------------------------------- /resources/migrations/056-add_owner_to_sources.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE source_set 2 | ADD COLUMN "owner-id" BIGINT REFERENCES users(id) DEFAULT NULL; 3 | -------------------------------------------------------------------------------- /resources/migrations/057-nullable_raster_file_sources.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE source_set 2 | ALTER COLUMN raster_file SET NOT NULL; 3 | -------------------------------------------------------------------------------- /resources/migrations/057-nullable_raster_file_sources.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE source_set 2 | ALTER COLUMN raster_file DROP NOT NULL; 3 | 4 | -------------------------------------------------------------------------------- /resources/migrations/058-add_sources_data_to_scenarios.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE scenarios 2 | DROP COLUMN "sources-data"; 3 | -------------------------------------------------------------------------------- /resources/migrations/058-add_sources_data_to_scenarios.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE scenarios 2 | ADD COLUMN "sources-data" TEXT; 3 | -------------------------------------------------------------------------------- /resources/migrations/059-add_new_providers_geom_to_scenarios.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE scenarios 2 | DROP COLUMN "new-providers-geom"; 3 | -------------------------------------------------------------------------------- /resources/migrations/059-add_new_providers_geom_to_scenarios.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE scenarios 2 | ADD COLUMN "new-providers-geom" TEXT; 3 | -------------------------------------------------------------------------------- /resources/migrations/060-add_error_message_to_scenarios.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE scenarios 2 | DROP COLUMN "error-message"; 3 | -------------------------------------------------------------------------------- /resources/migrations/060-add_error_message_to_scenarios.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE scenarios 2 | ADD COLUMN "error-message" TEXT; 3 | -------------------------------------------------------------------------------- /resources/migrations/061-remove_deleted_at_from_projects2.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "projects2" ADD COLUMN "deleted-at" TIMESTAMP DEFAULT NULL; 2 | -------------------------------------------------------------------------------- /resources/migrations/061-remove_deleted_at_from_projects2.up.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM "scenarios" s 2 | WHERE s."project-id" IN (SELECT id FROM "projects2" p WHERE p."deleted-at" is NOT NULL); 3 | DELETE FROM "projects2" WHERE "deleted-at" is NOT NULL; 4 | ALTER TABLE "projects2" DROP COLUMN IF EXISTS "deleted-at"; 5 | -------------------------------------------------------------------------------- /resources/migrations/062-coverage_contexts.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE coverages; 2 | DROP TABLE coverage_contexts; 3 | -------------------------------------------------------------------------------- /resources/migrations/062-coverage_contexts.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE coverage_contexts ( 2 | id BIGSERIAL PRIMARY KEY, -- id is internal and used for cross-reference with coverages only 3 | cid VARCHAR(255) UNIQUE, -- stringified version of the context id 4 | region_id BIGINT REFERENCES regions(id) ON DELETE CASCADE, 5 | options TEXT NOT NULL 6 | ); 7 | 8 | CREATE TABLE coverages ( 9 | context_id BIGINT REFERENCES coverage_contexts(id) ON DELETE CASCADE, 10 | lid VARCHAR(255) NOT NULL, -- stringified version of the location id 11 | location GEOMETRY(Point, 4326) NOT NULL, 12 | coverage GEOMETRY(MultiPolygon, 4326) NOT NULL, 13 | raster_file VARCHAR(255), -- name of the raster file, which is relative to the context directory 14 | 15 | CONSTRAINT id_with_context UNIQUE (context_id, lid) 16 | ); 17 | -------------------------------------------------------------------------------- /resources/migrations/063-add_geo_coverage_to_scenarios.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE scenarios DROP COLUMN "geo-coverage"; 2 | -------------------------------------------------------------------------------- /resources/migrations/063-add_geo_coverage_to_scenarios.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE scenarios ADD COLUMN "geo-coverage" FLOAT DEFAULT NULL; 2 | -------------------------------------------------------------------------------- /resources/migrations/064-add_coverage_to_projects2.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects2 DROP COLUMN "coverage-algorithm"; 2 | ALTER TABLE providers_set ADD COLUMN "coverage-algorithm" VARCHAR(100); 3 | -------------------------------------------------------------------------------- /resources/migrations/064-add_coverage_to_projects2.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE projects2 ADD COLUMN "coverage-algorithm" VARCHAR(100); 2 | UPDATE projects2 AS p2 3 | SET "coverage-algorithm" = ( 4 | SELECT "coverage-algorithm" 5 | FROM providers_set 6 | WHERE p2."provider-set-id" = providers_set.id 7 | ); 8 | ALTER TABLE providers_set DROP COLUMN "coverage-algorithm"; 9 | -------------------------------------------------------------------------------- /resources/migrations/065-add_population_under_coverage_to_scenarios.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE scenarios DROP COLUMN "population-under-coverage"; 2 | -------------------------------------------------------------------------------- /resources/migrations/065-add_population_under_coverage_to_scenarios.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE scenarios ADD COLUMN "population-under-coverage" FLOAT DEFAULT NULL; 2 | -------------------------------------------------------------------------------- /resources/migrations/066-rename_investment_to_effort.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE scenarios 2 | RENAME COLUMN "effort" TO "investment"; 3 | -------------------------------------------------------------------------------- /resources/migrations/066-rename_investment_to_effort.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE scenarios 2 | RENAME COLUMN "investment" TO "effort"; 3 | -------------------------------------------------------------------------------- /resources/migrations/067-add_providers_set_version_index.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX providers_set_version; 2 | -------------------------------------------------------------------------------- /resources/migrations/067-add_providers_set_version_index.up.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX providers_set_version ON providers ("provider-set-id", "version"); 2 | -------------------------------------------------------------------------------- /resources/migrations/068-add_regions_name_index.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE regions DROP CONSTRAINT "regions_name_index"; 2 | -------------------------------------------------------------------------------- /resources/migrations/068-add_regions_name_index.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE regions ADD CONSTRAINT "regions_name_index" UNIQUE ("country", "name", "admin_level"); 2 | -------------------------------------------------------------------------------- /resources/planwise/cli.edn: -------------------------------------------------------------------------------- 1 | {:duct.core/include ["planwise/config"] 2 | 3 | :duct.logger/timbre 4 | {:level :info 5 | :ns-blacklist ["duct.database.sql.hikaricp"]}} 6 | -------------------------------------------------------------------------------- /resources/planwise/errors/403.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Server Error 5 | 6 | 7 | 8 | 9 |

Access denied

10 |

You don't have enough permissions to access this page

11 | 12 | 13 | -------------------------------------------------------------------------------- /resources/planwise/errors/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Server Error 5 | 6 | 7 | 8 | 9 |

Resource Not Found

10 |

The requested page does not exist.

11 | 12 | 13 | -------------------------------------------------------------------------------- /resources/planwise/errors/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Server Error 5 | 6 | 7 | 8 | 9 |

Internal Server Error

10 |

Sorry, something went wrong.

11 | 12 | 13 | -------------------------------------------------------------------------------- /resources/planwise/plpgsql/regions-preview.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS postgis; 2 | CREATE EXTENSION IF NOT EXISTS pgrouting; 3 | 4 | CREATE OR REPLACE FUNCTION calculate_regions_previews() 5 | RETURNS void AS $$ 6 | DECLARE 7 | r_id INTEGER; 8 | BEGIN 9 | FOR r_id IN SELECT r.id FROM regions r WHERE r.preview_geom IS NULL LOOP 10 | PERFORM calculate_region_preview(r_id); 11 | END LOOP; 12 | END; 13 | $$ LANGUAGE plpgsql; 14 | 15 | CREATE OR REPLACE FUNCTION calculate_region_preview(region_id INTEGER) 16 | RETURNS INTEGER AS $$ 17 | DECLARE 18 | f_geom geometry(MultiPolygon,4326); 19 | simplify FLOAT; 20 | simplify_a FLOAT; 21 | simplify_b FLOAT; 22 | target INTEGER; 23 | current INTEGER; 24 | iter INTEGER; 25 | BEGIN 26 | simplify_a := 0; 27 | simplify_b := 1; 28 | target := 3600; 29 | iter := 0; 30 | 31 | WHILE (iter = 0 OR (ABS(target - current) > 20 AND iter < 20)) LOOP 32 | simplify := (simplify_a + simplify_b) / 2; 33 | iter := iter + 1; 34 | 35 | SELECT LENGTH(ST_AsGeoJSON(ST_SimplifyPreserveTopology(the_geom, simplify), 15, 3)) 36 | FROM regions WHERE id = region_id 37 | INTO current; 38 | 39 | IF current > target THEN 40 | simplify_a := simplify; 41 | ELSE 42 | simplify_b := simplify; 43 | END IF; 44 | END LOOP; 45 | 46 | -- FIXME: ST_SimplifyPreserveTopology doesn't always simplify and reduce the size of the resulting GeoJSON 47 | -- OTOH, ST_Simplify will produce invalid geometries sometimes (Dire Dawa in Ethiopia) 48 | UPDATE regions SET preview_geom = ST_Multi(ST_SimplifyPreserveTopology(the_geom, simplify)) WHERE id = region_id; 49 | RETURN current; 50 | END; 51 | $$ LANGUAGE plpgsql; 52 | -------------------------------------------------------------------------------- /resources/planwise/prod.edn: -------------------------------------------------------------------------------- 1 | {:duct.core/include ["planwise/config"]} 2 | -------------------------------------------------------------------------------- /resources/planwise/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/instedd/planwise/6da765a09481c6b86169dab585bbea71aabe120e/resources/planwise/public/favicon.ico -------------------------------------------------------------------------------- /resources/planwise/public/images/logo-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/instedd/planwise/6da765a09481c6b86169dab585bbea71aabe120e/resources/planwise/public/images/logo-transparent.png -------------------------------------------------------------------------------- /resources/planwise/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/instedd/planwise/6da765a09481c6b86169dab585bbea71aabe120e/resources/planwise/public/images/logo.png -------------------------------------------------------------------------------- /resources/planwise/public/images/project-background1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/instedd/planwise/6da765a09481c6b86169dab585bbea71aabe120e/resources/planwise/public/images/project-background1@2x.png -------------------------------------------------------------------------------- /resources/planwise/public/images/project-thumbnail1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/instedd/planwise/6da765a09481c6b86169dab585bbea71aabe120e/resources/planwise/public/images/project-thumbnail1@2x.png -------------------------------------------------------------------------------- /resources/planwise/public/images/project-thumbnail2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/instedd/planwise/6da765a09481c6b86169dab585bbea71aabe120e/resources/planwise/public/images/project-thumbnail2@2x.png -------------------------------------------------------------------------------- /resources/planwise/public/images/project-thumbnail3@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/instedd/planwise/6da765a09481c6b86169dab585bbea71aabe120e/resources/planwise/public/images/project-thumbnail3@2x.png -------------------------------------------------------------------------------- /resources/planwise/public/providers-sample.csv: -------------------------------------------------------------------------------- 1 | id,type,name,lat,lon,capacity,tags 2 | 1,hospital,Nanyuki Cottage Hospital,-19.983333,23.416667,50,private laboratory radiography pharmacy 3 | 2,hospital,Karen Hospital,-1.336063,36.72609,102,private general-wards pediatric-ward 24hs-pharmacy 4 | 3,hospital,The Nairobi Women's Hospital,-1.293856,36.796071,356,private obstetrics gynecology-service 5 | 4,hospital,Kenyatta National Hospital,-1.3017,36.80772,1800,public surgical-services general-medicine 6 | -------------------------------------------------------------------------------- /resources/planwise/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /resources/planwise/public/sources-sample.csv: -------------------------------------------------------------------------------- 1 | id,type,name,lat,lon,capacity,tags,required-capacity,used-capacity,satisfied-demand,unsatisfied-demand 2 | 1,hospital,Nanyuki Cottage Hospital,-19.983333,23.416667,50,private laboratory radiography pharmacy,0,50,50,0 3 | 2,hospital,Karen Hospital,-1.336063,36.72609,102,private general-wards pediatric-ward 24hs-pharmacy,0,102,102,0 4 | 3,hospital,The Nairobi Women's Hospital,-1.293856,36.796071,356,private obstetrics gynecology-service,0,356,356,0 5 | 4,hospital,Kenyatta National Hospital,-1.3017,36.80772,1800,public surgical-services general-medicine,200,1800,1800,200 6 | -------------------------------------------------------------------------------- /resources/planwise/sql/coverage/friction.sql: -------------------------------------------------------------------------------- 1 | -- :name find-country-region-with-point :? :1 2 | -- Countries are admin_level 0 (using gadm data) and the load-friction-raster 3 | -- script will only clip the global raster file to the regions delimited by them 4 | SELECT id FROM regions WHERE "admin_level" = 0 AND ST_Contains("the_geom", :point) LIMIT 1; 5 | -------------------------------------------------------------------------------- /resources/planwise/sql/coverage/pgrouting.sql: -------------------------------------------------------------------------------- 1 | -- :name compute-pgr-alpha-coverage :? :1 2 | SELECT "result", "polygon" 3 | FROM pgr_alpha_shape_coverage(:point, :threshold::integer) AS("result" TEXT, "polygon" GEOMETRY); 4 | -------------------------------------------------------------------------------- /resources/planwise/sql/coverage/simple.sql: -------------------------------------------------------------------------------- 1 | -- :name compute-simple-buffer-coverage :? :1 2 | SELECT "result", "polygon" 3 | FROM simple_buffer_coverage(:point, :distance::integer) AS("result" TEXT, "polygon" GEOMETRY); 4 | -------------------------------------------------------------------------------- /resources/planwise/sql/projects2.sql: -------------------------------------------------------------------------------- 1 | -- :name db-create-project! :ambulance -------------------------------------------------------------------------------- /resources/svg/icons/arrow-back.svg: -------------------------------------------------------------------------------- 1 | 2 | arrow-back 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/svg/icons/arrow-down.svg: -------------------------------------------------------------------------------- 1 | arrow-down -------------------------------------------------------------------------------- /resources/svg/icons/arrow-up.svg: -------------------------------------------------------------------------------- 1 | arrow-up -------------------------------------------------------------------------------- /resources/svg/icons/box.svg: -------------------------------------------------------------------------------- 1 | box -------------------------------------------------------------------------------- /resources/svg/icons/bulb.svg: -------------------------------------------------------------------------------- 1 | bulb -------------------------------------------------------------------------------- /resources/svg/icons/bus.svg: -------------------------------------------------------------------------------- 1 | bus -------------------------------------------------------------------------------- /resources/svg/icons/car.svg: -------------------------------------------------------------------------------- 1 | car -------------------------------------------------------------------------------- /resources/svg/icons/check-circle-wizard.svg: -------------------------------------------------------------------------------- 1 | check-circle-wizard 2 | -------------------------------------------------------------------------------- /resources/svg/icons/check-circle.svg: -------------------------------------------------------------------------------- 1 | check-circle -------------------------------------------------------------------------------- /resources/svg/icons/close.svg: -------------------------------------------------------------------------------- 1 | close -------------------------------------------------------------------------------- /resources/svg/icons/cross-circle.svg: -------------------------------------------------------------------------------- 1 | cross-circle -------------------------------------------------------------------------------- /resources/svg/icons/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | delete 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/svg/icons/demographics.svg: -------------------------------------------------------------------------------- 1 | demographics -------------------------------------------------------------------------------- /resources/svg/icons/download.svg: -------------------------------------------------------------------------------- 1 | download -------------------------------------------------------------------------------- /resources/svg/icons/error.svg: -------------------------------------------------------------------------------- 1 | 2 | error 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/svg/icons/exit.svg: -------------------------------------------------------------------------------- 1 | 2 | exit 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/svg/icons/filter.svg: -------------------------------------------------------------------------------- 1 | filter -------------------------------------------------------------------------------- /resources/svg/icons/hospital.svg: -------------------------------------------------------------------------------- 1 | hospital -------------------------------------------------------------------------------- /resources/svg/icons/key-arrow-down.svg: -------------------------------------------------------------------------------- 1 | key-arrow-down -------------------------------------------------------------------------------- /resources/svg/icons/key-arrow-left.svg: -------------------------------------------------------------------------------- 1 | key-arrow-left 2 | -------------------------------------------------------------------------------- /resources/svg/icons/key-arrow-right.svg: -------------------------------------------------------------------------------- 1 | key-arrow-right -------------------------------------------------------------------------------- /resources/svg/icons/location.svg: -------------------------------------------------------------------------------- 1 | location -------------------------------------------------------------------------------- /resources/svg/icons/logo.svg: -------------------------------------------------------------------------------- 1 | logo -------------------------------------------------------------------------------- /resources/svg/icons/logo2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | logo2 6 | 17 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /resources/svg/icons/mail-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | mail-outline 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/svg/icons/pickup.svg: -------------------------------------------------------------------------------- 1 | pickup -------------------------------------------------------------------------------- /resources/svg/icons/portfolio.svg: -------------------------------------------------------------------------------- 1 | portfolio -------------------------------------------------------------------------------- /resources/svg/icons/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | refresh 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/svg/icons/remove-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | remove-circle 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/svg/icons/remove.svg: -------------------------------------------------------------------------------- 1 | remove -------------------------------------------------------------------------------- /resources/svg/icons/repair.svg: -------------------------------------------------------------------------------- 1 | repair -------------------------------------------------------------------------------- /resources/svg/icons/scenario.svg: -------------------------------------------------------------------------------- 1 | scenario 2 | -------------------------------------------------------------------------------- /resources/svg/icons/search.svg: -------------------------------------------------------------------------------- 1 | search -------------------------------------------------------------------------------- /resources/svg/icons/share.svg: -------------------------------------------------------------------------------- 1 | 2 | share 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/svg/icons/signout.svg: -------------------------------------------------------------------------------- 1 | 2 | signout 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/svg/icons/transport-means.svg: -------------------------------------------------------------------------------- 1 | transport-means -------------------------------------------------------------------------------- /resources/svg/icons/walk.svg: -------------------------------------------------------------------------------- 1 | walk -------------------------------------------------------------------------------- /resources/svg/icons/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | warning 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /scripts/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile to create the instedd/planwise-tools image 2 | 3 | FROM node:bullseye-slim 4 | 5 | WORKDIR /tmp 6 | 7 | # Install some basic tools 8 | RUN \ 9 | apt-get update \ 10 | && DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget less vim procps postgresql-client gdal-bin \ 11 | && apt-get clean \ 12 | && rm -rf /var/lib/apt/lists/* 13 | 14 | # Install mapshaper 15 | RUN npm install -g mapshaper 16 | 17 | # Install glow (for reading Markdown) 18 | RUN \ 19 | wget https://github.com/charmbracelet/glow/releases/download/v1.4.1/glow_1.4.1_linux_amd64.deb && \ 20 | dpkg -i glow*.deb 21 | 22 | # Install babashka 23 | RUN \ 24 | curl -sLO https://raw.githubusercontent.com/babashka/babashka/master/install && \ 25 | chmod +x ./install && \ 26 | ./install --version 0.8.2 27 | 28 | # Preload required pods 29 | RUN \ 30 | bb -e "(require '[babashka.pods :as pods]) (pods/load-pod 'org.babashka/postgresql \"0.1.0\")" 31 | 32 | # Add sources for tools 33 | ADD tools/update-region-previews /tools/ 34 | RUN chmod +x /tools/update-region-previews 35 | ADD tools/update-source-sets /tools/ 36 | RUN chmod +x /tools/update-source-sets 37 | 38 | # Add geojson tool 39 | ADD geojson/ /tools/geojson/ 40 | RUN \ 41 | cd /tools/geojson && npm install && npm install -g 42 | 43 | # Add load-regions script 44 | ADD population/load-regions /tools/ 45 | RUN chmod +x /tools/load-regions 46 | 47 | # Add friction layer tools 48 | ADD friction/load-friction-raster /tools/ 49 | RUN chmod +x /tools/load-friction-raster 50 | 51 | # Add docker-entrypoint.sh overriding the base default 52 | ADD tools/docker-entrypoint.sh /usr/local/bin/ 53 | RUN chmod +x /usr/local/bin/docker-entrypoint.sh 54 | 55 | ADD tools/README.md /tools/ 56 | ADD tools/readme /usr/local/bin/ 57 | RUN chmod +x /usr/local/bin/readme 58 | ADD tools/tools /usr/local/bin/ 59 | RUN chmod +x /usr/local/bin/tools 60 | 61 | WORKDIR /tools 62 | 63 | ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] 64 | CMD ["/bin/bash"] 65 | -------------------------------------------------------------------------------- /scripts/bootstrap-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | cd $(dirname $0)/.. 6 | 7 | scripts/build-binaries 8 | scripts/migrate 9 | -------------------------------------------------------------------------------- /scripts/build-binaries: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$1" == "-r" -o "$1" == "--release" ]; then 4 | build_type=Release 5 | else 6 | build_type=Debug 7 | fi 8 | echo "Building binaries in ${build_type} mode" 9 | 10 | arch=`uname -s | tr '[A-Z]' '[a-z]'`-`uname -m` 11 | cd $(dirname $0)/../cpp 12 | mkdir build-${arch} 13 | cd build-${arch} 14 | 15 | cmake -DCMAKE_BUILD_TYPE=$build_type .. 16 | make clean all 17 | -------------------------------------------------------------------------------- /scripts/crop-source-raster: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script is used by the demand engine (see src/planwise/engine/common.clj) 4 | 5 | QUIET=-q 6 | 7 | while [ $# -ge 1 ]; do 8 | case $1 in 9 | -i|--input) 10 | INPUT=$2 11 | shift 12 | ;; 13 | -o|--output) 14 | OUTPUT=$2 15 | shift 16 | ;; 17 | -c|--cutline) 18 | CUTLINE=$2 19 | shift 20 | ;; 21 | -r|--resolution) 22 | XRES=$2 23 | YRES=$3 24 | shift 2 25 | ;; 26 | -v|--verbose) 27 | QUIET="" 28 | ;; 29 | *) 30 | echo Unknown option $1 31 | exit 1 32 | ;; 33 | esac; 34 | shift 35 | done 36 | 37 | if [ -z "$INPUT" -o -z "$OUTPUT" -o -z "$CUTLINE" -o -z "$XRES" -o -z "$YRES" ]; then 38 | echo Missing parameters 39 | exit 1 40 | fi 41 | 42 | if [ ! -f $INPUT ]; then 43 | echo Cannot read input raster 44 | exit 1 45 | fi 46 | 47 | if [ ! -f $CUTLINE ]; then 48 | echo Cannot read cutline file 49 | exit 1 50 | fi 51 | 52 | gdalwarp $QUIET \ 53 | -tap \ 54 | -tr $XRES $YRES \ 55 | -r average \ 56 | -co "TILED=YES" -co "COMPRESS=LZW" \ 57 | -crop_to_cutline \ 58 | -cutline $CUTLINE \ 59 | $INPUT $OUTPUT 60 | -------------------------------------------------------------------------------- /scripts/dev: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | if [[ -e env ]]; then 3 | source env 4 | fi; 5 | 6 | if $(which -s rlwrap); then 7 | (echo "(dev)"; echo "(go)"; cat <&0) | rlwrap -r -m '\\"' -b "(){}[],^%3@\\\";:'" lein repl; 8 | else 9 | (echo "(dev)"; echo "(go)"; cat <&0) | lein repl; 10 | fi; 11 | -------------------------------------------------------------------------------- /scripts/friction/README.md: -------------------------------------------------------------------------------- 1 | # Importing friction data 2 | 3 | NOTE: this tool is available in the Planwise Tools Docker image. See scripts/Dockerfile and scripts/tools/README 4 | 5 | Download the global friction raster from The Malaria Atlas Project: 6 | https://malariaatlas.org/research-project/accessibility_to_cities/ 7 | 8 | Direct download link: 9 | https://malariaatlas.org/geoserver/ows?service=CSW&version=2.0.1&request=DirectDownload&ResourceId=Explorer:2015_friction_surface_v1_Decompressed 10 | 11 | The zip file contains a .tif file with the friction layer for most of the 12 | planet. Unit is minutes/meter. Unzip it and execute the following script to clip 13 | by the country-level regions used by Planwise: 14 | 15 | ```sh 16 | scripts/friction/load-friction-raster PATH_TO_GLOBAL_FRICTION.tif 17 | ``` 18 | 19 | The script will retrieve the admin-level 0 regions from the database and clip 20 | the raster by the contour of each region, saving them in 21 | `$DATA_PATH/friction/regions`. 22 | 23 | The clips will be used when computing coverage for a site. The clip will be 24 | selected by inclusion of the site in the region polygon. 25 | -------------------------------------------------------------------------------- /scripts/geojson/README.md: -------------------------------------------------------------------------------- 1 | # Kml to GeoJson script 2 | 3 | NOTE: this tool is available in the Planwise Tools Docker image. See scripts/Dockerfile and scripts/tools/README 4 | 5 | ## Running the script 6 | 7 | ### To run local: 8 | (in project root) 9 | ``` 10 | $ cd scripts/geojson 11 | $ npm install 12 | $ ./gadm2geojson ARG ../../data/geojson 13 | ``` 14 | 15 | __Note:__ If you prefer to run the script directly with Node.js, you could replace the last line with: 16 | ``` 17 | $ node gadm2geojson.js ARG ../../data/geojson 18 | ``` 19 | 20 | ### Output files: 21 | The script generates two geojson files: 22 | 23 | * _``_`_adm0.geojson` 24 | * _``_`_adm1.geojson` 25 | 26 | and they will be placed at the output folder inside another folder with the country code as name. 27 | 28 | __For example:__ 29 | 30 | If the output folder is `../../data/geojson` then the files will be: 31 | 32 | * `../../data/geojson/ARG/ARG_adm0.geojson` 33 | * `../../data/geojson/ARG/ARG_adm1.geojson` 34 | 35 | ## Example country code 36 | [ISO 3166-1 alpha-3](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) 37 | 38 | Country | Code 39 | -------------| :---: 40 | Kenya | KEN 41 | Pakistan | PAK 42 | Philippines | PHL 43 | Nepal | NPL 44 | Burkina Faso | BFA 45 | -------------------------------------------------------------------------------- /scripts/geojson/gadm2geojson: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | node gadm2geojson.js "$@" 4 | -------------------------------------------------------------------------------- /scripts/geojson/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gadm2geojson", 3 | "version": "1.0.0", 4 | "description": "Tool to create country administrative boundaries data (in geojson format) from GADM data.", 5 | "main": "./gadm2geojson.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "bin": { 10 | "gadm2geojson": "./gadm2geojson.js" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@mapbox/togeojson": "^0.16.0", 16 | "adm-zip": "^0.4.13", 17 | "commander": "^2.15.1", 18 | "countryinfo": "^1.0.2", 19 | "minimist": "^1.2.6", 20 | "mkdirp": "^0.5.1", 21 | "request": "^2.88.0", 22 | "togeojson": "^0.16.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /scripts/migrate: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [[ -x $(command -v lein) ]]; then 3 | lein migrate 4 | elif [[ -e $JAR_PATH ]]; then 5 | java -jar $JAR_PATH :duct/migrator 6 | else 7 | echo "Not jar path '$JAR_PATH' nor lein command were found. Aborting." 8 | fi 9 | -------------------------------------------------------------------------------- /scripts/resize-raster: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script is used by the demand engine (see src/planwise/engine/common.clj) 4 | 5 | QUIET=-q 6 | 7 | while [ $# -ge 1 ]; do 8 | case $1 in 9 | -i|--input) 10 | INPUT=$2 11 | shift 12 | ;; 13 | -o|--output) 14 | OUTPUT=$2 15 | shift 16 | ;; 17 | -r|--resolution) 18 | XRES=$2 19 | YRES=$3 20 | shift 2 21 | ;; 22 | -v|--verbose) 23 | QUIET="" 24 | ;; 25 | *) 26 | echo Unknown option $1 27 | exit 1 28 | ;; 29 | esac; 30 | shift 31 | done 32 | 33 | if [ -z "$INPUT" -o -z "$OUTPUT" -o -z "$XRES" -o -z "$YRES" ]; then 34 | echo Missing parameters 35 | exit 1 36 | fi 37 | 38 | if [ ! -f $INPUT ]; then 39 | echo Cannot read input raster 40 | exit 1 41 | fi 42 | 43 | gdalwarp $QUIET \ 44 | -tap \ 45 | -tr $XRES $YRES \ 46 | -r average \ 47 | -co "TILED=YES" -co "COMPRESS=LZW" \ 48 | $INPUT $OUTPUT 49 | -------------------------------------------------------------------------------- /scripts/tools/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export PATH=/tools:$PATH 4 | 5 | echo "This is the Planwise Tools Docker image" 6 | echo "You can find several helper scripts and tools in the /tools directory to manage data from a Planwise installation." 7 | echo "Check the /tools/README.md file for more information (you can use the readme command)" 8 | echo 9 | 10 | if [ -z "$DATA_PATH" ]; then 11 | if [ -d /data ]; then 12 | echo The environment variable DATA_PATH is not defined. Using the default /data 13 | export DATA_PATH=/data 14 | else 15 | echo The environment variable DATA_PATH is not defined and there is no default /data directory. Some tools may not work. Did you mount the image properly? 16 | fi 17 | else 18 | echo Using DATA_PATH at ${DATA_PATH} 19 | fi 20 | echo 21 | 22 | if [ -z "$POSTGRES_HOST" ] || [ -z "$POSTGRES_PORT" ] || [ -z "$POSTGRES_USER" ] || [ -z "$POSTGRES_PASSWORD" ] || [ -z "$POSTGRES_DB" ]; then 23 | echo PostgreSQL configuration is missing or incomplete. Some tools will not work. 24 | echo Check the POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, POSTGRES_DB and POSTGRES_PASSWORD environment variables. 25 | echo 26 | else 27 | export PGDATABASE=$POSTGRES_DB 28 | export PGHOST=$POSTGRES_HOST 29 | export PGPORT=$POSTGRES_PORT 30 | export PGUSER=$POSTGRES_USER 31 | fi 32 | 33 | if [ -d "$DATA_PATH" ]; then 34 | cd $DATA_PATH 35 | fi 36 | 37 | exec "$@" 38 | -------------------------------------------------------------------------------- /scripts/tools/readme: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | glow -p /tools/README.md 4 | -------------------------------------------------------------------------------- /scripts/tools/tools: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Use this script if you are running from a container exec shell 4 | 5 | # for a Rancher console this helps 6 | export TERM=vt100 7 | 8 | /usr/local/bin/docker-entrypoint.sh /bin/bash 9 | -------------------------------------------------------------------------------- /src/planwise/boundary/auth.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.boundary.auth) 2 | 3 | (defprotocol Auth 4 | (find-auth-token [service scope user-ident]) 5 | (create-jwe-token [service user-ident])) 6 | -------------------------------------------------------------------------------- /src/planwise/boundary/engine.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.boundary.engine) 2 | 3 | (defprotocol Engine 4 | (compute-initial-scenario [this project] 5 | "Computes the initial scenario for a project") 6 | 7 | (compute-scenario [this project initial-scenario scenario] 8 | "Computes the scenario for a project. It assumes the initial scenario was created.") 9 | 10 | (clear-project-cache [this project-id] 11 | "Clears a project cache") 12 | 13 | (search-optimal-locations 14 | [engine project source] 15 | "According to project configuration and initial demand set 16 | returns suggestions for creating new provider") 17 | 18 | (search-optimal-interventions 19 | [engine project scenario settings] 20 | "Returns suggestions for either upgrading or increasing existing providers in project") 21 | 22 | (compute-scenario-stats 23 | [engine project scenario params] 24 | "Computes statistics for a scenario")) 25 | -------------------------------------------------------------------------------- /src/planwise/boundary/file_store.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.boundary.file-store 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.string :as str] 4 | [clojure.java.io :as io])) 5 | 6 | 7 | ;; Specs ===================================================================== 8 | ;; 9 | 10 | (s/def ::coll-type #{:coverages :projects}) 11 | (s/def ::coll-id some?) 12 | 13 | 14 | ;; Protocol defintions ======================================================= 15 | ;; 16 | 17 | (defprotocol FileStore 18 | (setup-collection [this coll-type coll-id] 19 | "Constructs a file path to the collection and ensures the directory for it 20 | exists. Returns the full path to the collection directory.") 21 | 22 | (destroy-collection [this coll-type coll-id] 23 | "Removes the directory and contained files in the given collection")) 24 | 25 | 26 | ;; Auxiliary functions ======================================================= 27 | ;; 28 | 29 | (defn build-file-id 30 | "Construct a valid filename derived from any value" 31 | [id] 32 | (let [file-id (-> (if (string? id) id (pr-str id)) 33 | (str/replace #"[:\[\]\(\){}]" "") 34 | (str/replace #"[ \".]" "-") 35 | (str/replace #"[/]" "_") 36 | (str/replace #"[^0-9a-zA-Z_-]" ""))] 37 | (when (> (count file-id) 200) 38 | (throw (ex-info "Derived file id too large" {:id id :file-id file-id}))) 39 | file-id)) 40 | 41 | (defn full-path 42 | "Builds a full path by joining parts with the OS separator" 43 | [parent & more] 44 | (str (apply io/file parent more))) 45 | 46 | (defn exists? 47 | [path] 48 | (if (some? path) 49 | (.exists (io/as-file path)) 50 | false)) 51 | -------------------------------------------------------------------------------- /src/planwise/boundary/jobrunner.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.boundary.jobrunner) 2 | 3 | ;; Job types are identified by a keyword, preferrably namespaced (eg. 4 | ;; :planwise.jobs/dataset-process) 5 | 6 | ;; Job identifiers can consist of a job type alone, or vector whose first 7 | ;; element is the job type (eg. [:planwise.jobs/dataset-process 42]) 8 | 9 | ;; Jobs also have an arbitrary value representing the state. If nil, the job is 10 | ;; considered finished and will be removed from the executing component. 11 | 12 | (defn job-type 13 | [job] 14 | (if (vector? job) (first job) job)) 15 | 16 | ;;; 17 | ;;; Jobs interface 18 | ;;; 19 | 20 | (defmulti job-next-task 21 | "Retrieve the next task to execute for this job. Must return a map with a 22 | :state key containing the next state, :task-id with an internal task 23 | identifier and :task-fn with the task implementation itself. A nil state 24 | signals a finished job." 25 | (fn [job state] (job-type job))) 26 | 27 | (defmethod job-next-task :default 28 | [job state] 29 | ;; no more tasks to execute, job is finished 30 | nil) 31 | 32 | (defmulti job-cancel! 33 | "Requests a job cancellation for a running job. Returns the updated state." 34 | (fn [job state] (job-type job))) 35 | 36 | (defmethod job-cancel! :default 37 | [job state] 38 | ;; ignored by default 39 | state) 40 | 41 | (defmulti job-task-success 42 | "Receive the report of a successfully completed task for the job. Returns the 43 | updated state." 44 | (fn [job state task result] (job-type job))) 45 | 46 | (defmethod job-task-success :default 47 | [job state task result] 48 | ;; ignore by default 49 | state) 50 | 51 | (defmulti job-task-failure 52 | "Receive the report of a failed task for the job. Returns the updated state." 53 | (fn [job state task error-info] (job-type job))) 54 | 55 | (defmethod job-task-failure :default 56 | [job state task error-info] 57 | ;; ignore by default 58 | state) 59 | 60 | 61 | (defprotocol JobRunner 62 | (queue-job [runner job job-state] 63 | "Queue a new job")) 64 | -------------------------------------------------------------------------------- /src/planwise/boundary/mailer.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.boundary.mailer) 2 | 3 | (defprotocol Mailer 4 | "Mail sender" 5 | 6 | (send-mail [service args] 7 | "Sends an email based on the specified args: `:from`, `:to`, `:cc`, 8 | `:subject` and `:body`. Refer to https://github.com/drewr/postal 9 | fore more information.")) 10 | 11 | -------------------------------------------------------------------------------- /src/planwise/boundary/maps.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.boundary.maps) 2 | 3 | (defprotocol Maps 4 | "Mapping utilities" 5 | 6 | (mapserver-url [service] 7 | "Retrieve the demographics tile URL template")) 8 | -------------------------------------------------------------------------------- /src/planwise/boundary/projects2.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.boundary.projects2) 2 | 3 | (defprotocol Projects2 4 | "API for manipulating projects" 5 | 6 | (create-project [this owner-id] 7 | "Creates a project. Returns a map with the database generated id.") 8 | 9 | (list-projects [this owner-id] 10 | "Returns projects accessible by the user.") 11 | 12 | (get-project [this project-id] 13 | "Returns project with the given ID") 14 | 15 | (update-project [this project] 16 | "Updates project's attributes. Returns the updated project.") 17 | 18 | (start-project [this project-id] 19 | "Starts project.") 20 | 21 | (delete-project [this project-id] 22 | "Delete project for given ID. 23 | Also deletes scenarios and files created in project") 24 | 25 | (reset-project [this project-id] 26 | "Resets project to draft.")) 27 | -------------------------------------------------------------------------------- /src/planwise/boundary/providers_set.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.boundary.providers-set) 2 | 3 | (defprotocol ProvidersSet 4 | "API for manipulating providers set" 5 | 6 | (list-providers-set [this owner-id] 7 | "Returns the list of the providers-set owned by the user") 8 | 9 | (get-provider-set [this provider-set-id] 10 | "Finds a provider-set by id") 11 | 12 | (get-provider [this provider-id] 13 | "Finds provider by id") 14 | 15 | (create-and-import-providers [this options csv-file] 16 | "Create and import a CSV file atomically") 17 | 18 | (get-providers-in-region [this provider-set-id version filter-options] 19 | "Retrieves providers and disabled-providers for a version of a provider-set located inside the region") 20 | 21 | (count-providers-filter-by-tags 22 | [this provider-set-id region-id tags] 23 | [this provider-set-id region-id tags version] 24 | "Count providers filtered by a single tag. Default version is the last one") 25 | 26 | (delete-provider-set 27 | [this provider-set-id] 28 | "Delete provider-set given a provider-set-id. 29 | Providers and providers coverage referenced from provider-set are also deleted. 30 | When provider set is referenced from valid project an exception is thrown. 31 | If provider set is deleted all created files are deleted.")) 32 | 33 | -------------------------------------------------------------------------------- /src/planwise/boundary/regions.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.boundary.regions) 2 | 3 | (defprotocol Regions 4 | "API for managing regions." 5 | 6 | (list-regions [this] 7 | "Returns all regions in the database.") 8 | 9 | (list-regions-with-preview [this ids] 10 | "Returns regions including a preview geojson of their boundaries given their 11 | ids.") 12 | 13 | (list-regions-with-geo [this ids simplify] 14 | "Returns regions including a simplified geojson with their boundaries given 15 | their ids.") 16 | 17 | (find-region [this id] 18 | "Returns region by ID, or nil if not found, without including any 19 | geometries") 20 | 21 | (enum-regions-intersecting-envelope [this envelope] 22 | "Return region ids which intersect in the given envelope") 23 | 24 | (get-region-geometry [this id] 25 | "Return the region geometry and bounding box as GeoJSON")) 26 | -------------------------------------------------------------------------------- /src/planwise/boundary/runner.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.boundary.runner) 2 | 3 | (defprotocol Runner 4 | (run-external [service kind timeout name args])) 5 | -------------------------------------------------------------------------------- /src/planwise/boundary/sources.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.boundary.sources) 2 | 3 | (defprotocol Sources 4 | "API for manipulating sources" 5 | 6 | (list-sources [this owner-id] 7 | "Returns the list of the sources") 8 | 9 | (import-from-csv [this options csv-file] 10 | "Import (create) with sources (type Point)") 11 | 12 | (get-source-set-by-id [this id] 13 | "Finds a sources-set by id") 14 | 15 | (list-sources-in-set [this source-set-id] 16 | "Returns the list of sources in a source-set") 17 | 18 | (get-sources-from-set-in-region [this source-set-id region-id] 19 | "Returns a list of sources in a source set that are inside the given region") 20 | 21 | (enum-sources-under-coverage [this source-set-id coverage-geom] 22 | "Returns the ids of the sources from the source set contained in the given geometry") 23 | 24 | (get-sources-extent [this ids] 25 | "Returns the extent of all ids")) 26 | -------------------------------------------------------------------------------- /src/planwise/boundary/users.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.boundary.users) 2 | 3 | (defprotocol Users 4 | (find-user [store user-id] 5 | "Retrieves a single user by ID") 6 | 7 | (find-or-create-user-by-email [store email]) 8 | (update-user-last-login! [store user-id]) 9 | (save-token-for-scope! [store scope email token]) 10 | (find-latest-token-for-scope [store scope email])) 11 | -------------------------------------------------------------------------------- /src/planwise/component/coverage/friction.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.component.coverage.friction 2 | (:require [hugsql.core :as hugsql] 3 | [clojure.string :as str] 4 | [taoensso.timbre :as timbre] 5 | [planwise.util.geo :as geo] 6 | [planwise.boundary.runner :as runner]) 7 | (:import [org.postgis PGgeometry])) 8 | 9 | (timbre/refer-timbre) 10 | 11 | (hugsql/def-db-fns "planwise/sql/coverage/friction.sql") 12 | 13 | (defn find-friction-raster 14 | [db-spec coords] 15 | (let [pg-point (geo/make-pg-point coords) 16 | result (find-country-region-with-point db-spec {:point pg-point})] 17 | (when-let [region-id (:id result)] 18 | ;; TODO: parameterize data folder 19 | (str "data/friction/regions/" region-id ".tif")))) 20 | 21 | (defn compute-polygon 22 | [{:keys [runner friction-raster coords time friction]}] 23 | (let [coords (->> coords ((juxt :lon :lat)) (str/join ",")) 24 | time-args (mapcat #(list "-m" %) time) 25 | friction-args (mapcat #(list "-f" %) friction) 26 | args (map str (concat ["-i" friction-raster "-g" coords] time-args friction-args)) 27 | polygon-wkt (runner/run-external runner :bin 2000 "walking-coverage" args)] 28 | (PGgeometry. (str "SRID=4326;" polygon-wkt)))) 29 | -------------------------------------------------------------------------------- /src/planwise/component/coverage/simple.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.component.coverage.simple 2 | (:require [hugsql.core :as hugsql] 3 | [planwise.util.geo :as geo])) 4 | 5 | (hugsql/def-db-fns "planwise/sql/coverage/simple.sql") 6 | 7 | (defn compute-coverage 8 | [db-spec point distance] 9 | (compute-simple-buffer-coverage db-spec {:point point :distance distance})) 10 | 11 | 12 | ;; REPL testing 13 | 14 | (comment 15 | ;; Kilifi location 16 | (compute-coverage (:spec (planwise.repl/db)) (geo/make-pg-point -3.0361 40.1333) 20000) 17 | 18 | ;; Nairobi location 19 | (compute-coverage (:spec (planwise.repl/db)) (geo/make-pg-point -1.2741 36.7931) 10000)) 20 | -------------------------------------------------------------------------------- /src/planwise/component/mailer.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.component.mailer 2 | (:require [planwise.boundary.mailer :as boundary] 3 | [integrant.core :as ig] 4 | [postal.core :as postal] 5 | [taoensso.timbre :as timbre])) 6 | 7 | (timbre/refer-timbre) 8 | 9 | (defn- mock? 10 | [{config :config}] 11 | (:mock? config)) 12 | 13 | (defn- connection-params 14 | [{config :config}] 15 | (if (get-in config [:smtp :host]) 16 | (:smtp config))) 17 | 18 | (defn- default-params 19 | [{config :config}] 20 | {:from (:sender config)}) 21 | 22 | (defn send-mail 23 | [{config :config, :as service} params] 24 | (let [mail-params (merge (default-params service) params)] 25 | (info "Sending email: " mail-params) 26 | (if-not (mock? service) 27 | (let [result (if-let [conn (connection-params service)] 28 | (postal/send-message conn mail-params) 29 | (postal/send-message mail-params))] 30 | (= :SUCCESS (:error result))) 31 | true))) 32 | 33 | (defrecord MailerService [config] 34 | 35 | ;; Reference implementation 36 | 37 | boundary/Mailer 38 | (send-mail [service args] 39 | (send-mail service args))) 40 | 41 | (defmethod ig/init-key :planwise.component/mailer 42 | [_ config] 43 | (map->MailerService config)) 44 | -------------------------------------------------------------------------------- /src/planwise/component/maps.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.component.maps 2 | (:require [planwise.boundary.maps :as boundary] 3 | [integrant.core :as ig] 4 | [taoensso.timbre :as timbre])) 5 | 6 | (timbre/refer-timbre) 7 | 8 | (defn mapserver-url 9 | [{config :config}] 10 | (:mapserver-url config)) 11 | 12 | (defn- data-path 13 | [{config :config} & args] 14 | (apply str (:data config) args)) 15 | 16 | (defrecord MapsService [config] 17 | boundary/Maps 18 | (mapserver-url [service] 19 | (mapserver-url service))) 20 | 21 | (defmethod ig/init-key :planwise.component/maps 22 | [_ config] 23 | (map->MapsService config)) 24 | -------------------------------------------------------------------------------- /src/planwise/component/regions.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.component.regions 2 | (:require [planwise.boundary.regions :as boundary] 3 | [integrant.core :as ig] 4 | [clojure.data.json :as json] 5 | [hugsql.core :as hugsql])) 6 | 7 | ;; ---------------------------------------------------------------------- 8 | ;; Auxiliary and utility functions 9 | 10 | (hugsql/def-db-fns "planwise/sql/regions.sql") 11 | 12 | (defn db->region [{json-bbox :bbox :as record}] 13 | (if json-bbox 14 | (let [coordinates (-> json-bbox json/read-str (get-in ["coordinates" 0])) 15 | [se ne nw sw se'] (map reverse coordinates)] 16 | (assoc record :bbox [sw ne])) 17 | record)) 18 | 19 | 20 | ;; ---------------------------------------------------------------------- 21 | ;; Service definition 22 | 23 | (defrecord RegionsService [db] 24 | 25 | boundary/Regions 26 | (list-regions [{:keys [db]}] 27 | (map db->region (select-regions (:spec db)))) 28 | (list-regions-with-preview [{:keys [db]} ids] 29 | (map db->region 30 | (select-regions-with-preview-given-ids (:spec db) {:ids ids}))) 31 | (list-regions-with-geo [{:keys [db]} ids simplify] 32 | (map db->region 33 | (select-regions-with-geo-given-ids (:spec db) 34 | {:ids ids :simplify simplify}))) 35 | (find-region [{:keys [db]} id] 36 | (db->region (select-region (:spec db) {:id id}))) 37 | 38 | (enum-regions-intersecting-envelope [{:keys [db]} envelope] 39 | (map :id (region-ids-intersecting-envelope (:spec db) envelope))) 40 | 41 | (get-region-geometry [{:keys [db]} id] 42 | (db->region (select-region-geometry (:spec db) {:id id})))) 43 | 44 | 45 | ;; ---------------------------------------------------------------------- 46 | ;; Service initialization 47 | 48 | (defmethod ig/init-key :planwise.component/regions 49 | [_ config] 50 | (map->RegionsService config)) 51 | -------------------------------------------------------------------------------- /src/planwise/component/runner.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.component.runner 2 | (:require [planwise.boundary.runner :as boundary] 3 | [integrant.core :as ig] 4 | [clojure.string :as str] 5 | [taoensso.timbre :as timbre] 6 | [clojure.java.shell :refer [sh]])) 7 | 8 | (timbre/refer-timbre) 9 | 10 | (defn- path-for 11 | [service kind name] 12 | (if kind 13 | (if-let [folder (kind service)] 14 | (apply str (kind service) name) 15 | (throw (ex-info (str "Path for " kind " not found") {:kind kind}))) 16 | name)) 17 | 18 | (defn run-external 19 | [service kind timeout name & args] 20 | (let [sh-args (cons (path-for service kind name) args) 21 | _ (info "Invoking" (str/join " " sh-args)) 22 | response (if timeout 23 | (deref (future (apply sh sh-args)) timeout {:exit :timeout, :err :timeout}) 24 | (apply sh sh-args))] 25 | (case (:exit response) 26 | 0 (:out response) 27 | (throw 28 | (ex-info 29 | (str "Error running external " kind ": " (:err response)) 30 | {:args args, :code (:exit response), :err (:err response)}))))) 31 | 32 | (defrecord RunnerService [config] 33 | boundary/Runner 34 | (run-external [service kind timeout name args] 35 | (apply run-external (concat [service kind timeout name] args)))) 36 | 37 | (defmethod ig/init-key :planwise.component/runner 38 | [_ config] 39 | (map->RunnerService config)) 40 | -------------------------------------------------------------------------------- /src/planwise/config.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.config 2 | (:require [duct.core.env :as env] 3 | [integrant.core :as ig] 4 | [clojure.java.io :as io] 5 | [clojure.string :as str] 6 | [planwise.model.project])) 7 | 8 | (defmethod env/coerce 'Bool [x _] 9 | (Boolean/valueOf x)) 10 | 11 | (def app-version 12 | (or (some-> (io/resource "planwise/version") 13 | slurp 14 | str/trim-newline 15 | str/trim) 16 | "development")) 17 | 18 | (defmethod ig/init-key :planwise.config/npm-deps [_ options] options) 19 | -------------------------------------------------------------------------------- /src/planwise/database.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.database 2 | (:require [integrant.core :as ig] 3 | [resauce.core :as resauce] 4 | [ragtime.jdbc :as rag-jdbc] 5 | [clojure.java.jdbc :as jdbc] 6 | [clojure.string :as str] 7 | [pandect.algo.sha1 :refer [sha1]] 8 | [taoensso.timbre :as timbre])) 9 | 10 | (timbre/refer-timbre) 11 | 12 | (defn load-and-execute-sql 13 | [database source] 14 | (try 15 | (let [sql-source (slurp source)] 16 | (jdbc/execute! (:spec database) sql-source)) 17 | (catch java.sql.SQLException e 18 | (fatal "Error loading/executing SQL file " (str source)) 19 | (throw e)))) 20 | 21 | (defn load-sql-functions 22 | [database] 23 | (doseq [source (resauce/resource-dir "planwise/plpgsql")] 24 | (load-and-execute-sql database source))) 25 | 26 | (defn table-exists? 27 | [database table] 28 | (-> (:spec database) 29 | (jdbc/query ["SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = ?" table]) 30 | empty? 31 | not)) 32 | 33 | (def ^:private colon (.getBytes ":" "US-ASCII")) 34 | (def ^:private comma (.getBytes "," "US-ASCII")) 35 | (def ^:private u= (.getBytes "u=" "US-ASCII")) 36 | (def ^:private d= (.getBytes "d=" "US-ASCII")) 37 | 38 | (defn- netstring [bs] 39 | (let [size (.getBytes (str (count bs)) "US-ASCII")] 40 | (byte-array (concat size colon bs comma)))) 41 | 42 | (defn- get-bytes [s] 43 | (.getBytes s "UTF-8")) 44 | 45 | (defn- coll->netstring [coll] 46 | (netstring (mapcat (comp netstring get-bytes) coll))) 47 | 48 | (defn- hash-migration [{:keys [up down]}] 49 | (sha1 (byte-array (concat u= (coll->netstring up) 50 | d= (coll->netstring down))))) 51 | 52 | (defn- add-hash-to-id [migration] 53 | (update migration :id str "#" (subs (hash-migration migration) 0 8))) 54 | 55 | (defmethod ig/init-key :planwise.database/migrations 56 | [_ [path]] 57 | (->> (rag-jdbc/load-resources path) 58 | (map add-hash-to-id))) 59 | 60 | (defmethod ig/init-key :planwise.database/pre-init 61 | [_ {:keys [db]}] 62 | (info "Loading PL/SQL functions into database") 63 | (load-sql-functions db)) 64 | -------------------------------------------------------------------------------- /src/planwise/endpoint/coverage.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.endpoint.coverage 2 | (:require [planwise.boundary.coverage :as coverage] 3 | [compojure.core :refer :all] 4 | [ring.util.response :refer [response]] 5 | [buddy.auth :refer [authenticated?]] 6 | [buddy.auth.accessrules :refer [restrict]] 7 | [integrant.core :as ig])) 8 | 9 | (defn endpoint-routes 10 | [service] 11 | (routes 12 | (GET "/algorithms" req 13 | (response 14 | (coverage/supported-algorithms service))))) 15 | 16 | (defmethod ig/init-key :planwise.endpoint/coverage 17 | [_ {:keys [coverage]}] 18 | (context "/api/coverage" [] 19 | (restrict (endpoint-routes coverage) {:handler authenticated?}))) 20 | -------------------------------------------------------------------------------- /src/planwise/endpoint/monitor.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.endpoint.monitor 2 | (:require [compojure.core :refer :all] 3 | [integrant.core :as ig] 4 | [ring.util.response :refer [response]] 5 | [buddy.auth :refer [authenticated?]] 6 | [buddy.auth.accessrules :refer [restrict]] 7 | [planwise.util.ring :as util])) 8 | 9 | (defn whoami-handler [request] 10 | (let [email (util/request-user-email request) 11 | user-id (util/request-user-id request)] 12 | (response {:id user-id :email email}))) 13 | 14 | (defn monitor-endpoint [system] 15 | (context "/api" [] 16 | (GET "/ping" [] {:status 200 17 | :headers {"content-type" "text/plain"} 18 | :body "pong"}) 19 | #_(GET "/crash" [] (throw (RuntimeException. "Crash"))) 20 | (GET "/whoami" req 21 | (restrict whoami-handler {:handler authenticated?})))) 22 | 23 | (defmethod ig/init-key :planwise.endpoint/monitor 24 | [_ config] 25 | (monitor-endpoint config)) 26 | -------------------------------------------------------------------------------- /src/planwise/endpoint/providers_set.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.endpoint.providers-set 2 | (:require [planwise.boundary.providers-set :as providers-set] 3 | [compojure.core :refer :all] 4 | [integrant.core :as ig] 5 | [taoensso.timbre :as timbre] 6 | [ring.util.response :refer [response status not-found]] 7 | [planwise.util.ring :as util] 8 | [buddy.auth :refer [authenticated?]] 9 | [buddy.auth.accessrules :refer [restrict]])) 10 | 11 | (timbre/refer-timbre) 12 | 13 | (defn- providers-set-routes 14 | [{service :providers-set}] 15 | (routes 16 | 17 | (GET "/" request 18 | (let [user-id (util/request-user-id request) 19 | sets (providers-set/list-providers-set service user-id)] 20 | (response sets))) 21 | 22 | (POST "/" [name :as request] 23 | (let [user-id (util/request-user-id request) 24 | csv-file (:tempfile (get (:multipart-params request) "file"))] 25 | (let [options {:name name 26 | :owner-id user-id} 27 | result (providers-set/create-and-import-providers service options csv-file) 28 | provider-set-id (:id result)] 29 | (response result)))) 30 | 31 | (DELETE "/" [id :as request] 32 | (let [user-id (util/request-user-id request) 33 | id (Integer. id)] 34 | (try 35 | (providers-set/delete-provider-set service id) 36 | {:status 204} 37 | (catch Exception e 38 | (error e "Failed to delete provider set") 39 | {:status 400 40 | :headers {} 41 | :body (ex-data e)})))))) 42 | 43 | 44 | (defn providers-set-endpoint 45 | [config] 46 | (context "/api/providers" [] 47 | (restrict (providers-set-routes config) {:handler authenticated?}))) 48 | 49 | (defmethod ig/init-key :planwise.endpoint/providers-set 50 | [_ config] 51 | (providers-set-endpoint config)) 52 | -------------------------------------------------------------------------------- /src/planwise/endpoint/regions.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.endpoint.regions 2 | (:require [compojure.core :refer :all] 3 | [integrant.core :as ig] 4 | [ring.util.response :refer [content-type response not-found]] 5 | [clojure.data.json :as json] 6 | [clojure.string :as str] 7 | [buddy.auth :refer [authenticated?]] 8 | [buddy.auth.accessrules :refer [restrict]] 9 | [planwise.boundary.regions :as regions])) 10 | 11 | ;; TODO consider storing a separate field with the name as provided by 12 | ;; OSM, or obtaining the name of the corresponding level-2 region. 13 | (defn- add-country-name [region] 14 | (let [country-name (->> region 15 | (:country) 16 | (#(clojure.string/split % #"-")) 17 | (map clojure.string/capitalize) 18 | (clojure.string/join " "))] 19 | (assoc region :country-name country-name))) 20 | 21 | (defn- endpoint-routes [service] 22 | (routes 23 | 24 | (GET "/" [] 25 | (let [regions (regions/list-regions service)] 26 | (->> regions 27 | (map add-country-name) 28 | (response)))) 29 | 30 | (GET "/:ids{[0-9\\,]+}/with-preview" [ids] 31 | (let [ids-array (map #(Integer/parseInt %) (str/split ids #",")) 32 | regions (regions/list-regions-with-preview service ids-array)] 33 | (->> regions 34 | (map add-country-name) 35 | (response)))) 36 | 37 | (GET "/:ids{[0-9\\,]+}/with-geo" [ids] 38 | (let [ids-array (map #(Integer/parseInt %) (str/split ids #",")) 39 | regions (regions/list-regions-with-geo service ids-array 0.0)] 40 | (->> regions 41 | (map add-country-name) 42 | (response)))))) 43 | 44 | (defn regions-endpoint [{service :regions}] 45 | (context "/api/regions" [] 46 | (restrict (endpoint-routes service) {:handler authenticated?}))) 47 | 48 | (defmethod ig/init-key :planwise.endpoint/regions 49 | [_ config] 50 | (regions-endpoint config)) 51 | -------------------------------------------------------------------------------- /src/planwise/endpoint/sources.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.endpoint.sources 2 | (:require [compojure.core :refer :all] 3 | [ring.util.response :refer [response]] 4 | [buddy.auth :refer [authenticated?]] 5 | [buddy.auth.accessrules :refer [restrict]] 6 | [planwise.component.sources :as sources] 7 | [planwise.util.ring :as util] 8 | [integrant.core :as ig])) 9 | 10 | (defn- sources-routes 11 | [service] 12 | (routes 13 | (GET "/" request 14 | (let [user-id (util/request-user-id request)] 15 | (response (sources/list-sources service user-id)))) 16 | 17 | (POST "/" [name unit :as request] 18 | (let [user-id (util/request-user-id request) 19 | csv-file (:tempfile (get (:multipart-params request) "csvfile")) 20 | options {:name name :unit unit :owner-id user-id}] 21 | (response (sources/import-from-csv service options csv-file)))))) 22 | 23 | (defn sources-endpoint 24 | [{service :sources}] 25 | (context "/api/sources" [] 26 | (restrict (sources-routes service) {:handler authenticated?}))) 27 | 28 | (defmethod ig/init-key :planwise.endpoint/sources 29 | [_ config] 30 | (sources-endpoint config)) 31 | -------------------------------------------------------------------------------- /src/planwise/main.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.main 2 | (:gen-class) 3 | (:require [clojure.java.io :as io] 4 | [duct.core :as duct] 5 | [planwise.config] 6 | [taoensso.timbre :as timbre])) 7 | 8 | (timbre/refer-timbre) 9 | 10 | (duct/load-hierarchy) 11 | 12 | (defn -main [& args] 13 | ;; Logging configuration for production 14 | (timbre/merge-config! {:level :info 15 | :ns-blacklist ["com.zaxxer.hikari.*" 16 | "org.apache.http.*" 17 | "org.eclipse.jetty.*"]}) 18 | (let [keys (or (duct/parse-keys args) [:duct/daemon])] 19 | (println "Starting" keys) 20 | (-> (duct/read-config (io/resource "planwise/prod.edn")) 21 | (duct/prep keys) 22 | (duct/exec keys)))) 23 | 24 | -------------------------------------------------------------------------------- /src/planwise/model/ident.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.model.ident 2 | (:require [schema.core :as s] 3 | [planwise.model.users :refer [User]])) 4 | 5 | (def Ident 6 | "User identity as found in the session cookie and JWE tokens" 7 | {:user-id s/Int 8 | :user-email s/Str 9 | 10 | ;; JWE tokens might include expiration information 11 | (s/optional-key :exp) s/Int}) 12 | 13 | ;; User identity related functions 14 | ;; The user identity is the user information carried around in the session 15 | ;; cookies and the JWE tokens. 16 | 17 | (s/defn user->ident :- Ident 18 | [user :- User] 19 | {:user-email (:email user) 20 | :user-id (:id user)}) 21 | 22 | (s/defn user-email :- s/Str 23 | [user-ident :- Ident] 24 | (:user-email user-ident)) 25 | 26 | (s/defn user-id :- s/Int 27 | [user-ident :- Ident] 28 | (:user-id user-ident)) 29 | -------------------------------------------------------------------------------- /src/planwise/model/project_review.cljc: -------------------------------------------------------------------------------- 1 | (ns planwise.model.project-review 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.string :refer [blank?]])) 4 | 5 | 6 | (s/def ::id number?) 7 | (s/def ::name (s/and string? (comp not blank?))) 8 | (s/def ::region-id number?) 9 | 10 | ;; Demographics 11 | (s/def ::source-set-id number?) 12 | (s/def ::target number?) 13 | (s/def ::unit-name string?) 14 | (s/def ::demographics (s/keys :req-un [::unit-name ::target])) 15 | 16 | (s/def ::provider-set-id number?) 17 | (s/def ::capacity number?) 18 | (s/def ::providers (s/keys :req-un [::capacity])) 19 | 20 | ;; Coverage 21 | (s/def ::driving-time number?) 22 | (s/def ::walking-time number?) 23 | (s/def ::distance number?) 24 | 25 | (s/def ::driving-options (s/keys :req-un [::driving-time])) 26 | (s/def ::walking-options (s/keys :req-un [::walking-time])) 27 | (s/def ::distance-options (s/keys :req-un [::distance])) 28 | 29 | 30 | (s/def ::filter-options (s/or :driving-options ::driving-options 31 | :walking-options ::walking-options 32 | :distance-options ::distance-options)) 33 | (s/def ::coverage (s/keys :req-un [::filter-options])) 34 | 35 | ;; Actions 36 | (s/def ::budget number?) 37 | (s/def ::actions (s/keys :req-un [::budget])) 38 | 39 | ;; Config 40 | (s/def ::config (s/keys :req-un [::demographics ::actions ::coverage ::providers])) 41 | 42 | (s/def ::validation (s/keys :req-un [::id ::owner-id ::name ::config ::provider-set-id ::source-set-id ::region-id])) 43 | -------------------------------------------------------------------------------- /src/planwise/model/projects2.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.model.projects2 2 | (:require [clojure.spec.alpha :as s])) 3 | 4 | (defn- default 5 | [map path value-default] 6 | (let [value (get-in map path)] 7 | (if (nil? value) 8 | (assoc-in map path value-default) 9 | map))) 10 | 11 | (defn apply-default 12 | [config] 13 | (-> config 14 | (default [:demographics :target] nil) 15 | (default [:analysis-type] "budget") 16 | (default [:actions :budget] nil) 17 | (default [:coverage :filter-options] {}) 18 | (default [:providers :capacity] 1))) 19 | 20 | (s/def ::target (s/nilable number?)) 21 | (s/def ::analysis-type string?) 22 | (s/def ::budget (s/nilable number?)) 23 | (s/def ::upgrade-budget (s/nilable number?)) 24 | (s/def ::build (s/nilable (s/coll-of map? :kind vector? :distinct true))) 25 | (s/def ::upgrade (s/nilable (s/coll-of map? :kind vector? :distinct true))) 26 | (s/def ::filter-options map?) 27 | 28 | (s/def ::demographics (s/keys :req-un [::target])) 29 | (s/def ::actions (s/keys :req-un [::budget] 30 | :opt-un [::build ::upgrade ::upgrade-budget])) 31 | (s/def ::coverage (s/keys :req-un [::filter-options])) 32 | (s/def ::capacity number?) 33 | (s/def ::providers (s/keys :req-un [::capacity])) 34 | 35 | (defmulti attribute-analysis-type-actions :analysis-type) 36 | (defmethod attribute-analysis-type-actions "budget" [_] 37 | (s/keys :req-un [::actions])) 38 | (defmethod attribute-analysis-type-actions "action" [_] 39 | (s/keys)) 40 | 41 | (s/def ::config-base (s/keys :req-un [::demographics ::coverage ::providers ::analysis-type])) 42 | (s/def ::config (s/nilable (s/merge ::config-base (s/multi-spec attribute-analysis-type-actions :analysis-type)))) 43 | (s/def ::id number?) 44 | (s/def ::name string?) 45 | (s/def ::provider-set-id (s/nilable number?)) 46 | (s/def ::region-id (s/nilable number?)) 47 | (s/def ::source-set-id (s/nilable number?)) 48 | 49 | (s/def ::project (s/keys :req-un [::id ::owner-id ::name ::config ::provider-set-id ::source-set-id ::region-id])) 50 | -------------------------------------------------------------------------------- /src/planwise/model/providers.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.model.providers 2 | (:require [planwise.util.collections :refer [sum-by merge-collections-by]])) 3 | 4 | (defn merge-provider 5 | "Merge two or more provider-like maps, but sum their capacity related fields 6 | and the satisfied demand." 7 | [& providers] 8 | (-> (apply merge providers) 9 | (assoc :capacity (sum-by :capacity providers) 10 | :satisfied-demand (sum-by :satisfied-demand providers) 11 | :used-capacity (sum-by :used-capacity providers) 12 | :free-capacity (sum-by :free-capacity providers) 13 | :reachable-demand (sum-by :reachable-demand providers)))) 14 | 15 | (defn merge-providers 16 | "Merge providers by id, but perform addition for the fields :capacity, 17 | :satisfied-demand, :used-capacity and :free-capacity." 18 | [& colls] 19 | (apply merge-collections-by :id merge-provider colls)) 20 | -------------------------------------------------------------------------------- /src/planwise/model/users.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.model.users 2 | (:require [schema.core :as s]) 3 | (:import [org.joda.time DateTime])) 4 | 5 | (def User 6 | "A system user" 7 | {:id s/Int 8 | :email s/Str 9 | :full-name (s/maybe s/Str) 10 | :last-login (s/maybe DateTime)}) 11 | -------------------------------------------------------------------------------- /src/planwise/router.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.router 2 | (:require [integrant.core :as ig] 3 | [compojure.core :as compojure])) 4 | 5 | (defn make-router 6 | [{:keys [handlers middleware]}] 7 | (let [router (apply compojure/routes handlers)] 8 | ((apply comp (reverse middleware)) router))) 9 | 10 | (defmethod ig/init-key :planwise.router/api 11 | [_ config] 12 | (make-router config)) 13 | 14 | (defmethod ig/init-key :planwise.router/app 15 | [_ config] 16 | (make-router config)) 17 | 18 | -------------------------------------------------------------------------------- /src/planwise/tasks/build_icons.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.tasks.build-icons 2 | (:require [clojure.xml :as xml] 3 | [clojure.java.io :as io] 4 | [clojure.string :as string] 5 | [taoensso.timbre :as timbre]) 6 | (:gen-class)) 7 | 8 | (timbre/refer-timbre) 9 | 10 | (defn process-svg [file] 11 | (info "Processing" file) 12 | (with-open [input (io/input-stream file)] 13 | (let [xml-tree (xml/parse input) 14 | title-tag (filter #(= :title (:tag %)) (xml-seq xml-tree)) 15 | name (-> title-tag first :content first) 16 | updated-xml (-> xml-tree 17 | (assoc-in [:attrs :id] (str "icon-" name)) 18 | (assoc-in [:attrs :data-name] (str "icon-" name)) 19 | (update-in [:attrs] dissoc :xmlns) 20 | (assoc :tag :symbol))] 21 | updated-xml))) 22 | 23 | (defn wrap-in-svg [symbols] 24 | {:tag :svg, 25 | :attrs {:xmlns "http://www.w3.org/2000/svg" 26 | :width "0" 27 | :height "0" 28 | :class "svg-hidden"} 29 | :content (vec symbols)}) 30 | 31 | (defn process-svgs [] 32 | (let [target "resources/svg/icons.svg" 33 | dirname "resources/svg/icons" 34 | svg (->> dirname 35 | (io/file) 36 | (file-seq) 37 | (filter #(string/ends-with? % ".svg")) 38 | (map process-svg) 39 | (wrap-in-svg))] 40 | (spit target 41 | (str 42 | "\n" 43 | (with-out-str (xml/emit-element svg)))))) 44 | 45 | (defn -main [& args] 46 | (timbre/set-level! :info) 47 | (info "Creating resources/svg/icons.svg from resources/svg/icons/*") 48 | (process-svgs)) 49 | -------------------------------------------------------------------------------- /src/planwise/util/collections.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.util.collections) 2 | 3 | (defn find-by 4 | [coll field value] 5 | (reduce #(when (= value (field %2)) (reduced %2)) nil coll)) 6 | 7 | (defn sum-by 8 | [key coll] 9 | (reduce + (filter number? (map key coll)))) 10 | 11 | (defn merge-collections-by 12 | [key merge-fn & colls] 13 | (map (fn [[id same-key-maps]] (apply merge-fn same-key-maps)) 14 | (group-by key (apply concat colls)))) 15 | 16 | (defn map-vals 17 | [f m] 18 | (reduce-kv (fn [acc k v] (assoc acc k (f v))) {} m)) 19 | 20 | (defn csv-data->maps 21 | "Converts the result of csv/read-csv (ie. a collection of vectors of strings) 22 | into a collection of maps using the first row (converted to keywords) as keys" 23 | [csv-data] 24 | (map zipmap 25 | (->> (first csv-data) 26 | (map keyword) 27 | repeat) 28 | (rest csv-data))) 29 | -------------------------------------------------------------------------------- /src/planwise/util/files.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.util.files 2 | (:require [clojure.java.io :as io])) 3 | 4 | (defn delete-files-recursively [fname & [silently]] 5 | (letfn [(delete-f [file] 6 | (when (.isDirectory file) 7 | (doseq [child-file (.listFiles file)] 8 | (delete-f child-file))) 9 | (clojure.java.io/delete-file file silently))] 10 | (delete-f (clojure.java.io/file fname)))) 11 | 12 | (defn create-temp-file 13 | [parent prefix suffix] 14 | (let [parent (io/as-file parent)] 15 | (io/make-parents parent) 16 | (.mkdir parent) 17 | (str (java.io.File/createTempFile prefix suffix parent)))) 18 | -------------------------------------------------------------------------------- /src/planwise/util/hash.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.util.hash) 2 | 3 | (defn update-if-contains 4 | [hash key fun & args] 5 | (if (contains? hash key) 6 | (apply update hash key fun args) 7 | hash)) 8 | 9 | (defn update-if 10 | [hash key fun & args] 11 | (if-not (nil? (get hash key)) 12 | (apply update hash key fun args) 13 | hash)) 14 | 15 | (defn update* 16 | [hash key fun & args] 17 | (if-not (nil? hash) 18 | (apply update hash key fun args) 19 | hash)) 20 | -------------------------------------------------------------------------------- /src/planwise/util/numbers.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.util.numbers) 2 | 3 | ;; abs version which works for any numeric type 4 | ;; Math/abs only works for Java's int, float and double 5 | (defn abs 6 | "(abs n) is the absolute value of n" 7 | [n] 8 | (cond 9 | (not (number? n)) (throw (IllegalArgumentException. 10 | "abs requires a number")) 11 | (neg? n) (- n) 12 | :else n)) 13 | 14 | (defn- scale [x y] 15 | (if (or (zero? x) (zero? y)) 16 | 1 17 | (abs x))) 18 | 19 | (defn float= 20 | ([x y] (float= x y 0.00001)) 21 | ([x y epsilon] (<= (abs (- x y)) 22 | (* (scale x y) epsilon)))) 23 | 24 | (defn nilable-float= 25 | "Like float= but tolerates nil parameters" 26 | ([x y] (nilable-float= x y 0.00001)) 27 | ([x y epsilon] 28 | (cond 29 | (and (nil? x) (nil? y)) true 30 | (and x y) (float= x y epsilon) 31 | :else false))) 32 | 33 | (defn float< 34 | ([x y] (float< x y 0.00001)) 35 | ([x y epsilon] (< x 36 | (- y (* (scale x y) epsilon))))) 37 | 38 | (defn float> 39 | ([x y] (float< y x)) 40 | ([x y epsilon] (float< y x epsilon))) 41 | 42 | (defn float<= 43 | ([x y] (not (float> x y))) 44 | ([x y epsilon] (not (float> x y epsilon)))) 45 | 46 | (defn float>= 47 | ([x y] (not (float< x y))) 48 | ([x y epsilon] (not (float< x y epsilon)))) 49 | -------------------------------------------------------------------------------- /src/planwise/util/str.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.util.str 2 | (:require [clojure.string :as str])) 3 | 4 | (defn trim-to-int [s] 5 | (let [trimmed (if (string? s) (-> s str/trim-newline str/trim) s)] 6 | (Integer. trimmed))) 7 | 8 | (defn try-trim-to-int [s] 9 | (try 10 | (trim-to-int s) 11 | (catch Exception e nil))) 12 | 13 | -------------------------------------------------------------------------------- /test/planwise/boundary/coverage_test.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.boundary.coverage-test 2 | (:require [clojure.test :refer :all] 3 | [shrubbery.core :refer :all] 4 | [planwise.boundary.coverage :as sut])) 5 | 6 | 7 | (deftest enumerate-algorithm-options-test 8 | (let [service (stub sut/CoverageService)] 9 | (is (empty? (sut/enumerate-algorithm-options service :foo)) 10 | "returns nil for invalid algorithms")) 11 | 12 | (let [service (stub sut/CoverageService 13 | {:supported-algorithms {:foo {}}})] 14 | (is (empty? (sut/enumerate-algorithm-options service :foo)) 15 | "returns nil for algorithms without options")) 16 | 17 | (let [service (stub sut/CoverageService 18 | {:supported-algorithms 19 | {:foo {:criteria {:bar {:type :enum 20 | :options [{:value 1} 21 | {:value 2} 22 | {:value 3}]}}}}})] 23 | (is (= [{:bar 1} {:bar 2} {:bar 3}] (sut/enumerate-algorithm-options service :foo)) 24 | "returns the possible criterias for enum options")) 25 | 26 | (let [service (stub sut/CoverageService 27 | {:supported-algorithms 28 | {:foo {:criteria {:bar {:type :number}}}}})] 29 | (is (empty? (sut/enumerate-algorithm-options service :foo)) 30 | "ignores non enum options")) 31 | 32 | (let [service (stub sut/CoverageService 33 | {:supported-algorithms 34 | {:foo {:criteria {:bar {:type :enum 35 | :options [{:value 1} {:value 2}]} 36 | :baz {:type :enum 37 | :options [{:value 3} {:value 4}]}}}}})] 38 | (is (= [{:bar 1 :baz 3} {:bar 1 :baz 4} 39 | {:bar 2 :baz 3} {:bar 2 :baz 4}] 40 | (sut/enumerate-algorithm-options service :foo)) 41 | "returns the possible criteria combinations for multiple enum options"))) 42 | -------------------------------------------------------------------------------- /test/planwise/component/regions_test.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.component.regions-test 2 | (:require [clojure.test :refer :all] 3 | [planwise.boundary.regions :as regions] 4 | [planwise.test-system :as test-system] 5 | [integrant.core :as ig]) 6 | (:import [org.postgis PGgeometry])) 7 | 8 | (defn sample-polygon [] 9 | (PGgeometry. (str "SRID=4326;MULTIPOLYGON(((0 0, 0 1, 1 1, 1 0, 0 0)))"))) 10 | 11 | (def fixture-data 12 | [[:regions 13 | [{:id 1 :country "kenya" :name "Kenya" :admin_level 2 :the_geom (sample-polygon) :preview_geom nil :total_population 1000}]]]) 14 | 15 | (defn test-config 16 | [] 17 | (test-system/config 18 | {:planwise.test/fixtures {:fixtures fixture-data} 19 | :planwise.component/regions {:db (ig/ref :duct.database/sql)}})) 20 | 21 | (deftest total-population-is-retrieved-with-list-regions 22 | (test-system/with-system (test-config) 23 | (let [service (:planwise.component/regions system) 24 | [kenya & rest] (regions/list-regions service)] 25 | (is (= 1000 (:total-population kenya)))))) 26 | 27 | (deftest total-population-is-retrieved-with-list-regions-with-preview 28 | (test-system/with-system (test-config) 29 | (let [service (:planwise.component/regions system) 30 | [kenya & rest] (regions/list-regions-with-preview service [1])] 31 | (is (= 1000 (:total-population kenya)))))) 32 | 33 | (deftest total-population-is-retrieved-with-list-regions-with-geo 34 | (test-system/with-system (test-config) 35 | (let [service (:planwise.component/regions system) 36 | [kenya & rest] (regions/list-regions-with-geo service [1] 0.5)] 37 | (is (= 1000 (:total-population kenya)))))) 38 | 39 | (deftest enum-regions-intersecting-envelope-test 40 | (test-system/with-system (test-config) 41 | (let [service (:planwise.component/regions system)] 42 | (is (empty? (regions/enum-regions-intersecting-envelope service {:min-lat 2 :max-lat 3 :min-lon 1 :max-lon 2}))) 43 | (is (= [1] (regions/enum-regions-intersecting-envelope service {:min-lat 0 :max-lat 2 :min-lon 0 :max-lon 2}))) 44 | (is (= [1] (regions/enum-regions-intersecting-envelope service {:min-lat 0.5 :max-lat 2 :min-lon 0.5 :max-lon 2})))))) 45 | -------------------------------------------------------------------------------- /test/planwise/component/taskmaster_test.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.component.taskmaster-test 2 | (:require [planwise.component.taskmaster :as sut] 3 | [clojure.test :as t])) 4 | 5 | ;; Dummy test implementation of TaskDispatcher 6 | 7 | (def next-task-id (atom 0)) 8 | (def tasks (atom [])) 9 | 10 | (defn pop-task 11 | [] 12 | (let [current-tasks @tasks 13 | task (peek current-tasks) 14 | updated-tasks (if (empty? current-tasks) 15 | current-tasks 16 | (pop current-tasks))] 17 | (if (compare-and-set! tasks current-tasks updated-tasks) 18 | task 19 | (recur)))) 20 | 21 | (defn queue-task 22 | ([task-fn] 23 | (let [task-id (swap! next-task-id inc) 24 | task {:task-id task-id 25 | :task-fn task-fn}] 26 | (swap! tasks #(vec (cons task %))) 27 | task-id))) 28 | 29 | (def dummy-task-dispatcher 30 | (reify sut/TaskDispatcher 31 | (next-task [this] 32 | (pop-task)) 33 | (task-completed [this task-id result] 34 | (println (str "Task #" task-id " completed successfully with result " result))) 35 | (task-failed [this task-id error-info] 36 | (println (str "Task #" task-id " failed with " error-info))))) 37 | -------------------------------------------------------------------------------- /test/planwise/model/scenarios_test.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.model.scenarios-test 2 | (:require [planwise.model.scenarios :as sut] 3 | [clojure.test :refer :all])) 4 | 5 | 6 | (deftest test-next-name-from-initial 7 | (is (= "Scenario A" (sut/next-name-from-initial ["Initial"]))) 8 | (is (= "Scenario A" (sut/next-name-from-initial ["Initial" "Scenario" "Other Name"]))) 9 | (is (= "Scenario A" (sut/next-name-from-initial ["Initial" "Scenario 1"]))) 10 | (is (= "Scenario B" (sut/next-name-from-initial ["Initial" "Scenario A"]))) 11 | (is (= "Scenario C" (sut/next-name-from-initial ["Initial" "Scenario B"]))) 12 | (is (= "Scenario Z" (sut/next-name-from-initial ["Initial" "Scenario Y"]))) 13 | (is (= "Scenario 1" (sut/next-name-from-initial ["Initial" "Scenario Z"]))) 14 | (is (= "Scenario 2" (sut/next-name-from-initial ["Initial" "Scenario Z" "Scenario 1"]))) 15 | (is (= "Scenario 43" (sut/next-name-from-initial ["Initial" "Scenario Z" "Scenario 42"])))) 16 | 17 | (deftest test-next-name-for-copy 18 | (is (= "Foo (1)" (sut/next-name-for-copy ["Foo"] "Foo"))) 19 | (is (= "Foo (1)" (sut/next-name-for-copy ["Foo" "Bar (1)"] "Foo"))) 20 | (is (= "Foo (1)" (sut/next-name-for-copy ["Foo" "Foo2 (1)"] "Foo"))) 21 | (is (= "Foo (2)" (sut/next-name-for-copy ["Foo" "Foo (1)"] "Foo"))) 22 | (is (= "Foo (2)" (sut/next-name-for-copy ["Foo" "Foo(1)"] "Foo"))) 23 | (is (= "Foo (2)" (sut/next-name-for-copy ["Foo" "Foo (1)"] "Foo (1)"))) 24 | (is (= "Foo (3)" (sut/next-name-for-copy ["Foo (2)"] "Foo")))) 25 | -------------------------------------------------------------------------------- /test/planwise/test_utils.clj: -------------------------------------------------------------------------------- 1 | (ns planwise.test-utils 2 | (:import [org.postgis PGgeometry])) 3 | 4 | (defn sample-polygon 5 | ([] 6 | (sample-polygon :small)) 7 | ([kind] 8 | (case kind 9 | :small (PGgeometry. (str "SRID=4326;MULTIPOLYGON(((1 1, 1 2, 2 2, 2 1, 1 1)))")) 10 | :large (PGgeometry. (str "SRID=4326;MULTIPOLYGON(((-30 -30, -30 60, 60 60, 60 -30, -30 -30)))"))))) 11 | 12 | (defn make-point [lat lon] 13 | (PGgeometry. (str "SRID=4326;POINT(" lon " " lat ")"))) 14 | -------------------------------------------------------------------------------- /test/resources/other-sites.csv: -------------------------------------------------------------------------------- 1 | id,type,name,lat,lon,capacity,tags 2 | 5,clinic,lorem,10,8,600,foo 3 | 6,clinic,ipsum,11,7,700,bar 4 | -------------------------------------------------------------------------------- /test/resources/pointe-noire-byte.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/instedd/planwise/6da765a09481c6b86169dab585bbea71aabe120e/test/resources/pointe-noire-byte.tif -------------------------------------------------------------------------------- /test/resources/pointe-noire-float.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/instedd/planwise/6da765a09481c6b86169dab585bbea71aabe120e/test/resources/pointe-noire-float.tif -------------------------------------------------------------------------------- /test/resources/sites.csv: -------------------------------------------------------------------------------- 1 | id,type,name,lat,lon,capacity,tags 2 | 1,clinic,foo,10,8,600,lorem 3 | 2,clinic,bar,11,7,700,ipsum 4 | 3,clinic,foofoo,12,8,680,dolor sit 5 | 4,clinic,barbar,13,9,560,amet 6 | -------------------------------------------------------------------------------- /test/resources/test.edn: -------------------------------------------------------------------------------- 1 | {:duct.module/sql 2 | {:database-url #duct/env ["TEST_DATABASE_URL" Str :or "jdbc:postgresql://localhost:5433/planwise-test?user=planwise&password=planwise"]} 3 | 4 | :planwise.test/logger 5 | {} 6 | 7 | :planwise.test/db-pre-setup 8 | {:db #ig/ref :duct.database/sql} 9 | 10 | :planwise.database/migrations 11 | ["migrations"] 12 | 13 | :duct.migrator/ragtime 14 | {:migrations ^:replace #ig/ref :planwise.database/migrations 15 | :pre-run #ig/ref :planwise.test/db-pre-setup} 16 | 17 | :planwise.test/fixtures 18 | {:db #ig/ref :duct.database/sql 19 | :pre-run #ig/ref :duct/migrator}} 20 | --------------------------------------------------------------------------------