Repository: atlasr-org/atlasr Branch: master Commit: 6c36edf2367d Files: 31 Total size: 63.8 KB Directory structure: gitextract_o8etyil8/ ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── justfile ├── public/ │ └── static/ │ ├── css/ │ │ └── default.css │ ├── image/ │ │ └── README.md │ └── javascript/ │ └── main.js └── source/ ├── api/ │ ├── geocode/ │ │ ├── indexer/ │ │ │ ├── Cargo.toml │ │ │ └── src/ │ │ │ └── main.rs │ │ └── searcher/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── main.rs │ └── tile/ │ ├── Cargo.toml │ ├── database/ │ │ └── .keep │ ├── diesel.toml │ └── src/ │ ├── main.rs │ ├── models.rs │ └── schema.rs ├── client/ │ ├── elm.json │ ├── src/ │ │ ├── Atlasr/ │ │ │ ├── Geocode.elm │ │ │ ├── Map.elm │ │ │ ├── MapboxGL/ │ │ │ │ ├── Options.elm │ │ │ │ └── Ports.elm │ │ │ ├── Position.elm │ │ │ └── Route.elm │ │ ├── Main.elm │ │ └── index.html │ └── tests/ │ ├── elm-package.json │ └── unit/ │ └── Atlasr/ │ └── Test/ │ └── Position.elm └── server/ ├── Cargo.toml └── src/ └── main.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /public/static/css/mapbox-gl.* /public/static/index.html /public/static/javascript/*.elm.js* /public/static/javascript/mapbox-gl.* /source/api/geocode/*.tsv /source/api/geocode/index/ /source/api/geocode/indexer/target/ /source/api/geocode/searcher/target/ /source/api/tile/target/ /source/api/tile/database/*.mbtiles /source/client/elm-stuff /source/client/tests/elm-stuff /source/server/target .idea/ .DS_Store ================================================ FILE: .gitmodules ================================================ [submodule "source/api/graphhopper"] path = source/api/route url = https://github.com/atlasr-org/graphhopper branch = master [submodule "source/map-style"] path = source/map-style url = https://github.com/atlasr-org/osm-liberty ================================================ FILE: LICENSE ================================================ New BSD License Copyright © 2018, Ivan Enderlin. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Atlasr nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # 🌍 Atlasr Atlasr is a truly open-source and free map browser. The goal is threefold: 1. Learn about all the layers and components that make a map, 2. Provide a ready-to-use set of tools to build a 100% open-source, free, and standalone map browser, 3. Provide an alternative service to the famous [Google Maps][GMaps] or [Apple Maps][AMAps], with no ads, with respect to user privacy, with 100% open-source and free data, and the top 10 features of Google Maps. A map architecture is composed of the following components: * **Map data**, A giant database containing all information about roads, trails, cafés, railway stations, buildings etc. The biggest major project is [OpenStreetMap][OSM], * **Tile server**, A program that, given a specific region, reads the map database and compiles those information in a certain format like [`.mbtiles`][MBtiles] for instance. Each region of a planet is named a _tile_. A tile is defined by a longitude, a latitude, and a zoom (an altitude scale), * **Map renderer**, A program that, given a set of tiles, renders/draws a map, so each roads, buildings etc. finally come alive at this step. A map renderer requires at least the following components: * **Tile decoder** to decode the tiles received from the tile server, * **Styles** to know how to draw the data from the map (e.g. “roads must be blue with a white strike”), * **Fonts** to render texts, * **Icons** to represent some places with an image (like hospitals, police stations, parcs etc.). * **Geocoding**, A program that can find the longitude and the latitude of a labellised element on earth, like a postal address, a building name, or a river name for examples, * **Routing**, A program that is able to find a path/route between one or many points (longitude + latitude) given some constraints (like the vehicule type, the road preference etc.). A route preferably comes with descriptions, like “Turn right in 100m”, “Follow A10 for 7km” etc. Obviously, each component comes with thousands of details and constraints. The previous list is a high overview of how it works. ## Goals The open-source map ecosystem is mature. Many projects already exist to address one component of the map architecture. However, a mainstream tool that combined all these projects, based on 100% open-source and free data, is still missing. Atlasr aims to be the response to this problem. The quality must be comparable with Google Maps or Apple Maps: * Smooth and fast experience, * Reliable data, * Beautiful design for the map and the UI. ## Roadmap The main technologies are the following: [Rust] for the server, [Elm] for the client, and [PostgreSQL]/[SQLite] for the databases. The actual roadmap is the following: * **Map data**, _all data comes from OpenStreetMap_, * **Tile server** [`source/api/tile/`]: * [x] Vector tiles are pre-computed by [OpenMapTiles][OMT], which relies on OpenStreetMap. * [x] Format is `.mbtiles`, * [x] Solid and robust tile server. * **Map renderer** [`source/map-style`]: * [x] Use [Mapbox GL JS] to render the map, * [x] Elm ports to Mapbox GL (JavaScript) for the Web UI, * [x] Use [OSM Liberty] for the style, forked to remove the dependencies to external services, and to use only “local” data, * [x] Icons/sprites, * [ ] Fonts. * **Geocoding** [`source/api/geocode`]: * [x] Data are pre-computed by [OSM Names][OSMNames], which relies on OpenStreetMap, * [x] Transform the data to build an index for the search engine, * [x] Custom and fast search engine to query the index, build with [Tantivy], * [x] Solid and robust server. * **Routing** [`source/api/route/`]: * [x] Delegate all the works to [GraphHopper], only use the open-source API. API to use: * [x] Routing API, * [ ] Isochrone API. * **HTTP server** [`source/server/`]: * [x] Fast and robust HTTP server between the client and all the API. * **Client**/**Web UI** [`source/client`]: * [x] Mobile-first, * [x] Smooth and fast, * [x] Search one or many positions (geocoding), * [x] Search a route (routing), * [ ] Search a route with constraints, * [ ] Enhance data with Wikipedia (photos, descriptions, metadata etc.), * [ ] All links are sharable, * More features. **Current focus**: The current hard work is to provide all map components as local and standalone instances. Everything has been addressed except the fonts in the map renderer (yet). **Next focus**: Replace the top 10 features on Google Maps. ### Screenshots * **Map renderer**: The tiles, the style, the icons, everything comes from Atlasr. No external service is used. ![Map](./documentation/images/map.jpg) * **Geocoding** and **Routing**: Atlasr is able to geoencode 2 postal addresses, and find a route between the two: ![Geocoding and routing](./documentation/images/routing.jpg) ## Usages/Installations [`just`] is required to run all the commands. Run `just --list` to get an overview of all the commands. * **Tile server**: ```sh $ # Install API tile server. $ just install-api-tile $ # Run the tile server. $ just run-api-tile ``` * **Geocoding**: ```sh $ # Download the data, install the indexer, and install the search engine. $ just install-api-geocode $ # Run the geocoding server. $ just run-api-geocode-searcher ``` * **Routing**: ```sh $ # Install the geocoding server. $ just install-api-route $ # Run the geocoding server. $ just run-api-route ``` * **Client**/**Web UI**: ```sh $ # Install the HTTP server for the client. $ just install-server $ # Run the HTTP server for the client. $ just run-server $ # Install the Web UI, its dependencies, and prepare the frontend. $ just install-client $ # Open the client. $ just open ``` Enjoy! ## License The entire project is under the BSD-3-Clause license. Please read the `LICENSE` file. [OSM]: https://openstreetmap.org/ [MBTiles]: https://github.com/mapbox/mbtiles-spec [GMaps]: https://google.com/maps [AMaps]: https://maps.apple.com/ [Rust]: https://rust-lang.org/ [Elm]: https://elm-lang.org/ [PostgreSQL]: https://www.postgresql.org/ [SQLite]: https://www.sqlite.org/ [OMT]: https://openmaptiles.org/ [Mapbox GL JS]: https://www.mapbox.com/mapbox-gl-js/api/ [OSM Liberty]: https://github.com/atlasr-org/osm-liberty/ [OSMNames]: http://osmnames.org/ [Tantivy]: https://github.com/tantivy-search/tantivy/ [GraphHopper]: https://www.graphhopper.com/ [`just`]: https://github.com/casey/just/ ================================================ FILE: justfile ================================================ # vim: filetype=just mapbox_gl_js_version = "0.50.0" server_address = "localhost:8889" geocode_api_address = "localhost:8990" route_api_address = "localhost:8989" geocode_data_planet = "https://github.com/OSMNames/OSMNames/releases/download/v2.0.4/planet-latest_geonames.tsv.gz" tile_api_address = "127.0.0.1:8991" # Open Atlasr in your favorite browser. open: install open http://{{server_address}} # Install Atlasr! install: install-server install-api install-client # Test Atlasr. test: test-server test-client # Uninstall Atlasr. uninstall: uninstall-server uninstall-api uninstall-client # Install the HTTP server. install-server: cd source/server && \ SERVER_ADDRESS={{server_address}} \ ROUTE_API_ADDRESS={{route_api_address}} \ GEOCODE_API_ADDRESS={{geocode_api_address}} \ TILE_API_ADDRESS={{tile_api_address}} \ cargo build --release # Test the HTTP server. test-server: cd source/server && cargo test # Uninstall the HTTP server. uninstall-server: rm -rf source/server/Cargo.lock source/server/target # Run the HTTP server (will not exit). run-server: install-server cd source/server && cargo run --release # Install all the APIs. install-api: install-api-geocode install-api-route install-api-tile # Uninstall all the APIs. uninstall-api: uninstall-api-geocode uninstall-api-route # Install the geocode API. install-api-geocode: install-api-geocode-data install-api-geocode-indexer install-api-geocode-searcher # Install/download data for the geocode API. install-api-geocode-data: install-api-geocode-indexer cd source/api/geocode && \ curl -L {{geocode_data_planet}} > planet.tsv.gz && \ gzip -df planet.tsv.gz && \ mkdir -p index && \ ./indexer/target/release/atlasr-api-geocode-indexer --source-file planet.tsv --index-directory index # Install the indexer for the geocode API. install-api-geocode-indexer: cd source/api/geocode/indexer && cargo build --release # Install the searcher for the geocode API. install-api-geocode-searcher: cd source/api/geocode/searcher && \ GEOCODE_API_ADDRESS={{geocode_api_address}} \ cargo build --release uninstall-api-geocode: # noop run-api-geocode-searcher: cd source/api/geocode/searcher && cargo run --release # Install the route API (GraphHopper). install-api-route: git submodule update --init source/api/route # Uninstall the route API (GraphHopper). uninstall-api-route: # noop # Run the route API (GraphHopper) for a particular PBF zone. run-api-route map_file='europe_switzerland': install-api-route cd source/api/route && ./graphhopper.sh web {{map_file}}.pbf # Install the tile API. install-api-tile: cd source/api/tile && \ SERVER_ADDRESS={{server_address}} \ TILE_API_ADDRESS={{tile_api_address}} \ cargo build --release # Run the tile API. run-api-tile: cd source/api/tile && cargo run --release # Uninstall the tile API. uninstall-api-tile: # noop #run-api-tile map_file='europe_switzerland': install-api-tile # cd source/api/tile && \ # curl -L 'https://openmaptiles.com/download/WyJjOWUzNGM1NS04MDQxLTQ4MTMtYmUzMy0yNmFjMGUyN2I5MWIiLCItMSIsODcxM10.DsGknA.wk4qsZRjBSL8gQrp22h2CRpCyi4/osm-2017-07-03-v3.6.1-{{map_file}}.mbtiles?usage=open-source' > database/{{map_file}}.mbtiles # Install client. install-client: install-client-index install-client-application install-client-dependencies # Test client. test-client: test-client-application # Uninstall client. uninstall-client: uninstall-client-index uninstall-client-application # Create a public `index.html` file. SED_INPLACE_OPTION = "\"s@{MAP-PLACEHOLDER.svg}@`cat public/static/image/map-placeholder.svg | sed -E 's/^ +//g; s/\"/'\"'\"'/g; s//%3e/g; s/\\#/%23/g' | tr -d \"\\n\"`@\"" install-client-index: #!/bin/sh cp source/client/src/index.html public/static/index.html if [[ "$OSTYPE" == "darwin"* ]]; then sed -i '' {{SED_INPLACE_OPTION}} public/static/index.html else sed -i {{SED_INPLACE_OPTION}} public/static/index.html fi # Remove the public `index.html` file. uninstall-client-index: rm -f public/static/index.html # Compile the Elm application to JS. install-client-application: cd source/client && elm make src/Main.elm --optimize --output ../../public/static/javascript/application.elm.js cd public/static/javascript && \ uglifyjs application.elm.js \ --compress 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' | \ uglifyjs \ --mangle \ --output=application.min.elm.js && \ gzip --best --stdout application.min.elm.js > application.min.elm.js.gz && \ brotli --best --stdout application.min.elm.js > application.min.elm.js.br # Test the Elm application. test-client-application: cd source/client && elm-test tests/unit/ # Remove the Elm build artifact. uninstall-client-application: rm -f public/static/javascript/application.elm.js # Install client dependencies. install-client-dependencies: install-mapbox-gl-js install-mapbox-gl-css # Uninstall client dependencies. uninstall-client-dependencies: uninstall-mapbox-gl-js uninstall-mapbox-gl-css # Install Mapbox GL JS, so JS and SourceMap. install-mapbox-gl-js: cd public/static/javascript/ && \ curl -L https://api.tiles.mapbox.com/mapbox-gl-js/v{{mapbox_gl_js_version}}/mapbox-gl.js > mapbox-gl.js && \ curl -L https://api.tiles.mapbox.com/mapbox-gl-js/v{{mapbox_gl_js_version}}/mapbox-gl.js.map > mapbox-gl.js.map && \ uglifyjs \ --compress \ --mangle \ --output=mapbox-gl.min.js mapbox-gl.js && \ gzip --best --stdout mapbox-gl.min.js > mapbox-gl.min.js.gz && \ brotli --best --stdout mapbox-gl.min.js > mapbox-gl.min.js.br # Install Mapbox GL CSS install-mapbox-gl-css: cd public/static/css/ && \ curl -L https://api.tiles.mapbox.com/mapbox-gl-js/v{{mapbox_gl_js_version}}/mapbox-gl.css > mapbox-gl.css # Uninstall Mapbox GL JS. uninstall-mapbox-gl-js: rm public/static/javascript/mapbox-gl.js rm public/static/javascript/mapbox-gl.js.map # Uninstall Mapbox GL CSS. uninstall-mapbox-gl-css: rm public/static/css/mapbox-gl.css # Local Variables: # mode: makefile # End: ================================================ FILE: public/static/css/default.css ================================================ :root { --brand-color : hsla(221, 40%, 14%, 1 ); --alt-color-lighter : hsla( 9, 93%, 100%, 1 ); --alt-color-light : hsla( 9, 50%, 72%, 1 ); --alt-color : hsla( 9, 93%, 72%, 1 ); --alt-color-dark : hsla( 9, 93%, 25%, .7); --spacing : 1rem; --spacing-small : calc(var(--spacing) / 2); --spacing-large : calc(var(--spacing) * 2); --border-radius : 2px; --transition-duration: .1s; --panel-width : 100vw; --panel-height : 70vh; --panel-min-width : auto; --panel-max-width : auto; --panel-unexpanded-translate-x : 0; --panel-unexpanded-translate-y : -100%; --panel-expanded-map-translate-x: 0; --panel-expanded-map-translate-y: 35vh; } @media all and (min-width: 600px) { :root { --panel-width : 30vw; --panel-height : 100vh; --panel-min-width : 300px; --panel-max-width : 500px; --panel-unexpanded-translate-x : -100%; --panel-unexpanded-translate-y : 0; --panel-expanded-map-translate-x: 15vw;; --panel-expanded-map-translate-y: 0; } } @font-face { font-family: Text; src: url('../font/OpenSans-regular.woff') format('woff'); font-weight: normal; font-style: normal; font-display: swap; } * { box-sizing: border-box !important; margin: 0; padding: 0; } [aria-hidden="true"] { display: none; } html { font-size: 100%; } body { color: var(--brand-color); font: 1.1rem/1.4rem Text, system-ui, sans-serif; -webkit-font-smoothing: antialiased; height: 100vh; } #body-background { position: absolute; object-fit: cover; width: 100%; height: 100%; } main { position: relative; width: 100vw; height: 100vh; overflow: hidden; } nav > form[role="search"] { position: fixed; top: var(--spacing); padding: 0 var(--spacing); width: var(--panel-width); min-width: var(--panel-min-width); max-width: var(--panel-max-width); z-index: 2; } nav > section { z-index: 1; position: absolute; width: var(--panel-width); min-width: var(--panel-min-width); max-width: var(--panel-max-width); height: var(--panel-height); padding: var(--spacing); display: flex; flex-direction: column; background: var(--alt-color); transform: translateX(var(--panel-unexpanded-translate-x)) translateY(var(--panel-unexpanded-translate-y)); transition: var(--transition-duration) transform ease-in-out; } nav[aria-expanded="true"] > section { box-shadow: 3px 0 var(--spacing) var(--alt-color-dark); transform: translateX(0) translateY(0); } article { position: absolute !important; width: 100vw; height: 100vh; } article > .mapboxgl-canvas-container { transform: translateX(0) translateY(0); transition: var(--transition-duration) transform ease-in-out; } nav[aria-expanded="true"] ~ article > .mapboxgl-canvas-container { transform: translateX(var(--panel-expanded-map-translate-x)) translateY(var(--panel-expanded-map-translate-y)); } footer { position: fixed; bottom: var(--spacing); right: var(--spacing); color: var(--alt-color); font-size: 1.1em; font-weight: bold; font-variant: all-small-caps; letter-spacing: .1em; text-shadow: var(--alt-color-lighter) 0 2px, var(--alt-color-lighter) 0 -2px, var(--alt-color-lighter) 2px 0, var(--alt-color-lighter) -2px 0; } @media all and (min-width: 600px) { footer { right: auto; left: 50%; transform: translateX(-50%); } } input { all: unset; -moz-appearance: none; -webkit-appearance: none; appearance: none; width: 100%; padding: var(--spacing-small) var(--spacing); line-height: var(--spacing-large); border: 1px var(--alt-color-light) solid; border-radius: var(--border-radius); background: var(--alt-color-lighter); } #map { font: inherit; } #map .mapboxgl-ctrl-group { border: 1px var(--alt-color-light) solid; box-shadow: unset; } #map .mapboxgl-ctrl-icon { border-color: var(--alt-color-light); } #map .mapboxgl-ctrl-attrib { font-size: .6em; background: hsla(0, 0%, 100%, .8); } ================================================ FILE: public/static/image/README.md ================================================ # `map-placeholder.svg` 1. Raster a part of the map, 2. Install [`primitive`](https://github.com/fogleman/primitive), 3. `primitive -m 1 -n 60 -i map-rasterized.png -o map-triangles.svg`, 4. Optimise it with [SVGOMG](https://jakearchibald.github.io/svgomg/), 5. Put the paths (only) into the following template: ```xml ``` ================================================ FILE: public/static/javascript/main.js ================================================ ( () => { const atlasr = Elm.Atlasr.Main.init({ node: document.body, flags: 1 }); const LAYER_ROUTE_MARKERS_PREFIX = 'markers-'; let map = null; let layer_route_markers = ''; let markers = []; atlasr.ports.mapboxgl_create_map.subscribe( ([id, options]) => { mapboxgl.accessToken = 'undefined'; map = new mapboxgl.Map({ container : id, style : options.style, center : options.center, zoom : options.zoom, hash : options.hash, pitchWithRotate : options.pitchWithRotate, attributionControl: options.attributionControl, dragRotate : options.dragRotate, dragPan : options.dragPan, keyboard : options.keyboard, doubleClickZoom : options.doubleClickZoom, touchZoomRotate : options.touchZoomRotate, trackResize : options.trackResize, renderWorldCopies : options.renderWorldCopies }); map.addControl( new mapboxgl.GeolocateControl( { positionOptions: { enableHighAccuracy: true }, trackUserLocation: true } ), 'bottom-right' ); map.addControl( new mapboxgl.NavigationControl(), 'bottom-right' ); } ); atlasr.ports.mapboxgl_fly_to.subscribe( (cameraOptions) => { map.flyTo(cameraOptions); } ); atlasr.ports.mapboxgl_add_markers.subscribe( (positions) => { for (let [longitude, latitude] of positions) { markers.push( new mapboxgl.Marker() .setLngLat([longitude, latitude]) .addTo(map) ); } } ); atlasr.ports.mapboxgl_remove_markers.subscribe( () => { for (let marker of markers) { marker.remove(); } markers = []; if (undefined !== map.getLayer(layer_route_markers)) { map.removeLayer(layer_route_markers); } } ); atlasr.ports.mapboxgl_connect_markers.subscribe( (positions) => { if (1 < positions.length) { layer_route_markers = LAYER_ROUTE_MARKERS_PREFIX + guid(); map.addLayer({ 'id': layer_route_markers, 'type': 'line', 'source': { 'type': 'geojson', 'data': { 'type': 'Feature', 'properties': {}, 'geometry': { 'type': 'LineString', 'coordinates': positions } } }, 'layout': { 'line-join': 'round', 'line-cap': 'round' }, 'paint': { 'line-color': '#00b3fd', 'line-width': 8 } }); } } ); function guid() { const s4 = () => { return Math.floor((1 + Math.random()) * 0x10000) .toString(16) .substring(1); }; return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); } } )(); ================================================ FILE: source/api/geocode/indexer/Cargo.toml ================================================ [package] name = "atlasr-api-geocode-indexer" version = "0.1.0" authors = ["Ivan Enderlin "] [dependencies] tantivy = "^0.6" clap = "^2.31.2" failure = "^0.1.1" csv = "^1.0.0" serde = "^1.0.66" serde_derive = "^1.0.66" ================================================ FILE: source/api/geocode/indexer/src/main.rs ================================================ extern crate failure; extern crate clap; extern crate csv; #[macro_use] extern crate serde_derive; extern crate tantivy; use failure::{ Error, ResultExt }; use clap::{App, Arg}; use tantivy::{ Index, IndexWriter, directory, schema::* }; use std::fs::File; use std::io::{ self, BufReader, Write }; /// [Definition of these fields](http://osmnames.readthedocs.io/en/latest/introduction.html#output-format). #[derive(Debug, Deserialize)] struct Record { name: String, alternative_names: Option, osm_type: String, osm_id: u64, class: String, #[serde(rename = "type")] object_type: String, #[serde(rename = "lon")] longitude: Option, #[serde(rename = "lat")] latitude: Option, place_rank: Option, importance: Option, street: Option, city: Option, county: Option, state: Option, country: Option, country_code: Option, display_name: String, #[serde(rename = "west")] bbox_west: Option, #[serde(rename = "south")] bbox_south: Option, #[serde(rename = "east")] bbox_east: Option, #[serde(rename = "north")] bbox_north: Option, wikidata: Option, wikipedia: Option, housenumbers: Option } impl Record { fn indexable(&self) -> bool { if self.longitude.is_none() || self.latitude.is_none() { return false; } match (self.class.as_str(), self.object_type.as_str()) { ("boundary", _) | ("highway", "residential") => { return true; }, (_, _) => { } } false } } fn create_schema() -> Schema { let mut schema_builder = SchemaBuilder::default(); schema_builder.add_text_field("display_name", TEXT | STORED); schema_builder.add_text_field("class", STORED); schema_builder.add_text_field("type", STORED); schema_builder.add_text_field("longitude", STORED); schema_builder.add_text_field("latitude", STORED); schema_builder.build() } fn create_index(index_directory: directory::MmapDirectory, schema: &Schema) -> Index { Index::create(index_directory, schema.clone()).unwrap() } fn index_record(index_writer: &mut IndexWriter, schema: &Schema, record: Record) -> tantivy::Result<()> { if ! record.indexable() { return Ok(()); } let field_display_name = schema.get_field("display_name").unwrap(); let field_class = schema.get_field("class").unwrap(); let field_type = schema.get_field("type").unwrap(); let field_longitude = schema.get_field("longitude").unwrap(); let field_latitude = schema.get_field("latitude").unwrap(); let mut document = Document::default(); document.add_text(field_display_name, &record.display_name); document.add_text(field_class, &record.class); document.add_text(field_type, &record.object_type); document.add_text(field_longitude, &record.longitude.unwrap().to_string()); document.add_text(field_latitude, &record.latitude.unwrap().to_string()); index_writer.add_document(document); Ok(()) } fn main() -> Result<(), Error> { let matches = App::new("atlasr-api-geocode-indexer") .version(env!("CARGO_PKG_VERSION")) .about("Atlasr: Indexer for the geocode API.") .author("Ivan Enderlin") .arg( Arg::with_name("source-file") .help("File containing the data to index, must be a `.tsv` file.") .short("s") .long("source-file") .value_name("SOURCE_FILE") .takes_value(true) .required(true) ) .arg( Arg::with_name("index-directory") .help("Directory that will contain the index.") .short("i") .long("index-directory") .value_name("INDEX_DIRECTORY") .takes_value(true) .required(true) ) .get_matches(); let source_file_name = matches.value_of("source-file").unwrap(); let source_file = File::open(source_file_name).context("Cannot open the given file.")?; let source_buffer = BufReader::new(source_file); let mut source_reader = csv::ReaderBuilder::new() .delimiter(b'\t') .double_quote(false) .flexible(true) .from_reader(source_buffer); let index_directory = matches.value_of("index-directory").unwrap(); let schema = create_schema(); let index = create_index(directory::MmapDirectory::open(index_directory)?, &schema); let mut index_writer = index.writer(50_000_000)?; let stdout = io::stdout(); let mut handle = stdout.lock(); for record in source_reader.deserialize() { let record: Record = record?; handle.write(b"~~> ").unwrap(); handle.write(record.name.as_bytes()).unwrap(); handle.write(b"\n").unwrap(); index_record(&mut index_writer, &schema, record)?; } index_writer.commit()?; Ok(()) } ================================================ FILE: source/api/geocode/searcher/Cargo.toml ================================================ [package] name = "atlasr-api-geocode-searcher" version = "0.1.0" authors = ["Ivan Enderlin "] [dependencies] tantivy = "^0.6" actix-web = "^0.6.10" ================================================ FILE: source/api/geocode/searcher/src/main.rs ================================================ extern crate actix_web; extern crate tantivy; use actix_web::{ App, HttpRequest, HttpResponse, http::Method, server }; use tantivy::{ Index, collector::TopCollector, directory, query::QueryParser }; use std::cmp::min; const GEOCODE_API_ADDRESS: &'static str = env!("GEOCODE_API_ADDRESS"); struct SearchState { index: Index, query_parser: QueryParser } fn serve_search(request: HttpRequest) -> HttpResponse { let term: &str; match request.query().get("term") { Some(value) => term = value, None => return HttpResponse::BadRequest().reason("Query `term` is missing").finish() } let limit: u8 = match request.query().get("limit") { Some(value) => min(value.parse().unwrap_or(3), 10), None => 3 }; let index = &request.state().index; let query_parser = &request.state().query_parser; let schema = index.schema(); let searcher = index.searcher(); let query = match query_parser.parse_query(term) { Ok(value) => { value }, Err(_) => { return HttpResponse::InternalServerError().finish(); } }; let mut top_collector = TopCollector::with_limit(limit as usize); match searcher.search(&*query, &mut top_collector) { Err(_) => { return HttpResponse::InternalServerError().finish(); } _ => { } } let mut response = String::from("["); for document in top_collector.docs() { let retrieved_document = match searcher.doc(&document) { Ok(value) => { value }, Err(_) => { return HttpResponse::InternalServerError().finish(); } }; response.push_str(&schema.to_json(&retrieved_document)); response.push_str(","); } if response.len() > 1 { response.pop(); } response.push_str("]"); HttpResponse::Ok().content_type("application/json").body(response) } fn main() { server ::new( || { App ::with_state( { let index = Index::open(directory::MmapDirectory::open("../index").unwrap()).unwrap(); index.load_searchers().unwrap(); let schema = index.schema(); let query_parser = QueryParser::for_index( &index, vec![ schema.get_field("display_name").unwrap() ] ); SearchState { index: index, query_parser: query_parser } } ) .resource( "/search", // ?term=…&limit=… |resource| { resource.method(Method::GET).f(serve_search) } ) } ) .bind(GEOCODE_API_ADDRESS) .expect(&format!("Cannot bind the server to {}.", GEOCODE_API_ADDRESS)) .shutdown_timeout(30) .run(); } ================================================ FILE: source/api/tile/Cargo.toml ================================================ [package] name = "atlasr-api-tile" version = "0.1.0" authors = ["Ivan Enderlin "] [dependencies] warp = "^0.1.9" diesel = { version = "^1.3.3", features = ["sqlite"] } r2d2 = "^0.8.3" r2d2-diesel = "^1.0.0" serde_json = "^1.0.0" ================================================ FILE: source/api/tile/database/.keep ================================================ ================================================ FILE: source/api/tile/diesel.toml ================================================ [print_schema] file = "src/schema.rs" ================================================ FILE: source/api/tile/src/main.rs ================================================ #[macro_use] extern crate warp; #[macro_use] extern crate diesel; extern crate r2d2; extern crate r2d2_diesel; extern crate serde_json as json; use warp::Filter; use warp::http; use diesel::prelude::*; use r2d2_diesel::ConnectionManager; use std::net::SocketAddr; use std::str::FromStr; mod schema; mod models; const SERVER_ADDRESS: &'static str = env!("SERVER_ADDRESS"); const TILE_API_ADDRESS: &'static str = env!("TILE_API_ADDRESS"); fn not_found() -> http::Result>> { http::Response::builder() .status(http::StatusCode::NOT_FOUND) .header("content-type", "text/plain; charset=utf-8") .body(vec![]) } fn error() -> http::Result> { http::Response::builder() .status(http::StatusCode::NOT_FOUND) .header("content-type", "text/plain; charset=utf-8") .body("error".to_string()) } fn main() { let manager = ConnectionManager::::new("database/europe_switzerland.mbtiles"); let pool = r2d2::Pool::builder() .max_size(15) .build(manager) .expect("Failed to create pool."); let pool1 = pool.clone(); let pool2 = pool.clone(); // `GET ////` let tiles = path!(u8 / u16 / u16) .map( move |z, x, y| { match pool1.get() { Ok(connection) => { use schema::tiles::dsl::*; use models::Tiles; let x = x as u32; let y = y as u32; let z = z as u32; let y = 2u32.pow(z) - 1 - y; let tile = tiles .filter(zoom_level.eq(z as i32)) .filter(tile_column.eq(x as i32)) .filter(tile_row.eq(y as i32)) .first::(&*connection); match tile { Ok(tile) => { http::Response::builder() .header("content-type", "application/x-protobuf") .header("content-encoding", "gzip") .body(tile.tile_data) }, Err(_) => { not_found() } } }, Err(_) => { not_found() } } } ); // `GET /metadata.json` let metadata = path!("metadata.json") .map( move || { match pool2.get() { Ok(connection) => { use schema::metadata::dsl::*; use models::Metadata; match metadata.load::(&*connection) { Ok(all_metadata) => { let mut output = json::map::Map::with_capacity(all_metadata.len()); for metadatum in all_metadata { let v = metadatum.value; let map_value = match metadatum.name.as_ref() { "bounds" | "center" => { Ok( json::Value::Array( v .splitn(4, ',') .map( |p| { json::Value::Number( json::Number::from_str(p).unwrap_or_else(|_| json::Number::from_f64(0.0).unwrap()) ) } ) .collect() ) ) }, "json" => { if let Ok(json::Value::Object(meta_metadata)) = json::from_str(&v) { output.extend(meta_metadata); } Err(()) }, _ => Ok(json::Value::String(v)) }; if let Ok(map_value) = map_value { output.insert(metadatum.name, map_value); } } if !output.contains_key("profile") { output.insert("profile".to_string(), json::Value::String("mercator".to_string())); } if !output.contains_key("scale") { output.insert("scale".to_string(), json::Value::Number(json::Number::from_f64(1.0).unwrap())); } if !output.contains_key("tilejson") { output.insert("tilejson".to_string(), json::Value::String("2.0.0".to_string())); } if !output.contains_key("tiles") { output.insert( "tiles".to_string(), json::Value::Array(vec![ json::Value::String( format!("http://{}/api/tile/{{z}}/{{x}}/{{y}}", SERVER_ADDRESS) ) ]) ); } http::Response::builder() .header("content-type", "application/json") .body(json::to_string(&output).unwrap()) }, Err(_) => error() } }, Err(_) => error() } } ); let routes = tiles.or(metadata); warp::serve(routes) .run(TILE_API_ADDRESS.parse::().expect(&format!("Cannot bind the server to {}", TILE_API_ADDRESS))); } ================================================ FILE: source/api/tile/src/models.rs ================================================ #[derive(Debug, Queryable)] pub struct Tiles { pub zoom_level: i32, pub tile_column: i32, pub tile_row: i32, pub tile_data: Vec, } #[derive(Debug, Queryable)] pub struct Metadata { pub name: String, pub value: String } ================================================ FILE: source/api/tile/src/schema.rs ================================================ table! { tiles (zoom_level, tile_column, tile_row) { zoom_level -> Integer, tile_column -> Integer, tile_row -> Integer, tile_data -> Binary, } } table! { metadata (name, value) { name -> Text, value -> Text, } } ================================================ FILE: source/client/elm.json ================================================ { "type": "application", "source-directories": [ "src" ], "elm-version": "0.19.0", "dependencies": { "direct": { "elm/browser": "1.0.1", "elm/core": "1.0.0", "elm/html": "1.0.0", "elm/http": "1.0.0", "elm/json": "1.0.0", "elm/url": "1.0.0", "fapian/elm-html-aria": "1.4.0" }, "indirect": { "elm/time": "1.0.0", "elm/virtual-dom": "1.0.2" } }, "test-dependencies": { "direct": {}, "indirect": {} } } ================================================ FILE: source/client/src/Atlasr/Geocode.elm ================================================ module Atlasr.Geocode exposing (Geocode, toGeocodes) import Atlasr.Position exposing (NamedPosition, defaultNamedPosition) import Http import Json.Decode import Task type alias Geocode = { label : String , longitude : String , latitude : String } {-| Geocode a list of position names. -} toGeocodes : (Result Http.Error (List (Maybe Geocode)) -> msg) -> List NamedPosition -> Cmd msg toGeocodes outputType positionsToGeocode = let ( defaultName, defaultPosition ) = defaultNamedPosition tasks = List.map (\( positionName, position ) -> if positionName /= defaultName then if position == defaultPosition then positionToGeocodeRequest positionName |> Http.toTask |> Task.map (\geocode -> Just geocode) else let ( longitude, latitude ) = position geocode = { label = positionName , longitude = String.fromFloat longitude , latitude = String.fromFloat latitude } in Task.succeed (Just geocode) else Task.succeed Nothing ) positionsToGeocode in Task.attempt outputType <| Task.sequence tasks {-| Create an HTTP request to geocode a position. -} positionToGeocodeRequest : String -> Http.Request Geocode positionToGeocodeRequest positionName = let url = "/api/geocode?term=" ++ positionName ++ "&limit=1" in Http.get url decodeGeocode {-| Decoder for the geocode payload from the HTTP service. -} decodeGeocode : Json.Decode.Decoder Geocode decodeGeocode = Json.Decode.at [ "0" ] (Json.Decode.map3 Geocode (Json.Decode.at [ "display_name", "0" ] Json.Decode.string) (Json.Decode.at [ "longitude", "0" ] Json.Decode.string) (Json.Decode.at [ "latitude", "0" ] Json.Decode.string) ) ================================================ FILE: source/client/src/Atlasr/Map.elm ================================================ module Atlasr.Map exposing (addMarkers, connectMarkers, create, flyTo, removeMarkers) import Atlasr.MapboxGL.Options as Options import Atlasr.MapboxGL.Ports exposing (..) import Atlasr.Position exposing (Position) {-| Create a map with an ID and some options. -} create : String -> Options.Map -> Cmd msg create id options = mapboxgl_create_map ( id, options ) {-| Move the camera to a specific position. -} flyTo : Position -> Cmd msg flyTo position = mapboxgl_fly_to { center = position } {-| Add markers to certain positions, and connect them. -} addMarkers : List Position -> Cmd msg addMarkers positions = mapboxgl_add_markers positions {-| Remove all markers. -} removeMarkers : Cmd msg removeMarkers = mapboxgl_remove_markers () {-| Draw lines between points/positions. -} connectMarkers : List Position -> Cmd msg connectMarkers positions = mapboxgl_connect_markers positions ================================================ FILE: source/client/src/Atlasr/MapboxGL/Options.elm ================================================ module Atlasr.MapboxGL.Options exposing (Camera, Map, default) import Atlasr.Position exposing (Position) type alias Map = { style : String , center : Position , zoom : Int , hash : Bool , pitchWithRotate : Bool , attributionControl : Bool , dragRotate : Bool , dragPan : Bool , keyboard : Bool , doubleClickZoom : Bool , touchZoomRotate : Bool , trackResize : Bool , renderWorldCopies : Bool } default : Map default = { style = "/static/map-style/style.json" , center = ( 2.294504285127, 48.858262790681 ) , zoom = 15 , hash = True , pitchWithRotate = True , attributionControl = True , dragRotate = True , dragPan = True , keyboard = True , doubleClickZoom = True , touchZoomRotate = True , trackResize = True , renderWorldCopies = True } type alias Camera = { center : Position } ================================================ FILE: source/client/src/Atlasr/MapboxGL/Ports.elm ================================================ port module Atlasr.MapboxGL.Ports exposing (mapboxgl_add_markers, mapboxgl_connect_markers, mapboxgl_create_map, mapboxgl_fly_to, mapboxgl_remove_markers) import Atlasr.MapboxGL.Options as Options import Atlasr.Position exposing (Position) port mapboxgl_create_map : ( String, Options.Map ) -> Cmd msg port mapboxgl_fly_to : Options.Camera -> Cmd msg port mapboxgl_add_markers : List Position -> Cmd msg port mapboxgl_remove_markers : () -> Cmd msg port mapboxgl_connect_markers : List Position -> Cmd msg ================================================ FILE: source/client/src/Atlasr/Position.elm ================================================ module Atlasr.Position exposing (Latitude, Longitude, NamedPosition, Position, defaultName, defaultNamedPosition, defaultPosition, extractLatitude, extractLongitude) type alias Longitude = Float type alias Latitude = Float type alias Position = ( Longitude, Latitude ) {-| Allocate a default position. -} defaultPosition : Position defaultPosition = ( 0.0, 0.0 ) {-| Extract the longitude of a position. -} extractLongitude : Position -> Float extractLongitude ( longitude, _ ) = longitude {-| Extract the latitude of a position. -} extractLatitude : Position -> Float extractLatitude ( _, latitude ) = latitude type alias NamedPosition = ( String, Position ) {-| Allocate a default position name. -} defaultName : String defaultName = "" {-| Allocate a default named position. -} defaultNamedPosition : NamedPosition defaultNamedPosition = ( defaultName, defaultPosition ) ================================================ FILE: source/client/src/Atlasr/Route.elm ================================================ module Atlasr.Route exposing (Route, toRoute) import Atlasr.Position exposing (Position) import Http import Json.Decode import Task import Url.Builder exposing (absolute, string) type alias Route = { points : List Position } type alias RawRoute = { points : List Point } type alias Point = { longitude : Float , latitude : Float } emptyRoute : Route emptyRoute = { points = [] } {-| Get a route from a list of position. -} toRoute : (Result Http.Error Route -> msg) -> List Position -> Cmd msg toRoute outputType positions = let task = if List.length positions <= 1 then Task.succeed emptyRoute else positionsToRouteRequest positions |> Http.toTask |> Task.map (\route -> Route (List.map (\point -> ( point.longitude, point.latitude ) ) route.points ) ) in Task.attempt outputType task {-| Create an HTTP request to get the route between positions. -} positionsToRouteRequest : List Position -> Http.Request RawRoute positionsToRouteRequest positions = let url = absolute [ "api/route" ] ([ string "points_encoded" "false" , string "vehicle" "car" ] ++ List.map (\position -> let ( longitude, latitude ) = position in string "point" (String.fromFloat latitude ++ "," ++ String.fromFloat longitude) ) positions ) in Http.get url decodeRoute {-| Decoder for the route payload from the HTTP service. -} decodeRoute : Json.Decode.Decoder RawRoute decodeRoute = Json.Decode.at [ "paths", "0" ] (Json.Decode.map RawRoute (Json.Decode.at [ "points", "coordinates" ] (Json.Decode.list (Json.Decode.map2 Point (Json.Decode.field "0" Json.Decode.float) (Json.Decode.field "1" Json.Decode.float) ) ) ) ) ================================================ FILE: source/client/src/Main.elm ================================================ module Atlasr.Main exposing (Model, Msg(..), init, main, subscriptions, update, view) import Array exposing (Array) import Atlasr.Geocode as Geocode import Atlasr.Map as Map import Atlasr.MapboxGL.Options as MapOptions import Atlasr.Position as Position exposing (NamedPosition, Position) import Atlasr.Route as Route import Browser import Html exposing (..) import Html.Attributes exposing (..) import Html.Attributes.Aria exposing (..) import Html.Events exposing (..) import Http import Process import Task as CoreTask main = Browser.document { init = init , view = view , update = update , subscriptions = subscriptions } type alias Model = { positions : Array NamedPosition } init : Int -> ( Model, Cmd Msg ) init x = ( { positions = Array.repeat 2 Position.defaultNamedPosition } , Map.create "map" MapOptions.default ) view : Model -> Browser.Document Msg view model = let hasAtLeastOnePositionName = Array.toList model.positions |> List.any (\( positionName, _ ) -> not (String.isEmpty positionName)) expandedNav = if hasAtLeastOnePositionName then "true" else "false" in { title = "Atlasr" , body = [ main_ [] [ nav [ ariaExpanded expandedNav ] [ Html.form [ role "search", onSubmit Search ] [ input [ type_ "text" , onInput (NewPositionName 0) , ariaLabel "Browse the world" , ariaRequired True , placeholder "Browse the world" , value (Array.get 0 model.positions |> Maybe.map (\( positionName, _ ) -> positionName) |> Maybe.withDefault Position.defaultName ) ] [] , input [ type_ "text" , onInput (NewPositionName 1) , ariaLabel "Search for a direction" , ariaRequired False , placeholder "Search for a direction" , value (Array.get 1 model.positions |> Maybe.map (\( positionName, _ ) -> positionName) |> Maybe.withDefault Position.defaultName ) ] [] , input [ type_ "submit" , value "Search" ] [] ] , section [] [] ] , article [ id "map" ] [] , footer [] [ text "Atlasr" ] ] ] } type Msg = NewPositionName Int String | GeoencodePositionNames (List NamedPosition) | NewPositionGeocodes (Result Http.Error (List (Maybe Geocode.Geocode))) | GetRoute (List Position) | NewPositionRoute (Result Http.Error Route.Route) | Search | AddMarkers (List Position) | RemoveMarkers | ConnectMarkers (List Position) | FlyTo Position | Chain (List Msg) update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of NewPositionName index positionName -> let positions = Array.set index ( positionName, Position.defaultPosition ) model.positions in ( { model | positions = positions }, Cmd.none ) GeoencodePositionNames positionsToGeocode -> ( model, Geocode.toGeocodes NewPositionGeocodes positionsToGeocode ) NewPositionGeocodes (Ok geocodes) -> let ( defaultLongitude, defaultLatitude ) = Position.defaultPosition namedPositions = List.map (\geocode_item -> Maybe.map (\geocode -> let positionName = geocode.label position = ( Maybe.withDefault defaultLongitude (String.toFloat geocode.longitude) , Maybe.withDefault defaultLatitude (String.toFloat geocode.latitude) ) in ( positionName, position ) ) geocode_item ) geocodes positions = List.filterMap (\namedPosition -> Maybe.map (\( positionName, position ) -> Just position) namedPosition |> Maybe.withDefault Nothing ) namedPositions in update (Chain [ AddMarkers positions, GetRoute positions ]) { model | positions = List.map (\namedPosition -> Maybe.withDefault Position.defaultNamedPosition namedPosition ) namedPositions |> Array.fromList } NewPositionGeocodes (Err _) -> ( model, Cmd.batch [] ) GetRoute positions -> ( model, Route.toRoute NewPositionRoute positions ) NewPositionRoute (Ok route) -> update (ConnectMarkers route.points) model NewPositionRoute (Err _) -> ( model, Cmd.batch [] ) Search -> update (Chain [ RemoveMarkers, Array.toList model.positions |> GeoencodePositionNames ]) model AddMarkers positions -> ( model, Map.addMarkers positions ) RemoveMarkers -> ( model, Map.removeMarkers ) ConnectMarkers positions -> ( model, Map.connectMarkers positions ) FlyTo position -> ( model, Map.flyTo position ) Chain messages -> let chain message ( model1, commands ) = let ( nextModel, nextCommands ) = update message model in ( nextModel, Cmd.batch [ commands, nextCommands ] ) in List.foldl chain ( model, Cmd.batch [] ) messages subscriptions : Model -> Sub Msg subscriptions model = Sub.none ================================================ FILE: source/client/src/index.html ================================================ Atlasr ================================================ FILE: source/client/tests/elm-package.json ================================================ { "version": "0.0.0", "summary": "Atlasr's test suites", "repository": "https://github.com/atlasr-org/atlasr.git", "license": "BSD3", "source-directories": [ "../src/", "./unit/", "./VerifyExamples/" ], "exposed-modules": [], "dependencies": { "eeue56/elm-html-test": "5.2.0 <= v < 6.0.0", "elm-community/elm-test": "4.0.0 <= v < 5.0.0", "elm-lang/core": "5.1.1 <= v < 6.0.0", "elm-lang/html": "2.0.0 <= v < 3.0.0", "elm-lang/http": "1.0.0 <= v < 2.0.0", "fapian/elm-html-aria": "1.2.2 <= v < 2.0.0" }, "elm-version": "0.18.0 <= v < 0.19.0" } ================================================ FILE: source/client/tests/unit/Atlasr/Test/Position.elm ================================================ module Atlasr.Test.Position exposing (..) import Atlasr.Position as Position import Test exposing (..) import Expect position : Test position = describe "Test suite for the `Position` module." [ test "Default position" <| \() -> Expect.equal ( 0.0, 0.0 ) Position.defaultPosition , test "Extract longitude" <| \() -> Expect.equal 1.2 (Position.extractLongitude ( 1.2, 3.4 )) , test "Extract latitude" <| \() -> Expect.equal 3.4 (Position.extractLatitude ( 1.2, 3.4 )) , test "Default name" <| \() -> Expect.equal "" Position.defaultName , test "Default named position" <| \() -> Expect.equal ( "", ( 0.0, 0.0 ) ) Position.defaultNamedPosition ] ================================================ FILE: source/server/Cargo.toml ================================================ [package] name = "atlasr-server" version = "0.0.1" authors = ["Ivan Enderlin "] [dependencies] actix-web = "^0.6.10" futures = "^0.1.21" ================================================ FILE: source/server/src/main.rs ================================================ extern crate actix_web; extern crate futures; use actix_web::{ App, HttpRequest, HttpResponse, Result, dev::HttpResponseBuilder, fs::NamedFile, http::Method, client, server, }; use futures::Future; use std::path::PathBuf; macro_rules! ROOT_DIRECTORY { () => { "../../public/" }; } const STATIC_DIRECTORY: &'static str = concat!(ROOT_DIRECTORY!(), "static/"); const SERVER_ADDRESS: &'static str = env!("SERVER_ADDRESS"); const ROUTE_API_ADDRESS: &'static str = env!("ROUTE_API_ADDRESS"); const GEOCODE_API_ADDRESS: &'static str = env!("GEOCODE_API_ADDRESS"); const TILE_API_ADDRESS: &'static str = env!("TILE_API_ADDRESS"); fn serve_static_files(request: HttpRequest) -> Result { let mut path: PathBuf = PathBuf::from(STATIC_DIRECTORY); let tail: String = request.match_info().query("tail")?; path.push(tail); Ok(NamedFile::open(path)?) } fn serve_api_geocode(request: HttpRequest) -> impl Future { client ::get(format!("http://{}/search?{}", GEOCODE_API_ADDRESS, request.query_string())) .finish() .unwrap() .send() .map( |client_response| { HttpResponseBuilder ::from(&client_response) .chunked() .streaming(client_response) } ) } fn serve_api_route(request: HttpRequest) -> impl Future { client ::get(format!("http://{}/route?{}", ROUTE_API_ADDRESS, request.query_string())) .finish() .unwrap() .send() .map( |client_response| { HttpResponseBuilder ::from(&client_response) .chunked() .streaming(client_response) } ) } fn serve_api_tile(request: HttpRequest) -> impl Future { let tail = request.match_info().get("tail").unwrap_or(""); client ::get(format!("http://{}/{}", TILE_API_ADDRESS, tail)) .finish() .unwrap() .send() .map( |client_response| { HttpResponseBuilder ::from(&client_response) .chunked() .streaming(client_response) } ) } fn serve_index(_request: HttpRequest) -> Result { let mut path: PathBuf = PathBuf::from(STATIC_DIRECTORY); path.push("index.html"); Ok(NamedFile::open(path)?) } fn main() { server ::new( || { vec![ App::new() .prefix("/static") .resource( r"/{tail:.*}", |resource| { resource.method(Method::GET).f(serve_static_files) } ), App::new() .prefix("/api") .resource( "/geocode", |resource| { resource.method(Method::GET).a(serve_api_geocode) } ) .resource( "/route", |resource| { resource.method(Method::GET).a(serve_api_route) } ) .resource( "/tile/{tail:.*}", |resource| { resource.method(Method::GET).a(serve_api_tile) } ), App::new() .prefix("/") .resource( "/index.html", |resource| { resource.method(Method::GET).f(serve_index) } ) .resource( "/", |resource| { resource.method(Method::GET).f(serve_index) } ) ] } ) .bind(SERVER_ADDRESS) .expect(&format!("Cannot bind the server to {}.", SERVER_ADDRESS)) .shutdown_timeout(30) .run(); }