├── .githooks └── pre-commit ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── assets └── sequentially-generate-planet-mbtiles-example.gif ├── build └── osmium │ └── Dockerfile ├── cmd └── sequentially-generate-planet-mbtiles │ ├── config.go │ ├── config_test.go │ ├── containers.go │ ├── dir.go │ ├── dir_test.go │ ├── downloader.go │ ├── filehandler.go │ ├── flags.go │ ├── git.go │ ├── loggers.go │ ├── loggers_test.go │ ├── sequentiallygenerateplanetmbtiles.go │ ├── sequentiallygenerateplanetmbtiles_test.go │ ├── unzip.go │ └── version.go ├── configs ├── config.json ├── styles │ ├── sgpm-basic.json │ └── sgpm-bright.json └── test │ ├── TEST_INVALID.json │ └── TEST_VALID.json ├── go.mod ├── go.sum ├── internal ├── describeloggers │ ├── errlog.go │ ├── errlog_test.go │ ├── proglog.go │ ├── proglog_test.go │ ├── replog.go │ └── replog_test.go ├── docker │ └── docker.go ├── extract │ ├── extract.go │ └── tree.go ├── filesystem │ └── filesystem.go ├── git │ └── git.go ├── mbtiles │ └── mbtiles.go ├── planet │ └── planet.go ├── system │ └── system.go └── validator │ └── validator.go ├── main.go ├── main_test.go └── pkg └── execute └── execute.go /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/sh 2 | 3 | echo "RUNNING PRE-COMMIT HOOK | .githooks/pre-commit" 4 | 5 | echo "Running go fmt" 6 | go fmt ./... -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test.json 2 | data 3 | main 4 | bin 5 | .vscode -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | lambdajack@protonmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## All contributions are welcome. 2 | 3 | #### We could particularly use help with: 4 | 5 | 1. macOS testing / improvements. 6 | 7 | ### Dev: 8 | 9 | - Build using 'make' and remember to set the $(VERSION) variable appropriately. 10 | - Testing may have to be done on built binaries if your golang and docker permissions clash. 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jack Bizzell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION := v3.1.0 2 | LDF := "-X github.com/lambdajack/sequentially-generate-planet-mbtiles/cmd/sequentially-generate-planet-mbtiles.sgpmVersion=$(VERSION)" 3 | 4 | all: clean darwin linux windows 5 | 6 | darwin: 7 | GOOS=darwin GOARCH=amd64 go build -o bin/sequentially-generate-planet-mbtiles--darwin-amd64-$(VERSION) -ldflags $(LDF) 8 | 9 | linux: 10 | GOOS=linux GOARCH=amd64 go build -o bin/sequentially-generate-planet-mbtiles--unix-amd64-$(VERSION) -ldflags $(LDF) 11 | GOOS=linux GOARCH=arm go build -o bin/sequentially-generate-planet-mbtiles--unix-arm64-$(VERSION) -ldflags $(LDF) 12 | 13 | windows: 14 | GOOS=windows GOARCH=amd64 go build -o bin/sequentially-generate-planet-mbtiles--win-amd64-$(VERSION).exe -ldflags $(LDF) 15 | 16 | clean: 17 | rm -rf bin/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sequentially Generate Planet Mbtiles 2 | 3 | ### _Sequentially generate and merge an entire planet.mbtiles vector tileset on low memory/power hardware for free._ 4 | 5 | ![](https://img.shields.io/badge/go%20report-A+-brightgreen.svg?style=flat) 6 | 7 | ![](assets/sequentially-generate-planet-mbtiles-example.gif) 8 | 9 | ## TL;DR give me planet vector tiles! (usage) 10 | 11 | 1. Have [Docker](https://docs.docker.com/get-docker/) installed. 12 | 2. Download the latest binary for your operating system from the [release page](https://github.com/lambdajack/sequentially-generate-planet-mbtiles/releases) 13 | 3. ```bash 14 | sudo ./sequentially-generate-planet-mbtiles--unix-amd64-v3.1.0 15 | ``` 16 | 4. Rejoice! See acknowledgements below for people to thank. 17 | 18 | ## Configuration 19 | 20 | ### config.json 21 | 22 | ```bash 23 | sudo ./sequentially-generate-planet-mbtiles--unix-amd64-v3.1.0 -c /path/to/config.json 24 | ``` 25 | 26 | ```json 27 | // config.json 28 | // Note if a config.json is provided, the program will ignore all other flags set. 29 | { 30 | "pbfFile": "", 31 | "workingDir": "", 32 | "outDir": "", 33 | "excludeOcean": false, 34 | "excludeLanduse": false, 35 | "TilemakerConfig": "", 36 | "TilemakerProcess": "", 37 | "maxRamMb": 0, 38 | "outAsDir": false, 39 | "skipSlicing": false, 40 | "mergeOnly": false, 41 | "skipDownload": false 42 | } 43 | ``` 44 | 45 | **_pbfFile_** - By default, the program downloads the latest planet data directrly from [OpenStreetMaps](https://planet.openstreetmap.org/). However, if you already have your own pbf file that you would like to use (for example, you may have a historical data set, or subset of the planet), you can provide the path to it here. 46 | 47 | **_workingDir_** - This is where files will be downloaded to and files generated as a result of processing osm data will be stored. Temporary files will be stored here. Please ensure your designated working directory has at least 300 GB of space available. If none is provided, a 'data' folder will be created in the current working directory. 48 | 49 | **_outDir_** - This is where the final planet.mbtiles file (or folder tree if outAsDir is true) will be placed. 50 | 51 | **_excludeOcean_** - By default the program will download the appropriate ocean/sea data and include it in the final output planet.mbtiles. If you do not wish to include the sea tiles (for example to save a little space), then you can set this option to true. If true, the ocean data will not be downloaded either. This can significantly increase the overall speed of generation as there are a lot of ocean tiles and writing them all often manifests as a filesystem io bottleneck. A planet map without ocean tiles however does look strange and empty (it can be hard to identify continent borders etc) - if your target end user is, for example, a customer who expects a pretty map, we would recommend they be included. 52 | 53 | **_excludeLanduse_** - By default the program will download the appropriate landuse/landcover data and include it in the final output planet.mbtiles. If you do not wish to include the landcover overlay (for example to save a little space), then you can set this option to true. If true, the landuse data will not be downloaded either. 54 | 55 | **_TilemakerConfig_** - **_[note capitalisation]_** The path to the config file that will be passed to [Tilemaker](https://github.com/systemed/tilemaker). See the default used [here](https://github.com/lambdajack/tilemaker/blob/b90347b2a4fd475470b9870b8e44e2829d8e4d6d/resources/config-openmaptiles.json). This will affect things like tags etc which will affect your front end styles when serving. It is reccomended to leave as default (blank) unless you have a specific reason to make changes (for example, you require a language other than english to be the primary language for the generated maps, or wish to change the zoom level (14 by default)). 56 | 57 | **_TilemakerProcess_** - **_[note capitalisation]_** The path to the process file that will be passed to [Tilemaker](https://github.com/systemed/tilemaker). See the default used [here](https://github.com/lambdajack/tilemaker/blob/b90347b2a4fd475470b9870b8e44e2829d8e4d6d/resources/process-openmaptiles.lua). Leaving blank will use the default. You can also use a special value to select one of the provided process files to match a given style. The special values are "**tileserver-gl-basic**", "**sgpm-bright**". Copies of the target styles can be viewed [here](configs/styles/). Feel free to copy one of the target styles to your front end project if necessary. 58 | 59 | **_maxRamMb_** - Provide the maximum amount of RAM in MB that the process should use. If a linux os is detected, the total system RAM will be detected from /proc/meminfo and a default will be set to a reasonably safe level, maximising the available resources. This assumes that only a minimal amount of system RAM is currently being used (such as an idle desktop environment (<2G)). If you are having memory problems, consider manually setting this flag to a reduced value. Note this is not guaranteed and some margin should be allowed for. The default is set to 4096 if the amount of available ram cannot be detected. This setting is not a hard cap and the program will bleed over if necessary and possible. 60 | 61 | **_outAsDir_** - The final output will be a directory of tiles rather than a single mbtiles file. This will generate hundreds of thousands of files in a predetermined directory structure. More information can ba found about this format and why you might use it over a single mbtiles file can be found [here](https://documentation.maptiler.com/hc/en-us/articles/360020886878-Folder-vs-MBTiles-vs-GeoPackage) 62 | 63 | **_skipSlicing_** - Skips the intermediate data processing/slicing and looks for existing files to convert into mbtiles in [workingDir]/pbf/slices. This is useful if you wish to experiment with different Tilemaker configs/process (for example if you wish to change the zoom levels or style tagging of the final output). Once the existing files have been converted to mbtiles, they will be merged either to a single file, or to a directory, respecting the -od flag. 64 | 65 | **_mergeOnly_** - Skips the entire generation process and instead looks for existing mbtiles in [workingDir]/mbtiles and merges them into a single planet.mbtiles file in the [outDir]. This is useful if you already have a tilesets you wish to merge. 66 | 67 | **_skipDownload_** - Skips planet downloading - must be set with skip slicing or merge only. Note, this should not be used if you have your own pbf file you wish to slice. In that case, just supply a path to the file in the pbfFile option and the download will be skipped anyway. 68 | 69 | ### Flags 70 | 71 | **_All options in the config.json can be set with flags. Options unique to flags are:_** 72 | 73 | **_-h, --help_** - Print the help message listing all available flags. 74 | 75 | **_-v, --version_** - Print version information. 76 | 77 | **_-s, --stage_** - Initialise required containers, Dirs and logs based on the supplied config file and then exit. Can be useful to check you are running with correct permissions etc (for example Docker and filesystem), but without running the hard work. It will take some time to build the required containers. 78 | 79 | **_-c, --config_** - Provide path to a config.json. No configuration is required. If a config.json is provided, all other "config flags" are ignored and runtime params are derived solely from the config.json. See documentation for example config.json 80 | 81 | **_-t, --test_** - Will run the entire program on a much smaller dataset (morocco-latest.osm.pbf). The program will download the test data and generate a planet.mbtiles from it. This is useful for testing both the output and that your system meets the requirements. You cannot set any other flags in conjunction with this flag. if you wish to run your own custom test then please set a config.json file with your own smaller dataset and other options. 82 | 83 | ## Why? 84 | 85 | There are some wonderful options out there for generating and serving your own map data and there are many reasons to want to do so. My reason, and the inspiration for this program was cost. It is expensive to use a paid tile server option after less users using it than you might think. The problem is, when trying to host your own, a lot of research has shown me that almost all solutions for self generating tiles for a map server require hugely expensive hardware to even complete (it's not uncommon to see requirements for 64 cores and 128 GB RAM!); indeed the largest I've seen wanted 150 GB of the stuff! (for generating the planet that is). If you want a small section of the world, then it is much easier, but I need the planet - so what to do? Generate smaller sections of the world, then combine them. 86 | 87 | That's where [sequentially-generate-planet-mbtiles](https://github.com/lambdajack/sequentially-generate-planet-mbtiles) comes in. It downloads the latest osm data (or uses your supplied pbf file), splits it into manageable chunks, generates mbtiles from those chunks and then stitches it all together. 88 | 89 | **_This program aims to be a simple set and forget, one liner which gives anyone - a way to get a full-featured and bang up to date set of vector tiles for the entire planet on small hardware._** 90 | 91 | It's also designed (work in progress) to be fail safe - meaning that if your hardware (or our software) does crash mid process, you have not lost all your data, and you are able to resume the work where the program left off. 92 | 93 | This also uses the openmaptiles mbtiles spec (by default but this can be changed), meaning that when accessing the served tiles you can easily use most of the open source styles available. See our included styles which we can target - you may wish to use those on your front end for a quick setup. More information on styles can be found below. 94 | 95 | ## Considerations 96 | 97 | 1. Hardware usage - this will consume effectively 100% CPU for up to a few days and will also do millions of read/writes from ssd/RAM/CPUcache. While modern hardware and vps' are perfectly capable of handling this, if you are using old hardware, beware that its remaining lifespan may be significantly reduced. 98 | 2. Cost - related to the above, while this program and everything it uses is entirely free and open source - the person's/company's computer you're running it on might charge you electricity/load costs etc. Please check with your provider, how they handle fair use. 99 | 3. Time - your hardware will be unable to do anything much other than run this program while it is running. This is in order to be efficient and is by design. If your hardware is hosting other production software or will be needed for other things in the next few days, be aware that it will perform suboptimally while this is running. 100 | 4. Bandwidth - this will download the entire planet's worth of openstreetmap data directly from OSM. At the time of writing, this is approx. 64 GB. **Please note: ** the program will look for a `planet-latest.osm.pbf` file in the `data/pbf` folder. If this is already present, it will skip the download and use this file. If you already have the data you wish to generate mbtiles for, you can place it there to skip the download. This can be useful if you want historical data, or are generating the mbtiles on multiple computers. 101 | 5. Data generation - in order to remain relatively fast on low spec hardware, this program systematically breaks up the OSM data into more manegable chunks before processing. Therefore, expect around 300 GB of storage to be used up on completion. 102 | 103 | ## Requirements 104 | 105 | ### Hardware 106 | 107 | 1. About 300 GB clear disk space for the entire planet. Probably an SSD unless you like pain, suffering and the watching the slow creep of old age. 108 | 2. About 3 GB of clear RAM (so maybe 4/5 GB if used on a desktop pc). We are working on options in the future for lower RAM requirements. 109 | 3. Time. As above, this has been written to massively streamline the process of getting a planetary vector tile set for the average person who might not have the strongest hardware or the desire to spend £££ on a 64 core 128 GB RAM server. Unfortunately, if you cut out the cost, you increase the time. Expect the process to take a couple of days from start to finish on small/old hardware. 110 | 111 | ### Software 112 | 113 | 1. Have [Docker](https://www.docker.com/) installed. 114 | 115 | # Serving mbtiles 116 | 117 | ## Software 118 | 119 | We would recommend something like [tileserver-gl](https://github.com/maptiler/tileserver-gl) as a good place to start. Further reading can be found on the [openstreetmap wiki](https://wiki.openstreetmap.org/wiki/MBTiles). 120 | 121 | You can quickly serve using tileserver-gl (remember to mount the correct volume containing your planet.mbtiles): 122 | 123 | ```bash 124 | docker run --rm -it -v $(pwd)/data/out:/data -p 8080:80 maptiler/tileserver-gl 125 | ``` 126 | 127 | ## Styles 128 | 129 | The default output of `sequentially-generate-planet-mbtiles` looks to match with the open source tileserver-gl ['Basic'](https://github.com/lambdajack/sequentially-generate-planet-mbtiles/blob/master/configs/styles/sgpm-basic.json) style. 130 | 131 | When accessing your tileserver with something like [MapLibre](https://maplibre.org/maplibre-gl-js-docs/api/) from a front end application, a good place to start would be passing it a copy of the above style, **making sure to edit the urls to point to the correct places**. 132 | 133 | You can edit the output of `sequentially-generate-planet-mbtiles` by providing a customised process or config file through the config file. We also have built in support for targeting the open source OSM ['Bright'](https://github.com/lambdajack/sequentially-generate-planet-mbtiles/blob/master/configs/styles/sgpm-bright.json) style. 134 | 135 | ### Some style considerations 136 | 137 | If making your own style or editing an existing one, note that `sequentially-generate-planet-mbtiles` by default will write text to the `name:latin` tag. If your maps are displayed, but missing text, check that your style is looking for `name:latin` and not something else (e.g. simply `name`). 138 | 139 | Pay attention to your fonts. The OSM Bright style makes use of Noto Sans variants (bold, italic & regular). If you are using tileserver-gl to serve your tiles, it only comes with the regular variant of Noto Sans (not the bold or the italic); therefore, it may look like text labels are missing since the style won't be able to find the fonts. You should therefore consider editing the style and changing all mentions of the font to use only the regular variant. Alternatively, you could ensure that all fonts necessary are present. 140 | 141 | ## FAQ 142 | 143 | 1. **How long will this take?** We are pleased to anounce that the new version 3 slicing algorithm is seen to be about twice as fast as the one used in version 2. An average 8core CPU/16 GB pc should take less than 24 hours (download speed often being the largest cause of variance). 144 | 2. **Do I have to download the entire planet?** No! If you already have a pbf file of your own that you would like to generate and mbtiles file from, you can provide it as the pbfFile in the config (or with flags). Support is comming in the future for continent processing (meaning if you want 'Africa' for example, in the future you should be able to pass that as a flag and we will take care of the rest). 145 | 3. **Would I use this if I have powerful hardware?** Maybe. Since the program essentially saves its progress as it goes, even if you have strong hardware, you are reducing the time taken to redo the process in the event of a crash or file corruption. Further, the RAM is what is really saved here so if you have say 32 cores and 64 GB RAM, you still would not be able to generate the entire planet by loading it into memory. Additionally, it just saves time configuring everything. 146 | 4. **Why do I have to run part of the program with 'sudo' privileges?** Many docker installations require sudo to be executed. You may not have to execute the program with sudo. 147 | 5. **Does 'low spec' mean I can run it on my toaster?** Maybe, but mostly not. But you can happily run it on you 4core CPU/4 GB RAM home pc without too much trouble. Just time. 148 | 6. **Why would I use this over Planetiler?** Planetiler is a fantastic project, however it still requires over 32 GB RAM to complete the entire planet (0.5x the size of the planet pbf file). 149 | 150 | ## Examples 151 | 152 | Use all defaults (download all required data and generate a planet.mbtiles file): 153 | 154 | ```bash 155 | sudo ./sequentially-generate-planet-mbtiles--unix-amd64-v3.1.0 156 | ``` 157 | 158 | Providing a config.json: 159 | 160 | ```bash 161 | sudo ./sequentially-generate-planet-mbtiles--unix-amd64-v3.1.0 -c /path/to/config.json 162 | ``` 163 | 164 | Use a specific source file, and send the output to a specific place. Target style to match sgpm-bright: 165 | 166 | ```bash 167 | sudo ./sequentially-generate-planet-mbtiles--unix-amd64-v3.1.0 -p /path/to/planet-latest.osm.pbf -o /path/to/output/dir -tp sgpm-bright 168 | ``` 169 | 170 | ## Acknowledgements 171 | 172 | Please take the time to thank the folks over at [tilemaker](https://github.com/systemed/tilemaker), [tippecanoe](https://github.com/mapbox/tippecanoe), [osmium](https://github.com/osmcode/libosmium) and [gdal](https://gdal.org/). They are the reason any of this is possible in the first place. It goes without saying, our thanks go out to [OpenStreetMap](https://www.openstreetmap.org/copyright). 173 | 174 | ## Attribution 175 | 176 | Please attribute openmaptiles, openstreemap contributors and tippecanoe if any data derived from this program is used in production. 177 | 178 | ## Licenses 179 | 180 | Files generated by `sequentially-generate-planet-mbtiles` are subject to the licenses described by [tippecanoe](https://github.com/mapbox/tippecanoe) and [OpenStreetMap](https://www.openstreetmap.org/copyright). All third party licences can be found in the relevant submodule to this repo. We encourage you to consider them carefully, as always. 181 | 182 | `sequentially-generate-planet-mbtiles` is subject to the MIT [license](LICENSE). 183 | 184 | ## Contributions 185 | 186 | All welcome! Feature request, pull request, bug reports/fixes etc - go for it. 187 | 188 | ## Version 2 (old) 189 | 190 | See the version 2 [README](https://github.com/lambdajack/sequentially-generate-planet-mbtiles/blob/v2.2.0/README.md). 191 | 192 | ### Currently working on: 193 | 194 | - v4.0.0 milestone - add automatic fetching of pbf files for continents to use instead of the planet. 195 | - v4.0.0 milestone - ability to download and merge osm updates to existing mbtiles. 196 | -------------------------------------------------------------------------------- /assets/sequentially-generate-planet-mbtiles-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdajack/sequentially-generate-planet-mbtiles/b9a53973f6bd5bd76d0cc5fe3c4e537116add9dc/assets/sequentially-generate-planet-mbtiles-example.gif -------------------------------------------------------------------------------- /build/osmium/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm-slim as builder 2 | 3 | RUN apt-get update 4 | RUN apt-get update && apt-get install -y \ 5 | libosmium2-dev libprotozero-dev libboost-program-options-dev libbz2-dev zlib1g-dev liblz4-dev libexpat1-dev cmake pandoc build-essential 6 | 7 | RUN mkdir /var/install 8 | WORKDIR /var/install 9 | 10 | COPY libosmium libosmium 11 | 12 | RUN cd libosmium && \ 13 | mkdir build && cd build && \ 14 | cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_EXAMPLES=OFF -DBUILD_TESTING=OFF -DINSTALL_PROTOZERO=ON .. && \ 15 | make 16 | 17 | COPY osmium-tool osmium-tool 18 | 19 | RUN cd osmium-tool && \ 20 | mkdir build && cd build && \ 21 | cmake -DOSMIUM_INCLUDE_DIR=/var/install/libosmium/include/ .. && \ 22 | make 23 | 24 | RUN mv /var/install/osmium-tool/build/src/osmium /usr/bin/osmium -------------------------------------------------------------------------------- /cmd/sequentially-generate-planet-mbtiles/config.go: -------------------------------------------------------------------------------- 1 | package sequentiallygenerateplanetmbtiles 2 | 3 | import ( 4 | "bufio" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/lambdajack/lj_go/pkg/lj_json" 13 | ) 14 | 15 | type configuration struct { 16 | srcFileProvided bool 17 | PbfFile string `json:"pbfFile"` 18 | WorkingDir string `json:"workingDir"` 19 | OutDir string `json:"outDir"` 20 | ExcludeOcean bool `json:"excludeOcean"` 21 | ExcludeLanduse bool `json:"excludeLanduse"` 22 | TilemakerConfig string `json:"TilemakerConfig"` 23 | TilemakerProcess string `json:"TilemakerProcess"` 24 | MaxRamMb uint64 `json:"maxRamMb"` 25 | OutAsDir bool `json:"outAsDir"` 26 | SkipSlicing bool `json:"skipSlicing"` 27 | MergeOnly bool `json:"mergeOnly"` 28 | SkipDownload bool `json:"skipDownload"` 29 | } 30 | 31 | func initConfig() { 32 | if fl.config == "" { 33 | setConfigByFlags() 34 | } else { 35 | setConfigByJSON() 36 | } 37 | 38 | verifyPaths() 39 | 40 | cfg.PbfFile = convertAbs(cfg.PbfFile) 41 | cfg.WorkingDir = convertAbs(cfg.WorkingDir) 42 | cfg.OutDir = convertAbs(cfg.OutDir) 43 | } 44 | 45 | func setConfigByJSON() { 46 | err := lj_json.DecodeTo(cfg, fl.config, 1000) 47 | if err != nil { 48 | log.Printf("Unable to decode config: %s", err) 49 | os.Exit(exitInvalidJSON) 50 | } 51 | if cfg.MaxRamMb == 0 { 52 | cfg.MaxRamMb = getRam() 53 | } 54 | } 55 | 56 | func setConfigByFlags() { 57 | cfg.PbfFile = fl.pbfFile 58 | cfg.WorkingDir = fl.workingDir 59 | cfg.OutDir = fl.outDir 60 | cfg.ExcludeOcean = fl.excludeOcean 61 | cfg.ExcludeLanduse = fl.excludeLanduse 62 | cfg.TilemakerConfig = fl.tilemakerConfig 63 | cfg.TilemakerProcess = fl.tilemakerProcess 64 | cfg.MaxRamMb = getRam() 65 | cfg.OutAsDir = fl.outAsDir 66 | cfg.SkipSlicing = fl.skipSlicing 67 | cfg.MergeOnly = fl.mergeOnly 68 | cfg.SkipDownload = fl.skipDownload 69 | } 70 | 71 | func setTmPaths() { 72 | if cfg.TilemakerProcess == "" || strings.ToLower(cfg.TilemakerProcess) == "tileserver-gl-basic" { 73 | cfg.TilemakerProcess = filepath.Join(pth.temp, "tilemaker", "resources", "process-openmaptiles.lua") 74 | log.Println("using tileserver-gl-basic style target") 75 | } 76 | 77 | if cfg.TilemakerProcess == "sgpm-bright" { 78 | cfg.TilemakerProcess = filepath.Join(pth.temp, "tilemaker", "resources", "process-bright.lua") 79 | log.Println("using sgpm-bright style target") 80 | } 81 | 82 | if cfg.TilemakerConfig == "" { 83 | cfg.TilemakerConfig = filepath.Join(pth.temp, "tilemaker", "resources", "config-openmaptiles.json") 84 | } 85 | 86 | cfg.TilemakerConfig = convertAbs(cfg.TilemakerConfig) 87 | cfg.TilemakerProcess = convertAbs(cfg.TilemakerProcess) 88 | } 89 | 90 | func verifyPaths() { 91 | if cfg.PbfFile != "" { 92 | if _, err := os.Stat(cfg.PbfFile); os.IsNotExist(err) { 93 | log.Fatalf("planet file does not exist: %s", cfg.PbfFile) 94 | } 95 | cfg.srcFileProvided = true 96 | } else { 97 | if fl.test { 98 | cfg.PbfFile = filepath.Join(cfg.WorkingDir, "pbf", "morocco-latest.osm.pbf") 99 | } else { 100 | cfg.PbfFile = filepath.Join(cfg.WorkingDir, "pbf", "planet-latest.osm.pbf") 101 | } 102 | } 103 | 104 | if cfg.TilemakerConfig != "" { 105 | if _, err := os.Stat(cfg.TilemakerConfig); os.IsNotExist(err) { 106 | log.Fatalf("tilemaker config does not exist: %s", cfg.TilemakerConfig) 107 | } 108 | } 109 | 110 | if cfg.TilemakerProcess != "" && cfg.TilemakerProcess != "sgpm-bright" && cfg.TilemakerProcess != "tileserver-gl-basic" { 111 | if _, err := os.Stat(cfg.TilemakerProcess); os.IsNotExist(err) { 112 | log.Fatalf("tilemaker process does not exist: %s", cfg.TilemakerProcess) 113 | } 114 | } 115 | } 116 | 117 | func getRam() uint64 { 118 | if fl.maxRamMb != 0 { 119 | return fl.maxRamMb 120 | } 121 | 122 | var memTotalKb uint64 123 | 124 | f, err := os.Open("/proc/meminfo") 125 | if err != nil { 126 | log.Printf("Unable to open /proc/meminfo: %s", err) 127 | memTotalKb = 4096 * 1024 128 | } else { 129 | defer f.Close() 130 | 131 | scanner := bufio.NewScanner(f) 132 | 133 | memTotalStr := "" 134 | 135 | for scanner.Scan() { 136 | if strings.HasPrefix(scanner.Text(), "MemTotal:") { 137 | memTotalStr = regexp.MustCompile(`[0-9]+`).FindString(scanner.Text()) 138 | } 139 | } 140 | 141 | memTotalKb, err = strconv.ParseUint(memTotalStr, 10, 64) 142 | if err != nil { 143 | log.Printf("Unable to parse MemTotal: %s", err) 144 | memTotalKb = 4096 * 1024 145 | } 146 | } 147 | 148 | if (memTotalKb / 1024) < 2048 { 149 | log.Printf("system ram is less than 2 GB; this is dangerously low, but we will do our best!") 150 | return 1024 151 | } 152 | 153 | memTotalMb := memTotalKb / 1024 154 | 155 | totalRamToUse := memTotalMb - 2048 156 | 157 | if totalRamToUse < 1024 { 158 | totalRamToUse = 1024 159 | } 160 | 161 | log.Printf("attempting to use not more than %d MB of ram", totalRamToUse) 162 | 163 | return totalRamToUse 164 | } 165 | -------------------------------------------------------------------------------- /cmd/sequentially-generate-planet-mbtiles/config_test.go: -------------------------------------------------------------------------------- 1 | package sequentiallygenerateplanetmbtiles 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "testing" 10 | ) 11 | 12 | func init() { 13 | 14 | tmpDir := filepath.Join(os.TempDir(), "sequentially-generate-planet-mbtiles") 15 | 16 | err := os.MkdirAll(tmpDir, os.ModePerm) 17 | if err != nil { 18 | panic("Could not make os.TempDir/sequentially-generate-planet-mbtiles. Permissions issue? TempleOS? Expect many tests to fail.") 19 | } 20 | 21 | if _, err := os.Stat(tmpDir); os.IsNotExist(err) { 22 | panic("Could not find os.TempDir. Permissions issue? TempleOS? Expect many tests to fail.") 23 | } 24 | 25 | cfg = &configuration{ 26 | PbfFile: "", // CHANGE TO TEST FILE ONCE SET U 27 | WorkingDir: tmpDir, 28 | OutDir: filepath.Join(tmpDir, "out"), 29 | ExcludeOcean: true, 30 | ExcludeLanduse: true, 31 | TilemakerConfig: "", 32 | TilemakerProcess: "", 33 | MaxRamMb: 1, 34 | } 35 | 36 | fmt.Printf("cfg initialised: %+v\n", cfg) 37 | } 38 | 39 | func TestSetInvalidConfigByJSON(t *testing.T) { 40 | // Test run in separate processes so as not to pollute other tests 41 | 42 | invalidConfig := "../../configs/test/TEST_INVALID.json" 43 | 44 | if _, err := os.Stat(invalidConfig); os.IsNotExist(err) { 45 | t.Errorf("TestSetConfigByJSON test config file does not exist") 46 | } 47 | 48 | if os.Getenv("TEST_SET_CONFIG_BY_JSON") == "1" { 49 | os.Args = append(os.Args, "-c", invalidConfig) 50 | flag.Parse() 51 | setConfigByJSON() 52 | return 53 | } 54 | 55 | cmd := exec.Command(os.Args[0], "-test.run=TestSetInvalidConfigByJSON") 56 | cmd.Env = append(os.Environ(), "TEST_SET_CONFIG_BY_JSON=1") 57 | defer os.Unsetenv("TEST_SET_CONFIG_BY_JSON") 58 | err := cmd.Run() 59 | 60 | if e, ok := err.(*exec.ExitError); ok && !e.Success() { 61 | if e.ExitCode() == exitInvalidJSON { 62 | return 63 | } 64 | } 65 | 66 | t.Fatalf("process ran with err %v, want exit status %v", err, exitInvalidJSON) 67 | } 68 | 69 | func TestSetValidConfigByJSON(t *testing.T) { 70 | validConfig := "../../configs/test/TEST_VALID.json" 71 | 72 | if _, err := os.Stat(validConfig); os.IsNotExist(err) { 73 | t.Errorf("TestSetConfigByJSON test config file does not exist") 74 | } 75 | 76 | if os.Getenv("TEST_SET_VALID_CONFIG_BY_JSON") == "1" { 77 | t.Log(os.Args) 78 | os.Args = append(os.Args, "-c", validConfig) 79 | flag.Parse() 80 | setConfigByJSON() 81 | return 82 | } 83 | 84 | cmd := exec.Command(os.Args[0], "-test.run=TestSetValidConfigByJSON") 85 | cmd.Env = append(os.Environ(), "TEST_SET_VALID_CONFIG_BY_JSON=1") 86 | defer os.Unsetenv("TEST_SET_VALID_CONFIG_BY_JSON") 87 | err := cmd.Run() 88 | if err != nil { 89 | t.Fatalf("process ran with err %v, want %v", err, nil) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /cmd/sequentially-generate-planet-mbtiles/containers.go: -------------------------------------------------------------------------------- 1 | package sequentiallygenerateplanetmbtiles 2 | 3 | import ( 4 | _ "embed" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/lambdajack/sequentially-generate-planet-mbtiles/internal/docker" 9 | "github.com/lambdajack/sequentially-generate-planet-mbtiles/internal/system" 10 | ) 11 | 12 | type containers struct { 13 | gdal *docker.Container 14 | osmium *docker.Container 15 | tilemaker *docker.Container 16 | tippecanoe *docker.Container 17 | } 18 | 19 | var ct containers 20 | 21 | func setupContainers(osmiumDf []byte) { 22 | ct = containers{ 23 | gdal: docker.New(docker.Container{ 24 | Name: "sequential-gdal", 25 | Dockerfile: filepath.Join(gh.gdal.Dst, "docker", "alpine-small", "Dockerfile"), 26 | Context: gh.gdal.Dst, 27 | }), 28 | osmium: docker.New(docker.Container{ 29 | Name: "sequential-osmium", 30 | Dockerfile: setOsmiumDockerfile(osmiumDf), 31 | // Context set to pth.temp/osmium since the docker file needs to pull in two separate repos, both in the same dir 32 | Context: filepath.Join(pth.temp, "osmium"), 33 | }), 34 | tilemaker: docker.New(docker.Container{ 35 | Name: "sequential-tilemaker", 36 | Dockerfile: filepath.Join(gh.tilemaker.Dst, "Dockerfile"), 37 | Context: gh.tilemaker.Dst, 38 | }), 39 | tippecanoe: docker.New(docker.Container{ 40 | Name: "sequential-tippecanoe", 41 | Dockerfile: filepath.Join(gh.tippecanoe.Dst, "Dockerfile"), 42 | Context: gh.tippecanoe.Dst, 43 | }), 44 | } 45 | 46 | err := ct.osmium.Build() 47 | if err != nil { 48 | lg.err.Fatalln("failed to build osmium container:", err) 49 | } 50 | 51 | err = ct.gdal.Build() 52 | if err != nil { 53 | lg.err.Fatalln("failed to build gdal container:", err) 54 | } 55 | 56 | err = ct.tilemaker.Build() 57 | if err != nil { 58 | lg.err.Fatalln("failed to build tilemaker container:", err) 59 | } 60 | 61 | err = ct.tippecanoe.Build() 62 | if err != nil { 63 | lg.err.Fatalln("failed to build tippecanoe container:", err) 64 | } 65 | } 66 | 67 | func cleanContainers() { 68 | err := ct.gdal.Clean() 69 | if err != nil { 70 | lg.err.Println("failed to clean gdal container:", err) 71 | } 72 | 73 | err = ct.osmium.Clean() 74 | if err != nil { 75 | lg.err.Println("failed to clean osmium container:", err) 76 | } 77 | 78 | err = ct.tilemaker.Clean() 79 | if err != nil { 80 | lg.err.Println("failed to clean tilemaker container:", err) 81 | } 82 | 83 | err = ct.tippecanoe.Clean() 84 | if err != nil { 85 | lg.err.Println("failed to clean tippecanoe container:", err) 86 | } 87 | } 88 | 89 | func setOsmiumDockerfile(df []byte) string { 90 | f, err := os.Create(filepath.Join(pth.temp, "Dockerfile")) 91 | if err != nil { 92 | lg.err.Fatalln("failed to create Osmium Dockerfile:", err) 93 | } 94 | 95 | _, err = f.Write(df) 96 | if err != nil { 97 | lg.err.Fatalln("failed to write Osmium Dockerfile:", err) 98 | } 99 | 100 | system.SetUserOwner(f.Name()) 101 | 102 | return f.Name() 103 | } 104 | -------------------------------------------------------------------------------- /cmd/sequentially-generate-planet-mbtiles/dir.go: -------------------------------------------------------------------------------- 1 | package sequentiallygenerateplanetmbtiles 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/lambdajack/sequentially-generate-planet-mbtiles/internal/system" 9 | ) 10 | 11 | type paths struct { 12 | workingDir string 13 | outDir string 14 | coastlineDir string 15 | landcoverDir string 16 | landCoverUrbanDepth string 17 | landCoverIceShelvesDepth string 18 | landCoverGlaciatedDepth string 19 | pbfDir string 20 | pbfSlicesDir string 21 | mbtilesDir string 22 | logsDir string 23 | temp string 24 | } 25 | 26 | var pth = paths{} 27 | 28 | func initDirStructure() { 29 | 30 | tmp, err := os.MkdirTemp(os.TempDir(), "*") 31 | if err != nil { 32 | log.Printf("Unable to create temp dir: %s", err) 33 | os.Exit(exitPermissions) 34 | } 35 | 36 | pth.workingDir = convertAbs(cfg.WorkingDir) 37 | makeDir(pth.workingDir) 38 | 39 | pth.outDir = convertAbs(cfg.OutDir) 40 | makeDir(pth.outDir) 41 | 42 | if !cfg.ExcludeOcean { 43 | pth.coastlineDir = filepath.Join(pth.workingDir, "coastline") 44 | makeDir(pth.coastlineDir) 45 | } else { 46 | pth.coastlineDir = filepath.Join(tmp, "coastline") 47 | } 48 | 49 | if !cfg.ExcludeLanduse { 50 | pth.landcoverDir = filepath.Join(pth.workingDir, "landcover") 51 | makeDir(pth.landcoverDir) 52 | } else { 53 | pth.landcoverDir = filepath.Join(tmp, "landcover") 54 | } 55 | 56 | pth.landCoverUrbanDepth = filepath.Join(pth.landcoverDir, "ne_10m_urban_areas") 57 | makeDir(pth.landCoverUrbanDepth) 58 | 59 | pth.landCoverIceShelvesDepth = filepath.Join(pth.landcoverDir, "ne_10m_antarctic_ice_shelves_polys") 60 | makeDir(pth.landCoverIceShelvesDepth) 61 | 62 | pth.landCoverGlaciatedDepth = filepath.Join(pth.landcoverDir, "ne_10m_glaciated_areas") 63 | makeDir(pth.landCoverGlaciatedDepth) 64 | 65 | pth.pbfDir = filepath.Join(pth.workingDir, "pbf") 66 | makeDir(pth.pbfDir) 67 | 68 | pth.pbfSlicesDir = filepath.Join(pth.pbfDir, "slices") 69 | makeDir(pth.pbfSlicesDir) 70 | 71 | pth.mbtilesDir = filepath.Join(pth.workingDir, "mbtiles") 72 | makeDir(pth.mbtilesDir) 73 | 74 | pth.logsDir = filepath.Join(pth.workingDir, "logs") 75 | makeDir(pth.logsDir) 76 | 77 | if system.DockerIsSnap() { 78 | log.Println("snap version of docker detected; using local tmp folder since snap docker cannot access system /tmp") 79 | pth.temp = filepath.Join(pth.workingDir, "tmp") 80 | } else { 81 | ucd := system.UserCacheDir() 82 | if ucd != "" { 83 | pth.temp = filepath.Join(system.UserCacheDir(), "sequentially-generate-planet-mbtiles") 84 | } else { 85 | pth.temp = filepath.Join(pth.workingDir, "tmp") 86 | } 87 | } 88 | makeDir(pth.temp) 89 | } 90 | 91 | func convertAbs(path string) string { 92 | if path == "" { 93 | return "" 94 | } 95 | 96 | p, err := filepath.Abs(path) 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | return p 101 | } 102 | 103 | func makeDir(fldr string) { 104 | if err := os.MkdirAll(fldr, os.ModePerm); err != nil { 105 | log.Printf("Unable to create %s Dir", fldr) 106 | os.Exit(exitPermissions) 107 | } 108 | system.SetUserOwner(fldr) 109 | } 110 | -------------------------------------------------------------------------------- /cmd/sequentially-generate-planet-mbtiles/dir_test.go: -------------------------------------------------------------------------------- 1 | package sequentiallygenerateplanetmbtiles 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | ) 8 | 9 | func TestMakeDir(t *testing.T) { 10 | tmp := t.TempDir() 11 | 12 | Dir := filepath.Join(tmp, "test") 13 | 14 | makeDir(Dir) 15 | 16 | if _, err := os.Stat(Dir); os.IsNotExist(err) { 17 | t.Errorf("Dir %s does not exist", Dir) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /cmd/sequentially-generate-planet-mbtiles/downloader.go: -------------------------------------------------------------------------------- 1 | package sequentiallygenerateplanetmbtiles 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/lambdajack/lj_go/pkg/lj_http" 9 | ) 10 | 11 | type downloadInformation struct { 12 | url, destFileName, destDir string 13 | } 14 | 15 | func downloadOsmData() { 16 | var downloads = []downloadInformation{ 17 | { 18 | url: "https://planet.openstreetmap.org/pbf/planet-latest.osm.pbf", 19 | destFileName: "planet-latest.osm.pbf", 20 | destDir: pth.pbfDir, 21 | }, 22 | { 23 | url: "https://osmdata.openstreetmap.de/download/water-polygons-split-4326.zip", 24 | destFileName: "water-polygons-split-4326.zip", 25 | destDir: pth.workingDir, 26 | }, 27 | { 28 | url: "https://naciscdn.org/naturalearth/10m/cultural/ne_10m_urban_areas.zip", 29 | destFileName: "ne_10m_urban_areas.zip", 30 | destDir: pth.workingDir, 31 | }, 32 | { 33 | url: "https://naciscdn.org/naturalearth/10m/physical/ne_10m_antarctic_ice_shelves_polys.zip", 34 | destFileName: "ne_10m_antarctic_ice_shelves_polys.zip", 35 | destDir: pth.workingDir, 36 | }, 37 | { 38 | url: "https://naciscdn.org/naturalearth/10m/physical/ne_10m_glaciated_areas.zip", 39 | destFileName: "ne_10m_glaciated_areas.zip", 40 | destDir: pth.workingDir, 41 | }, 42 | } 43 | 44 | if fl.test { 45 | downloads[0].url = "https://download.geofabrik.de/africa/morocco-latest.osm.pbf" 46 | downloads[0].destFileName = "morocco-latest.osm.pbf" 47 | lg.rep.Printf("test flag provided; downloading test data %s - %s", downloads[0].destFileName, downloads[0].url) 48 | } 49 | 50 | for _, dl := range downloads { 51 | if _, err := os.Stat(filepath.Join(dl.destDir, dl.destFileName)); os.IsNotExist(err) { 52 | if dl.destFileName == "planet-latest.osm.pbf" { 53 | if cfg.srcFileProvided { 54 | lg.rep.Printf("source file provided - skipping planet download %s", dl.url) 55 | continue 56 | } 57 | if cfg.SkipDownload { 58 | lg.rep.Printf("skip download flag provided - skipping planet download %s", dl.url) 59 | continue 60 | } 61 | } 62 | 63 | if cfg.ExcludeOcean && strings.Contains(dl.destFileName, "water") { 64 | lg.rep.Printf("skipping download of %s", dl.url) 65 | continue 66 | } 67 | 68 | if cfg.ExcludeLanduse && strings.Contains(dl.destFileName, "ne_") { 69 | lg.rep.Printf("skipping download of %s", dl.url) 70 | continue 71 | } 72 | 73 | err := lj_http.Download(dl.url, dl.destDir, dl.destFileName) 74 | if err != nil { 75 | lg.err.Printf("error downloading %s: %s", dl.url, err) 76 | os.Exit(exitDownloadURL) 77 | } 78 | lg.rep.Printf("Download success: %v\n", dl.destFileName) 79 | } else { 80 | lg.rep.Printf("%v already exists; skipping download.\n", dl.destFileName) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /cmd/sequentially-generate-planet-mbtiles/filehandler.go: -------------------------------------------------------------------------------- 1 | package sequentiallygenerateplanetmbtiles 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | func moveOcean() { 10 | if !cfg.ExcludeOcean { 11 | filepath.Walk(pth.coastlineDir, func(path string, info fs.FileInfo, err error) error { 12 | if !info.IsDir() { 13 | err := os.Rename(path, filepath.Join(pth.coastlineDir, filepath.Base(path))) 14 | if err != nil { 15 | return err 16 | } 17 | } 18 | return nil 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cmd/sequentially-generate-planet-mbtiles/flags.go: -------------------------------------------------------------------------------- 1 | package sequentiallygenerateplanetmbtiles 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | ) 9 | 10 | type flags struct { 11 | version bool 12 | stage bool 13 | config string 14 | test bool 15 | pbfFile string 16 | workingDir string 17 | outDir string 18 | excludeOcean bool 19 | excludeLanduse bool 20 | tilemakerConfig string 21 | tilemakerProcess string 22 | maxRamMb uint64 23 | outAsDir bool 24 | skipSlicing bool 25 | mergeOnly bool 26 | skipDownload bool 27 | } 28 | 29 | var fl = &flags{} 30 | 31 | func initFlags() { 32 | flag.BoolVar(&fl.version, "v", false, "") 33 | flag.BoolVar(&fl.version, "version", false, "") 34 | 35 | flag.BoolVar(&fl.stage, "s", false, "") 36 | flag.BoolVar(&fl.stage, "stage", false, "") 37 | 38 | flag.StringVar(&fl.config, "c", "", "") 39 | flag.StringVar(&fl.config, "config", "", "") 40 | 41 | flag.BoolVar(&fl.test, "t", false, "") 42 | flag.BoolVar(&fl.test, "test", false, "") 43 | 44 | flag.StringVar(&fl.pbfFile, "p", "", "") 45 | flag.StringVar(&fl.pbfFile, "pbf-file", "", "") 46 | 47 | flag.StringVar(&fl.workingDir, "w", "data", "") 48 | flag.StringVar(&fl.workingDir, "working-dir", "data", "") 49 | 50 | flag.StringVar(&fl.outDir, "o", "data/out", "") 51 | flag.StringVar(&fl.outDir, "outdir", "data/out", "") 52 | 53 | flag.BoolVar(&fl.excludeOcean, "eo", false, "") 54 | flag.BoolVar(&fl.excludeOcean, "exclude-ocean", false, "") 55 | 56 | flag.BoolVar(&fl.excludeLanduse, "el", false, "") 57 | flag.BoolVar(&fl.excludeLanduse, "exclude-landuse", false, "") 58 | 59 | flag.StringVar(&fl.tilemakerConfig, "tc", "", "") 60 | flag.StringVar(&fl.tilemakerConfig, "tilemaker-config", "", "") 61 | 62 | flag.StringVar(&fl.tilemakerProcess, "tp", "", "") 63 | flag.StringVar(&fl.tilemakerProcess, "tilemaker-process", "", "") 64 | 65 | flag.Uint64Var(&fl.maxRamMb, "r", 0, "") 66 | flag.Uint64Var(&fl.maxRamMb, "ram", 0, "") 67 | 68 | flag.BoolVar(&fl.outAsDir, "od", false, "") 69 | flag.BoolVar(&fl.outAsDir, "out-as-dir", false, "") 70 | 71 | flag.BoolVar(&fl.skipSlicing, "ss", false, "") 72 | flag.BoolVar(&fl.skipSlicing, "skip-slicing", false, "") 73 | 74 | flag.BoolVar(&fl.mergeOnly, "mo", false, "") 75 | flag.BoolVar(&fl.mergeOnly, "merge-only", false, "") 76 | 77 | flag.BoolVar(&fl.skipDownload, "sd", false, "") 78 | flag.BoolVar(&fl.skipDownload, "skip-download", false, "") 79 | 80 | flag.Parse() 81 | } 82 | 83 | func validateFlags() { 84 | if fl.skipDownload && !fl.skipSlicing && !fl.mergeOnly { 85 | fmt.Println("-sd must be used with -ss or -mo. See -h for more information.") 86 | os.Exit(exitFlags) 87 | } 88 | 89 | configFlag := fl.config 90 | 91 | var defaultConfigFlagValue string 92 | 93 | flag.Visit(func(f *flag.Flag) { 94 | if f.Name == "config" || f.Name == "c" { 95 | defaultConfigFlagValue = f.DefValue 96 | } 97 | }) 98 | 99 | if fl.test { 100 | of := false 101 | flag.Visit(func(f *flag.Flag) { 102 | if f.Name != "test" && f.Name != "t" { 103 | of = true 104 | } 105 | }) 106 | if of { 107 | log.Println("you cannot use the -t or --test flag with any other flags.") 108 | os.Exit(exitFlags) 109 | } 110 | } 111 | 112 | if configFlag != defaultConfigFlagValue { 113 | invalidFlags := false 114 | flag.Visit(func(f *flag.Flag) { 115 | if f.Name != "config" && f.Name != "c" && f.Name != "s" && f.Name != "stage" { 116 | if f.Value.String() != f.DefValue { 117 | log.Printf("[WARN] -%s flag was provided but is overwitten by the provided config.json. Please supply either only a config.json file OR configuration flags. See '-h' for more information.", f.Name) 118 | invalidFlags = true 119 | } 120 | } 121 | if f.Name == "eo" || f.Name == "exclude-ocean" || f.Name == "el" || f.Name == "exclude-landuse" { 122 | log.Printf("[WARN] -%s flag was provided but is overwitten by the provided config.json. Please supply either only a config.json file OR configuration flags. See '-h' for more information.", f.Name) 123 | invalidFlags = true 124 | } 125 | }) 126 | 127 | if invalidFlags { 128 | os.Exit(exitFlags) 129 | } 130 | } 131 | } 132 | 133 | func helpMessage() { 134 | flag.Usage = func() { 135 | h := ` 136 | Sequentially Generate Planet Mbtiles 137 | ____________________________________ 138 | 139 | Usage: 140 | sequentially-generate-planet-mbtiles [OPTIONS] 141 | 142 | Options: 143 | -h, --help Print this help message 144 | -v, --version Print version information 145 | 146 | -s, --stage Initialise required containers, Dirs and logs 147 | based on the supplied config file and then exit. 148 | 149 | -c, --config Provide path to a config.json. No configuration 150 | is required. If a config.json is provided, all 151 | other "config flags" are ignored and runtime 152 | params are derived solely from the config.json. 153 | See documentation for example config.json. 154 | 155 | -t, --test Will run the entire program on a much smaller 156 | dataset (morocco-latest.osm.pbf). The program 157 | will download the test data and generate a 158 | planet.mbtiles from it. This is useful for 159 | testing both the output and that your system 160 | meets the requirements. You cannot set any 161 | other flags in conjunction with this flag. 162 | if you wish to run your own custom test then 163 | please set a config.json file with your own 164 | smaller dataset and other options. 165 | Config Flags: 166 | -p, --pbf-file Provide path to your osm.pbf file to be turned 167 | into mbtiles. By default a planet-latest.osm.pbf 168 | will be downloaded directly from OpenStreetMap. If 169 | a file is provided, downloading the latest 170 | planet osm data from openstreetmap is 171 | skipped and the supplied file will be used. 172 | You may use this to provide a file other than 173 | an entire planet .osm.pbf file (such as a region 174 | downloaded from https://download.geofabrik.de). 175 | 176 | -w, --working-dir Provide path to the working directory. This is 177 | where files will be downloaded to and files 178 | generated as a result of processing osm data will 179 | be stored. Temporary files will be stored here. 180 | Please ensure your designated working directory 181 | has at least 300 GB of space available. 182 | 183 | -o, --outdir Provide path to output directory for the final 184 | planet.mbtiles file. 185 | 186 | -eo, --exclude-ocean Exclude ocean tiles in final planet.mbtiles. This 187 | can significantly increase overall speed since 188 | there are a lot of ocean tiles which often forms 189 | a filesystem io bottleneck when writing them. 190 | 191 | -el, --exclude-landuse Exclude landuse layer in final planet.mbtiles. 192 | 193 | -tc, --tilemaker-config Provide path to a tilemaker configuration file. The 194 | default configuration is embedded into the release 195 | binary. See the default used here: 196 | https://github.com/lambdajack/tilemaker/blob/master/resources/config-openmaptiles.json 197 | 198 | -tp, --tilemaker-process Provide path to a tilemaker process file OR 199 | a special value. Special values are 200 | "tileserver-gl-basic", "sgpm-bright". Setting a 201 | special value will use one of the included configs 202 | to match the appropriate target style. See the 203 | default used here: 204 | https://github.com/lambdajack/tilemaker/blob/master/resources/process-openmaptiles.lua 205 | 206 | -r, --ram Provide the maximum amount of RAM in MB that the 207 | process should use. If a linux os is detected, 208 | the total system RAM will be detected from 209 | /proc/meminfo and a default will be set to a 210 | reasonably safe level, maximising the available 211 | resources. This assumes that only a minimal amount 212 | of system RAM is currently being used (such as an 213 | idle desktop environment (<2G)). If you are having 214 | memory problems, consider manually setting this flag 215 | to a reduced value. NOTE THIS IS NOT GUARANTEED AND 216 | SOME SAFETY MARGIN SHOULD BE ALLOWED. On non unix 217 | operating systems the default is set to 4096. 218 | 219 | -od, --out-as-dir The final output will be a directory of tiles 220 | rather than a single mbtiles file. This will 221 | generate hundreds of thousands of files in a 222 | predetermined directory structure. More 223 | information can ba found about this format here: 224 | https://documentation.maptiler.com/hc/en-us/articles/360020886878-Folder-vs-MBTiles-vs-GeoPackage 225 | 226 | -ss, --skip-slicing Skips the intermediate data processing/slicing 227 | and looks for existing files to convert into 228 | mbtiles in [workingDir]/pbf/slices. This is 229 | useful if you wish to experiment with different 230 | Tilemaker configs/process (for example if you 231 | wish to change the zoom levels or style tagging 232 | of the final output). Once the existing files 233 | have been converted to mbtiles, they will be 234 | merged either to a single file, or to a 235 | directory, respecting the -od flag. 236 | 237 | -mo, --merge-only Skips the entire generation process and instead 238 | looks for existing mbtiles in [workingDir]/mbtiles 239 | and merges them into a single planet.mbtiles file 240 | in the [outDir]. This is useful if you already 241 | have a tileset you wish to merge. 242 | 243 | -sd, --skip-download Skips planet download. Must be set in conjunction 244 | with -p, -ss or -mo. 245 | ` 246 | h += "\nExit Codes:\n" 247 | h += fmt.Sprintf(" %d\t%s\n", exitOK, "OK") 248 | h += fmt.Sprintf(" %d\t%s\n", exitPermissions, "Do not have permission") 249 | h += fmt.Sprintf(" %d\t%s\n", exitReadInput, "Error reading input") 250 | h += fmt.Sprintf(" %d\t%s\n", exitDownloadURL, "Error fetching URL") 251 | h += fmt.Sprintf(" %d\t%s\n", exitFlags, "Error parsing flags") 252 | h += fmt.Sprintf(" %d\t%s\n", exitInvalidJSON, "Invalid JSON") 253 | 254 | fmt.Fprint(os.Stderr, h) 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /cmd/sequentially-generate-planet-mbtiles/git.go: -------------------------------------------------------------------------------- 1 | package sequentiallygenerateplanetmbtiles 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/lambdajack/sequentially-generate-planet-mbtiles/internal/git" 8 | "github.com/lambdajack/sequentially-generate-planet-mbtiles/internal/system" 9 | ) 10 | 11 | type repos struct { 12 | gdal git.Repo 13 | osmiumTool git.Repo 14 | libosmium git.Repo 15 | tilemaker git.Repo 16 | tippecanoe git.Repo 17 | } 18 | 19 | var gh repos 20 | 21 | func cloneRepos() { 22 | gh = repos{ 23 | gdal: git.Repo{ 24 | Url: "https://github.com/lambdajack/gdal", 25 | Dst: filepath.Join(pth.temp, "gdal"), 26 | }, 27 | osmiumTool: git.Repo{ 28 | Url: "https://github.com/lambdajack/osmium-tool", 29 | Dst: filepath.Join(pth.temp, "osmium", "osmium-tool"), 30 | }, 31 | libosmium: git.Repo{ 32 | Url: "https://github.com/lambdajack/libosmium", 33 | Dst: filepath.Join(pth.temp, "osmium", "libosmium"), 34 | }, 35 | tilemaker: git.Repo{ 36 | Url: "https://github.com/lambdajack/tilemaker", 37 | Dst: filepath.Join(pth.temp, "tilemaker"), 38 | }, 39 | tippecanoe: git.Repo{ 40 | Url: "https://github.com/lambdajack/tippecanoe", 41 | Dst: filepath.Join(pth.temp, "tippecanoe"), 42 | }, 43 | } 44 | 45 | var f []string 46 | 47 | err := gh.gdal.Clone() 48 | if err != nil { 49 | f = append(f, "gdal") 50 | } 51 | 52 | err = gh.osmiumTool.Clone() 53 | if err != nil { 54 | f = append(f, "osmium-tool") 55 | } 56 | 57 | err = gh.libosmium.Clone() 58 | if err != nil { 59 | f = append(f, "libosmium") 60 | } 61 | 62 | err = gh.tilemaker.Clone() 63 | if err != nil { 64 | f = append(f, "tilemaker") 65 | } 66 | 67 | err = gh.tippecanoe.Clone() 68 | if err != nil { 69 | f = append(f, "tippecanoe") 70 | } 71 | 72 | filepath.Walk(cfg.WorkingDir, func(path string, info os.FileInfo, err error) error { 73 | if err != nil { 74 | return err 75 | } 76 | system.SetUserOwner(path) 77 | return nil 78 | }) 79 | 80 | for _, e := range f { 81 | lg.err.Fatalf("error cloning %s: %v", e, err) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /cmd/sequentially-generate-planet-mbtiles/loggers.go: -------------------------------------------------------------------------------- 1 | package sequentiallygenerateplanetmbtiles 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/lambdajack/sequentially-generate-planet-mbtiles/internal/describeloggers" 10 | ) 11 | 12 | type loggers struct { 13 | prog *log.Logger 14 | err *log.Logger 15 | rep *log.Logger 16 | } 17 | 18 | var lg = &loggers{} 19 | 20 | func initLoggers() { 21 | lg.prog = initProg(filepath.Join(pth.logsDir, "prog.log")) 22 | lg.err = initErr(filepath.Join(pth.logsDir, "err.log")) 23 | lg.rep = initRep(filepath.Join(pth.logsDir, "rep.log")) 24 | 25 | lg.prog.Println("#") 26 | lg.err.Println("#") 27 | lg.rep.Println("#") 28 | } 29 | 30 | func initProg(logPath string) *log.Logger { 31 | lf, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 32 | 33 | writer := io.MultiWriter(os.Stdout, lf) 34 | lggr := describeloggers.Prog(&writer) 35 | return lggr 36 | } 37 | 38 | func initErr(logPath string) *log.Logger { 39 | lf, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 40 | 41 | writer := io.MultiWriter(os.Stderr, lf) 42 | lggr := describeloggers.Err(&writer) 43 | return lggr 44 | } 45 | 46 | func initRep(logPath string) *log.Logger { 47 | lf, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 48 | 49 | writer := io.MultiWriter(os.Stdout, lf) 50 | lggr := describeloggers.Rep(&writer) 51 | return lggr 52 | } 53 | -------------------------------------------------------------------------------- /cmd/sequentially-generate-planet-mbtiles/loggers_test.go: -------------------------------------------------------------------------------- 1 | package sequentiallygenerateplanetmbtiles 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "path/filepath" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestInitLoggers(t *testing.T) { 13 | lg := &loggers{} 14 | initLoggers() 15 | 16 | if reflect.TypeOf(lg.prog).String() != "*log.Logger" { 17 | t.Errorf("got %v, want *log.Logger", reflect.TypeOf(lg.prog)) 18 | } 19 | 20 | if reflect.TypeOf(lg.err).String() != "*log.Logger" { 21 | t.Errorf("got %v, want *log.Logger", reflect.TypeOf(lg.err)) 22 | } 23 | 24 | if reflect.TypeOf(lg.rep).String() != "*log.Logger" { 25 | t.Errorf("got %v, want *log.Logger", reflect.TypeOf(lg.rep)) 26 | } 27 | } 28 | 29 | func TestInitProg(t *testing.T) { 30 | tmpDir := t.TempDir() 31 | logFile := filepath.Clean(tmpDir + "/prog.log") 32 | 33 | lg := initProg(logFile) 34 | 35 | lg.Println("PROGTEST") 36 | 37 | f, err := os.Open(logFile) 38 | if err != nil { 39 | t.Error("Failed to open log file for reading") 40 | } 41 | defer f.Close() 42 | 43 | scanner := bufio.NewScanner(f) 44 | 45 | for scanner.Scan() { 46 | if strings.HasSuffix(scanner.Text(), "PROGTEST") { 47 | break 48 | } 49 | t.Errorf("got %v, want suffix PROGTEST", scanner.Text()) 50 | } 51 | 52 | } 53 | 54 | func TestInitErr(t *testing.T) { 55 | tmpDir := t.TempDir() 56 | logFile := filepath.Clean(tmpDir + "/prog.log") 57 | 58 | lg := initErr(logFile) 59 | 60 | lg.Println("ERRTEST") 61 | 62 | f, err := os.Open(logFile) 63 | if err != nil { 64 | t.Error("Failed to open log file for reading") 65 | } 66 | defer f.Close() 67 | 68 | scanner := bufio.NewScanner(f) 69 | 70 | for scanner.Scan() { 71 | if strings.HasSuffix(scanner.Text(), "ERRTEST") { 72 | break 73 | } 74 | t.Errorf("got %v, want suffix ERRTEST", scanner.Text()) 75 | } 76 | 77 | } 78 | 79 | func TestInitRep(t *testing.T) { 80 | tmpDir := t.TempDir() 81 | logFile := filepath.Clean(tmpDir + "/prog.log") 82 | 83 | lg := initRep(logFile) 84 | 85 | lg.Println("REPTEST") 86 | 87 | f, err := os.Open(logFile) 88 | if err != nil { 89 | t.Error("Failed to open log file for reading") 90 | } 91 | defer f.Close() 92 | 93 | scanner := bufio.NewScanner(f) 94 | 95 | for scanner.Scan() { 96 | if strings.HasSuffix(scanner.Text(), "REPTEST") { 97 | break 98 | } 99 | t.Errorf("got %v, want suffix REPTEST", scanner.Text()) 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /cmd/sequentially-generate-planet-mbtiles/sequentiallygenerateplanetmbtiles.go: -------------------------------------------------------------------------------- 1 | package sequentiallygenerateplanetmbtiles 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "log" 8 | "math" 9 | "os" 10 | "os/signal" 11 | "path/filepath" 12 | "runtime" 13 | "strings" 14 | "syscall" 15 | 16 | "github.com/lambdajack/sequentially-generate-planet-mbtiles/internal/extract" 17 | "github.com/lambdajack/sequentially-generate-planet-mbtiles/internal/mbtiles" 18 | "github.com/lambdajack/sequentially-generate-planet-mbtiles/internal/planet" 19 | "github.com/lambdajack/sequentially-generate-planet-mbtiles/internal/system" 20 | ) 21 | 22 | const ( 23 | exitOK = 0 24 | exitPermissions = iota + 100 25 | exitReadInput 26 | exitDownloadURL 27 | exitFlags 28 | exitInvalidJSON 29 | exitBuildContainers 30 | ) 31 | 32 | var cfg = &configuration{} 33 | 34 | func init() { 35 | helpMessage() 36 | } 37 | 38 | func EntryPoint(df []byte) int { 39 | initFlags() 40 | 41 | if fl.version { 42 | fmt.Printf("sequentially-generate-planet-mbtiles version %s\n", sgpmVersion) 43 | os.Exit(exitOK) 44 | } 45 | 46 | validateFlags() 47 | 48 | initConfig() 49 | 50 | initDirStructure() 51 | 52 | setTmPaths() 53 | 54 | initLoggers() 55 | 56 | lgInitStr := fmt.Sprintf("sequentially-generate-planet-mbtiles started: %+v\n", cfg) 57 | cfgCompare(lgInitStr) 58 | lg.rep.Printf(lgInitStr) 59 | 60 | cloneRepos() 61 | 62 | setupContainers(df) 63 | 64 | if fl.stage { 65 | lg.rep.Println("stage flag set; staging complete; exiting...") 66 | os.Exit(exitOK) 67 | } 68 | 69 | if !cfg.MergeOnly { 70 | downloadOsmData() 71 | 72 | unzipSourceData() 73 | 74 | moveOcean() 75 | 76 | c := make(chan os.Signal) 77 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 78 | go func() { 79 | <-c 80 | cleanContainers() 81 | os.Exit(1) 82 | }() 83 | defer close(c) 84 | 85 | if !cfg.SkipSlicing { 86 | count := 0 87 | slicingDone := false 88 | 89 | filepath.Walk(pth.pbfSlicesDir, func(path string, info os.FileInfo, err error) error { 90 | if !info.IsDir() && !strings.Contains(path, "converted-") { 91 | count++ 92 | } 93 | if strings.Contains(path, "converted-") { 94 | slicingDone = true 95 | } 96 | return nil 97 | }) 98 | 99 | if count != 0 { 100 | lg.rep.Println("previous progress detected; attempting to continue...") 101 | pbb := extract.IncompleteProgress(cfg.PbfFile, pth.pbfSlicesDir, ct.gdal, lg.err, lg.rep) 102 | if pbb != "" { 103 | np, err := extract.Extract(cfg.PbfFile, filepath.Join(pth.pbfDir, "resume.osm.pbf"), pbb, ct.osmium) 104 | if err != nil || np == "" { 105 | lg.err.Println("failed to continue previous progress; will attempt to continue from scratch... ", err) 106 | } 107 | if np != "" { 108 | cfg.PbfFile = np 109 | filepath.Walk(pth.pbfDir, func(path string, info os.FileInfo, err error) error { 110 | if !info.IsDir() { 111 | if !strings.Contains(path, "slices") && !strings.Contains(path, "resume") { 112 | log.Println("removing dirty files: ", path) 113 | return os.Remove(path) 114 | } 115 | } 116 | return nil 117 | }) 118 | } 119 | } else { 120 | lg.rep.Println("failed to get previous progress; starting from scratch...") 121 | filepath.Walk(pth.pbfDir, func(path string, info os.FileInfo, err error) error { 122 | if !info.IsDir() { 123 | if strings.Contains(path, "resume") || strings.Contains(path, "tmp") { 124 | log.Println("removing dirty files: ", path) 125 | return os.Remove(path) 126 | } 127 | } 128 | return nil 129 | }) 130 | } 131 | } 132 | 133 | if !slicingDone { 134 | lg.rep.Println("slice generation started; there may be significant gaps between logs") 135 | lg.rep.Printf("target file size: %d MB\n", uint64(math.Floor(float64(cfg.MaxRamMb)/15))) 136 | runtime.GC() 137 | extract.TreeSlicer(cfg.PbfFile, pth.pbfSlicesDir, pth.pbfDir, uint64(math.Floor(float64(cfg.MaxRamMb)/15)), ct.gdal, ct.osmium, lg.err, lg.prog, lg.rep) 138 | } else { 139 | lg.rep.Println("slicing already complete; moving on to tile generation") 140 | } 141 | 142 | filepath.Walk(pth.pbfSlicesDir, func(path string, info os.FileInfo, err error) error { 143 | if err != nil { 144 | lg.rep.Fatalf(err.Error()) 145 | } 146 | system.SetUserOwner(path) 147 | if !info.IsDir() { 148 | if !strings.Contains(path, "converted-") { 149 | runtime.GC() 150 | if !cfg.OutAsDir { 151 | mbtiles.Generate(path, pth.mbtilesDir, pth.coastlineDir, pth.landcoverDir, cfg.TilemakerConfig, cfg.TilemakerProcess, cfg.OutAsDir, ct.tilemaker, lg.err, lg.prog, lg.rep) 152 | } else { 153 | mbtiles.Generate(path, pth.outDir, pth.coastlineDir, pth.landcoverDir, cfg.TilemakerConfig, cfg.TilemakerProcess, cfg.OutAsDir, ct.tilemaker, lg.err, lg.prog, lg.rep) 154 | } 155 | os.Rename(path, filepath.Join(filepath.Dir(path), "converted-"+filepath.Base(path))) 156 | } else { 157 | lg.rep.Printf("already converted; skipping %s\n", path) 158 | } 159 | } 160 | return nil 161 | }) 162 | } 163 | } 164 | 165 | final := pth.outDir 166 | 167 | if !cfg.OutAsDir { 168 | runtime.GC() 169 | f := planet.Generate(pth.mbtilesDir, pth.outDir, ct.tippecanoe, lg.err, lg.prog, lg.rep) 170 | final = f 171 | } 172 | 173 | if !cfg.OutAsDir && final == pth.outDir { 174 | lg.rep.Printf("Hmmm - we think you will find success at %s, but we can't quite tell for some reason... Maybe we don't have permission to see?\n", pth.outDir) 175 | } else { 176 | lg.rep.Println("success: ", final) 177 | } 178 | 179 | system.SetUserOwner(final) 180 | 181 | endMessage(final) 182 | 183 | return exitOK 184 | } 185 | 186 | func endMessage(out string) { 187 | fmt.Println(` 188 | __________________________________________________ 189 | | | 190 | | Thank you for using | 191 | | Sequentially Generate Planet Mbtiles!! | 192 | |__________________________________________________| 193 | 194 | 195 | Your carriage awaits you at: ` + out + "\n") 196 | 197 | if !cfg.OutAsDir { 198 | fmt.Printf("TRY: docker run --rm -it -v %s:/data -p 8080:80 maptiler/tileserver-gl\n\n", filepath.Dir(out)) 199 | fmt.Print("REMEMBER: To view the map with proper styles, you may need to set up a frontend with something like Maplibre or Leaflet.js using the correct style.json, rather than using the tileserver-gl's inbuilt 'Viewer'; although the viewer is great for checking that the mbtiles work and you got the area you were expecting.\n\n") 200 | } 201 | fmt.Print("We would love to make this process as easy and reliable as possible for everyone. If you have any feedback, suggestions, or bug reports please come over to https://github.com/lambdajack/sequentially-generate-planet-mbtiles and let us know.\n\n") 202 | } 203 | 204 | func cfgCompare(lgInitStr string) { 205 | f, err := os.Open(filepath.Join(pth.logsDir, "rep.log")) 206 | if err != nil { 207 | lg.err.Println("unable to open log file: ", filepath.Join(pth.logsDir, "rep.log")) 208 | return 209 | } 210 | s, err := io.ReadAll(f) 211 | if err != nil { 212 | lg.err.Println("unable to read log file: ", filepath.Join(pth.logsDir, "rep.log")) 213 | return 214 | } 215 | idx := strings.LastIndex(string(s), "sequentially-generate-planet-mbtiles started: ") 216 | if idx > 0 { 217 | ss := strings.Split(string(s)[idx:], "\n") 218 | if strings.TrimSpace(ss[0]) != strings.TrimSpace(lgInitStr) { 219 | lg.rep.Println("resumption of a previously stopped execution with different parameters detected") 220 | fmt.Println("UNKNOWN THINGS CAN HAPPEN WHEN RESUMING WITH DIFFERENT PARAMETERS. You should only proceed if you are confident that your changed parameters will have no adverse affect on the end result or are prepared to accept the outcome; otherwise you should clean the working/output directories and start again (you can save and replace downloaded files if requried). DO YOU WISH TO CONTINUE? yes/no") 221 | r := bufio.NewReader(os.Stdin) 222 | a, err := r.ReadString('\n') 223 | if err != nil { 224 | lg.err.Fatal("unable to read answer to resumption question; aborting; recommend starting fresh") 225 | } 226 | if a != "yes\n" && a != "y\n" { 227 | os.Exit(exitOK) 228 | } 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /cmd/sequentially-generate-planet-mbtiles/sequentiallygenerateplanetmbtiles_test.go: -------------------------------------------------------------------------------- 1 | package sequentiallygenerateplanetmbtiles 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "testing" 9 | ) 10 | 11 | func init() { 12 | // Ensure that test flags match the ones in EntryPoint() 13 | 14 | flag.BoolVar(&fl.version, "v", false, "") 15 | flag.BoolVar(&fl.version, "version", false, "") 16 | 17 | flag.BoolVar(&fl.stage, "s", false, "") 18 | flag.BoolVar(&fl.stage, "stage", false, "") 19 | 20 | flag.StringVar(&fl.config, "c", "", "") 21 | flag.StringVar(&fl.config, "config", "", "") 22 | 23 | flag.BoolVar(&fl.test, "t", false, "") 24 | flag.BoolVar(&fl.test, "test", false, "") 25 | 26 | flag.StringVar(&fl.pbfFile, "p", "", "") 27 | flag.StringVar(&fl.pbfFile, "pbf-file", "", "") 28 | 29 | flag.StringVar(&fl.workingDir, "w", "data", "") 30 | flag.StringVar(&fl.workingDir, "working-dir", "data", "") 31 | 32 | flag.StringVar(&fl.outDir, "o", "data/out", "") 33 | flag.StringVar(&fl.outDir, "outdir", "data/out", "") 34 | 35 | flag.BoolVar(&fl.excludeOcean, "eo", false, "") 36 | flag.BoolVar(&fl.excludeOcean, "exclude-ocean", false, "") 37 | 38 | flag.BoolVar(&fl.excludeLanduse, "el", false, "") 39 | flag.BoolVar(&fl.excludeLanduse, "exclude-landuse", false, "") 40 | 41 | flag.StringVar(&fl.tilemakerConfig, "tc", "", "") 42 | flag.StringVar(&fl.tilemakerConfig, "tilemaker-config", "", "") 43 | 44 | flag.StringVar(&fl.tilemakerProcess, "tp", "", "") 45 | flag.StringVar(&fl.tilemakerProcess, "tilemaker-process", "", "") 46 | 47 | flag.Uint64Var(&fl.maxRamMb, "r", 0, "") 48 | flag.Uint64Var(&fl.maxRamMb, "ram", 0, "") 49 | 50 | flag.BoolVar(&fl.outAsDir, "od", false, "") 51 | flag.BoolVar(&fl.outAsDir, "out-as-dir", false, "") 52 | 53 | flag.BoolVar(&fl.skipSlicing, "ss", false, "") 54 | flag.BoolVar(&fl.skipSlicing, "skip-slicing", false, "") 55 | 56 | flag.BoolVar(&fl.mergeOnly, "mo", false, "") 57 | flag.BoolVar(&fl.mergeOnly, "merge-only", false, "") 58 | 59 | flag.BoolVar(&fl.skipDownload, "sd", false, "") 60 | flag.BoolVar(&fl.skipDownload, "skip-download", false, "") 61 | 62 | } 63 | 64 | func TestEntryPoint(t *testing.T) { 65 | fmt.Println("IMPLEMENT ENTRY POINT TEST") 66 | } 67 | 68 | func TestValidateFlags(t *testing.T) { 69 | 70 | if os.Getenv("TEST_VALIDATE_FLAGS") == "1" { 71 | os.Args = append(os.Args, "-c", "/into/the/unknown", "-eo") 72 | flag.Parse() 73 | validateFlags() 74 | return 75 | } 76 | 77 | cmd := exec.Command(os.Args[0], "-test.run=TestValidateFlags") 78 | cmd.Env = append(os.Environ(), "TEST_VALIDATE_FLAGS=1") 79 | err := cmd.Run() 80 | 81 | if e, ok := err.(*exec.ExitError); ok && !e.Success() { 82 | if e.ExitCode() == exitFlags { 83 | return 84 | } 85 | } 86 | 87 | t.Fatalf("process ran with err %v, want exit status %v", err, exitFlags) 88 | } 89 | -------------------------------------------------------------------------------- /cmd/sequentially-generate-planet-mbtiles/unzip.go: -------------------------------------------------------------------------------- 1 | package sequentiallygenerateplanetmbtiles 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | 7 | "github.com/lambdajack/lj_go/pkg/lj_archive" 8 | ) 9 | 10 | type unzipInformation struct { 11 | srcPath string 12 | destPath string 13 | } 14 | 15 | func unzipSourceData() { 16 | var unzipInfo = []unzipInformation{ 17 | {srcPath: filepath.Join(pth.workingDir, "water-polygons-split-4326.zip"), destPath: pth.coastlineDir}, 18 | {srcPath: filepath.Join(pth.workingDir, "ne_10m_urban_areas.zip"), destPath: pth.landCoverUrbanDepth}, 19 | {srcPath: filepath.Join(pth.workingDir, "ne_10m_antarctic_ice_shelves_polys.zip"), destPath: pth.landCoverIceShelvesDepth}, 20 | {srcPath: filepath.Join(pth.workingDir, "ne_10m_glaciated_areas.zip"), destPath: pth.landCoverGlaciatedDepth}, 21 | } 22 | 23 | for _, info := range unzipInfo { 24 | if cfg.ExcludeOcean && strings.Contains(info.srcPath, "water") { 25 | continue 26 | } 27 | 28 | if cfg.ExcludeLanduse && strings.Contains(info.srcPath, "ne_") { 29 | continue 30 | } 31 | 32 | lg.rep.Println("unzipping", info.srcPath) 33 | lj_archive.Unzip(info.srcPath, info.destPath) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cmd/sequentially-generate-planet-mbtiles/version.go: -------------------------------------------------------------------------------- 1 | package sequentiallygenerateplanetmbtiles 2 | 3 | // Automatically set at compile time 4 | var sgpmVersion string 5 | -------------------------------------------------------------------------------- /configs/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "pbfFile": "", 3 | "workingDir": "", 4 | "outDir": "", 5 | "excludeOcean": false, 6 | "excludeLanduse": false, 7 | "TilemakerConfig": "", 8 | "TilemakerProcess": "", 9 | "maxRamMb": 0, 10 | "outAsDir": false, 11 | "skipSlicing": false, 12 | "mergeOnly": false, 13 | "skipDownload": false 14 | } 15 | -------------------------------------------------------------------------------- /configs/styles/sgpm-basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "Basic preview", 4 | "metadata": { "openmaptiles:version": "3.x" }, 5 | "sources": { 6 | "openmaptiles": { 7 | "type": "vector", 8 | "url": "http://localhost:8080/data/v3.json" 9 | } 10 | }, 11 | "glyphs": "http://localhost:8080/fonts/{fontstack}/{range}.pbf", 12 | "layers": [ 13 | { 14 | "id": "background", 15 | "paint": { "background-color": "hsl(47, 26%, 88%)" }, 16 | "type": "background" 17 | }, 18 | { 19 | "filter": [ 20 | "all", 21 | ["==", "$type", "Polygon"], 22 | ["in", "class", "residential", "suburb", "neighbourhood"] 23 | ], 24 | "id": "landuse-residential", 25 | "paint": { "fill-color": "hsl(47, 13%, 86%)", "fill-opacity": 0.7 }, 26 | "source": "openmaptiles", 27 | "source-layer": "landuse", 28 | "type": "fill" 29 | }, 30 | { 31 | "filter": ["==", "class", "grass"], 32 | "id": "landcover_grass", 33 | "paint": { "fill-color": "hsl(82, 46%, 72%)", "fill-opacity": 0.45 }, 34 | "source": "openmaptiles", 35 | "source-layer": "landcover", 36 | "type": "fill" 37 | }, 38 | { 39 | "filter": ["==", "class", "wood"], 40 | "id": "landcover_wood", 41 | "paint": { 42 | "fill-color": "hsl(82, 46%, 72%)", 43 | "fill-opacity": { 44 | "base": 1, 45 | "stops": [ 46 | [8, 0.6], 47 | [22, 1] 48 | ] 49 | } 50 | }, 51 | "source": "openmaptiles", 52 | "source-layer": "landcover", 53 | "type": "fill" 54 | }, 55 | { 56 | "filter": ["all", ["==", "$type", "Polygon"], ["!=", "intermittent", 1]], 57 | "id": "water", 58 | "paint": { "fill-color": "hsl(205, 56%, 73%)" }, 59 | "source": "openmaptiles", 60 | "source-layer": "water", 61 | "type": "fill" 62 | }, 63 | { 64 | "filter": ["all", ["==", "$type", "Polygon"], ["==", "intermittent", 1]], 65 | "id": "water_intermittent", 66 | "paint": { "fill-color": "hsl(205, 56%, 73%)", "fill-opacity": 0.7 }, 67 | "source": "openmaptiles", 68 | "source-layer": "water", 69 | "type": "fill" 70 | }, 71 | { 72 | "filter": ["==", "subclass", "ice_shelf"], 73 | "id": "landcover-ice-shelf", 74 | "paint": { "fill-color": "hsl(47, 26%, 88%)", "fill-opacity": 0.8 }, 75 | "source": "openmaptiles", 76 | "source-layer": "landcover", 77 | "type": "fill" 78 | }, 79 | { 80 | "filter": ["==", "subclass", "glacier"], 81 | "id": "landcover-glacier", 82 | "paint": { 83 | "fill-color": "hsl(47, 22%, 94%)", 84 | "fill-opacity": { 85 | "base": 1, 86 | "stops": [ 87 | [0, 1], 88 | [8, 0.5] 89 | ] 90 | } 91 | }, 92 | "source": "openmaptiles", 93 | "source-layer": "landcover", 94 | "type": "fill" 95 | }, 96 | { 97 | "filter": ["all", ["in", "class", "sand"]], 98 | "id": "landcover_sand", 99 | "metadata": {}, 100 | "paint": { 101 | "fill-antialias": false, 102 | "fill-color": "rgba(232, 214, 38, 1)", 103 | "fill-opacity": 0.3 104 | }, 105 | "source": "openmaptiles", 106 | "source-layer": "landcover", 107 | "type": "fill" 108 | }, 109 | { 110 | "filter": ["==", "class", "agriculture"], 111 | "id": "landuse", 112 | "paint": { "fill-color": "#eae0d0" }, 113 | "source": "openmaptiles", 114 | "source-layer": "landuse", 115 | "type": "fill" 116 | }, 117 | { 118 | "filter": ["==", "class", "national_park"], 119 | "id": "landuse_overlay_national_park", 120 | "paint": { 121 | "fill-color": "#E1EBB0", 122 | "fill-opacity": { 123 | "base": 1, 124 | "stops": [ 125 | [5, 0], 126 | [9, 0.75] 127 | ] 128 | } 129 | }, 130 | "source": "openmaptiles", 131 | "source-layer": "landcover", 132 | "type": "fill" 133 | }, 134 | { 135 | "filter": [ 136 | "all", 137 | ["==", "$type", "LineString"], 138 | ["==", "brunnel", "tunnel"] 139 | ], 140 | "id": "waterway-tunnel", 141 | "paint": { 142 | "line-color": "hsl(205, 56%, 73%)", 143 | "line-dasharray": [3, 3], 144 | "line-gap-width": { 145 | "stops": [ 146 | [12, 0], 147 | [20, 6] 148 | ] 149 | }, 150 | "line-opacity": 1, 151 | "line-width": { 152 | "base": 1.4, 153 | "stops": [ 154 | [8, 1], 155 | [20, 2] 156 | ] 157 | } 158 | }, 159 | "source": "openmaptiles", 160 | "source-layer": "waterway", 161 | "type": "line" 162 | }, 163 | { 164 | "filter": [ 165 | "all", 166 | ["==", "$type", "LineString"], 167 | ["!in", "brunnel", "tunnel", "bridge"], 168 | ["!=", "intermittent", 1] 169 | ], 170 | "id": "waterway", 171 | "paint": { 172 | "line-color": "hsl(205, 56%, 73%)", 173 | "line-opacity": 1, 174 | "line-width": { 175 | "base": 1.4, 176 | "stops": [ 177 | [8, 1], 178 | [20, 8] 179 | ] 180 | } 181 | }, 182 | "source": "openmaptiles", 183 | "source-layer": "waterway", 184 | "type": "line" 185 | }, 186 | { 187 | "filter": [ 188 | "all", 189 | ["==", "$type", "LineString"], 190 | ["!in", "brunnel", "tunnel", "bridge"], 191 | ["==", "intermittent", 1] 192 | ], 193 | "id": "waterway_intermittent", 194 | "paint": { 195 | "line-color": "hsl(205, 56%, 73%)", 196 | "line-opacity": 1, 197 | "line-width": { 198 | "base": 1.4, 199 | "stops": [ 200 | [8, 1], 201 | [20, 8] 202 | ] 203 | }, 204 | "line-dasharray": [2, 1] 205 | }, 206 | "source": "openmaptiles", 207 | "source-layer": "waterway", 208 | "type": "line" 209 | }, 210 | { 211 | "filter": [ 212 | "all", 213 | ["==", "$type", "LineString"], 214 | ["==", "brunnel", "tunnel"], 215 | ["==", "class", "transit"] 216 | ], 217 | "id": "tunnel_railway_transit", 218 | "layout": { "line-cap": "butt", "line-join": "miter" }, 219 | "minzoom": 0, 220 | "paint": { 221 | "line-color": "hsl(34, 12%, 66%)", 222 | "line-dasharray": [3, 3], 223 | "line-opacity": { 224 | "base": 1, 225 | "stops": [ 226 | [11, 0], 227 | [16, 1] 228 | ] 229 | } 230 | }, 231 | "source": "openmaptiles", 232 | "source-layer": "transportation", 233 | "type": "line" 234 | }, 235 | { 236 | "id": "building", 237 | "paint": { 238 | "fill-antialias": true, 239 | "fill-color": "rgba(222, 211, 190, 1)", 240 | "fill-opacity": { 241 | "base": 1, 242 | "stops": [ 243 | [13, 0], 244 | [15, 1] 245 | ] 246 | }, 247 | "fill-outline-color": { 248 | "stops": [ 249 | [15, "rgba(212, 177, 146, 0)"], 250 | [16, "rgba(212, 177, 146, 0.5)"] 251 | ] 252 | } 253 | }, 254 | "source": "openmaptiles", 255 | "source-layer": "building", 256 | "type": "fill" 257 | }, 258 | { 259 | "filter": ["==", "$type", "Point"], 260 | "id": "housenumber", 261 | "layout": { 262 | "text-field": "{housenumber}", 263 | "text-font": ["Noto Sans Regular"], 264 | "text-size": 10 265 | }, 266 | "minzoom": 17, 267 | "paint": { "text-color": "rgba(212, 177, 146, 1)" }, 268 | "source": "openmaptiles", 269 | "source-layer": "housenumber", 270 | "type": "symbol" 271 | }, 272 | { 273 | "id": "road_area_pier", 274 | "type": "fill", 275 | "metadata": {}, 276 | "source": "openmaptiles", 277 | "source-layer": "transportation", 278 | "filter": ["all", ["==", "$type", "Polygon"], ["==", "class", "pier"]], 279 | "paint": { "fill-color": "hsl(47, 26%, 88%)", "fill-antialias": true } 280 | }, 281 | { 282 | "id": "road_pier", 283 | "type": "line", 284 | "metadata": {}, 285 | "source": "openmaptiles", 286 | "source-layer": "transportation", 287 | "filter": ["all", ["==", "$type", "LineString"], ["in", "class", "pier"]], 288 | "layout": { "line-cap": "round", "line-join": "round" }, 289 | "paint": { 290 | "line-color": "hsl(47, 26%, 88%)", 291 | "line-width": { 292 | "base": 1.2, 293 | "stops": [ 294 | [15, 1], 295 | [17, 4] 296 | ] 297 | } 298 | } 299 | }, 300 | { 301 | "filter": [ 302 | "all", 303 | ["==", "$type", "Polygon"], 304 | ["in", "brunnel", "bridge"] 305 | ], 306 | "id": "road_bridge_area", 307 | "layout": {}, 308 | "paint": { "fill-color": "hsl(47, 26%, 88%)", "fill-opacity": 0.5 }, 309 | "source": "openmaptiles", 310 | "source-layer": "transportation", 311 | "type": "fill" 312 | }, 313 | { 314 | "filter": [ 315 | "all", 316 | ["==", "$type", "LineString"], 317 | ["in", "class", "path", "track"] 318 | ], 319 | "id": "road_path", 320 | "layout": { "line-cap": "square", "line-join": "bevel" }, 321 | "paint": { 322 | "line-color": "hsl(0, 0%, 97%)", 323 | "line-dasharray": [1, 1], 324 | "line-width": { 325 | "base": 1.55, 326 | "stops": [ 327 | [4, 0.25], 328 | [20, 10] 329 | ] 330 | } 331 | }, 332 | "source": "openmaptiles", 333 | "source-layer": "transportation", 334 | "type": "line" 335 | }, 336 | { 337 | "filter": [ 338 | "all", 339 | ["==", "$type", "LineString"], 340 | ["in", "class", "minor", "service"] 341 | ], 342 | "id": "road_minor", 343 | "layout": { "line-cap": "round", "line-join": "round" }, 344 | "paint": { 345 | "line-color": "hsl(0, 0%, 97%)", 346 | "line-width": { 347 | "base": 1.55, 348 | "stops": [ 349 | [4, 0.25], 350 | [20, 30] 351 | ] 352 | } 353 | }, 354 | "source": "openmaptiles", 355 | "source-layer": "transportation", 356 | "type": "line", 357 | "minzoom": 13 358 | }, 359 | { 360 | "filter": [ 361 | "all", 362 | ["==", "$type", "LineString"], 363 | ["==", "brunnel", "tunnel"], 364 | ["==", "class", "minor_road"] 365 | ], 366 | "id": "tunnel_minor", 367 | "layout": { "line-cap": "butt", "line-join": "miter" }, 368 | "paint": { 369 | "line-color": "#efefef", 370 | "line-dasharray": [0.36, 0.18], 371 | "line-width": { 372 | "base": 1.55, 373 | "stops": [ 374 | [4, 0.25], 375 | [20, 30] 376 | ] 377 | } 378 | }, 379 | "source": "openmaptiles", 380 | "source-layer": "transportation", 381 | "type": "line" 382 | }, 383 | { 384 | "filter": [ 385 | "all", 386 | ["==", "$type", "LineString"], 387 | ["==", "brunnel", "tunnel"], 388 | ["in", "class", "primary", "secondary", "tertiary", "trunk"] 389 | ], 390 | "id": "tunnel_major", 391 | "layout": { "line-cap": "butt", "line-join": "miter" }, 392 | "paint": { 393 | "line-color": "#fff", 394 | "line-dasharray": [0.28, 0.14], 395 | "line-width": { 396 | "base": 1.4, 397 | "stops": [ 398 | [6, 0.5], 399 | [20, 30] 400 | ] 401 | } 402 | }, 403 | "source": "openmaptiles", 404 | "source-layer": "transportation", 405 | "type": "line" 406 | }, 407 | { 408 | "filter": [ 409 | "all", 410 | ["==", "$type", "Polygon"], 411 | ["in", "class", "runway", "taxiway"] 412 | ], 413 | "id": "aeroway-area", 414 | "metadata": { "mapbox:group": "1444849345966.4436" }, 415 | "minzoom": 4, 416 | "paint": { 417 | "fill-color": "rgba(255, 255, 255, 1)", 418 | "fill-opacity": { 419 | "base": 1, 420 | "stops": [ 421 | [13, 0], 422 | [14, 1] 423 | ] 424 | } 425 | }, 426 | "source": "openmaptiles", 427 | "source-layer": "aeroway", 428 | "type": "fill" 429 | }, 430 | { 431 | "filter": [ 432 | "all", 433 | ["in", "class", "taxiway"], 434 | ["==", "$type", "LineString"] 435 | ], 436 | "id": "aeroway-taxiway", 437 | "layout": { "line-cap": "round", "line-join": "round" }, 438 | "metadata": { "mapbox:group": "1444849345966.4436" }, 439 | "minzoom": 12, 440 | "paint": { 441 | "line-color": "rgba(255, 255, 255, 1)", 442 | "line-opacity": 1, 443 | "line-width": { 444 | "base": 1.5, 445 | "stops": [ 446 | [12, 1], 447 | [17, 10] 448 | ] 449 | } 450 | }, 451 | "source": "openmaptiles", 452 | "source-layer": "aeroway", 453 | "type": "line" 454 | }, 455 | { 456 | "filter": [ 457 | "all", 458 | ["in", "class", "runway"], 459 | ["==", "$type", "LineString"] 460 | ], 461 | "id": "aeroway-runway", 462 | "layout": { "line-cap": "round", "line-join": "round" }, 463 | "metadata": { "mapbox:group": "1444849345966.4436" }, 464 | "minzoom": 4, 465 | "paint": { 466 | "line-color": "rgba(255, 255, 255, 1)", 467 | "line-opacity": 1, 468 | "line-width": { 469 | "base": 1.5, 470 | "stops": [ 471 | [11, 4], 472 | [17, 50] 473 | ] 474 | } 475 | }, 476 | "source": "openmaptiles", 477 | "source-layer": "aeroway", 478 | "type": "line" 479 | }, 480 | { 481 | "filter": [ 482 | "all", 483 | ["==", "$type", "LineString"], 484 | ["in", "class", "trunk", "primary"] 485 | ], 486 | "id": "road_trunk_primary", 487 | "layout": { "line-cap": "round", "line-join": "round" }, 488 | "paint": { 489 | "line-color": "#fff", 490 | "line-width": { 491 | "base": 1.4, 492 | "stops": [ 493 | [6, 0.5], 494 | [20, 30] 495 | ] 496 | } 497 | }, 498 | "source": "openmaptiles", 499 | "source-layer": "transportation", 500 | "type": "line" 501 | }, 502 | { 503 | "filter": [ 504 | "all", 505 | ["==", "$type", "LineString"], 506 | ["in", "class", "secondary", "tertiary"] 507 | ], 508 | "id": "road_secondary_tertiary", 509 | "layout": { "line-cap": "round", "line-join": "round" }, 510 | "paint": { 511 | "line-color": "#fff", 512 | "line-width": { 513 | "base": 1.4, 514 | "stops": [ 515 | [6, 0.5], 516 | [20, 20] 517 | ] 518 | } 519 | }, 520 | "source": "openmaptiles", 521 | "source-layer": "transportation", 522 | "type": "line" 523 | }, 524 | { 525 | "filter": [ 526 | "all", 527 | ["==", "$type", "LineString"], 528 | ["==", "class", "motorway"] 529 | ], 530 | "id": "road_major_motorway", 531 | "layout": { "line-cap": "round", "line-join": "round" }, 532 | "paint": { 533 | "line-color": "hsl(0, 0%, 100%)", 534 | "line-offset": 0, 535 | "line-width": { 536 | "base": 1.4, 537 | "stops": [ 538 | [8, 1], 539 | [16, 10] 540 | ] 541 | } 542 | }, 543 | "source": "openmaptiles", 544 | "source-layer": "transportation", 545 | "type": "line" 546 | }, 547 | { 548 | "filter": [ 549 | "all", 550 | ["==", "class", "transit"], 551 | ["!=", "brunnel", "tunnel"] 552 | ], 553 | "id": "railway-transit", 554 | "paint": { 555 | "line-color": "hsl(34, 12%, 66%)", 556 | "line-opacity": { 557 | "base": 1, 558 | "stops": [ 559 | [11, 0], 560 | [16, 1] 561 | ] 562 | } 563 | }, 564 | "source": "openmaptiles", 565 | "source-layer": "transportation", 566 | "type": "line" 567 | }, 568 | { 569 | "filter": ["==", "class", "rail"], 570 | "id": "railway", 571 | "paint": { 572 | "line-color": "hsl(34, 12%, 66%)", 573 | "line-opacity": { 574 | "base": 1, 575 | "stops": [ 576 | [11, 0], 577 | [16, 1] 578 | ] 579 | } 580 | }, 581 | "source": "openmaptiles", 582 | "source-layer": "transportation", 583 | "type": "line" 584 | }, 585 | { 586 | "filter": [ 587 | "all", 588 | ["==", "$type", "LineString"], 589 | ["==", "brunnel", "bridge"] 590 | ], 591 | "id": "waterway-bridge-case", 592 | "layout": { "line-cap": "butt", "line-join": "miter" }, 593 | "paint": { 594 | "line-color": "#bbbbbb", 595 | "line-gap-width": { 596 | "base": 1.55, 597 | "stops": [ 598 | [4, 0.25], 599 | [20, 30] 600 | ] 601 | }, 602 | "line-width": { 603 | "base": 1.6, 604 | "stops": [ 605 | [12, 0.5], 606 | [20, 10] 607 | ] 608 | } 609 | }, 610 | "source": "openmaptiles", 611 | "source-layer": "waterway", 612 | "type": "line" 613 | }, 614 | { 615 | "filter": [ 616 | "all", 617 | ["==", "$type", "LineString"], 618 | ["==", "brunnel", "bridge"] 619 | ], 620 | "id": "waterway-bridge", 621 | "layout": { "line-cap": "round", "line-join": "round" }, 622 | "paint": { 623 | "line-color": "hsl(205, 56%, 73%)", 624 | "line-width": { 625 | "base": 1.55, 626 | "stops": [ 627 | [4, 0.25], 628 | [20, 30] 629 | ] 630 | } 631 | }, 632 | "source": "openmaptiles", 633 | "source-layer": "waterway", 634 | "type": "line" 635 | }, 636 | { 637 | "filter": [ 638 | "all", 639 | ["==", "$type", "LineString"], 640 | ["==", "brunnel", "bridge"], 641 | ["==", "class", "minor_road"] 642 | ], 643 | "id": "bridge_minor case", 644 | "layout": { "line-cap": "butt", "line-join": "miter" }, 645 | "paint": { 646 | "line-color": "#dedede", 647 | "line-gap-width": { 648 | "base": 1.55, 649 | "stops": [ 650 | [4, 0.25], 651 | [20, 30] 652 | ] 653 | }, 654 | "line-width": { 655 | "base": 1.6, 656 | "stops": [ 657 | [12, 0.5], 658 | [20, 10] 659 | ] 660 | } 661 | }, 662 | "source": "openmaptiles", 663 | "source-layer": "transportation", 664 | "type": "line" 665 | }, 666 | { 667 | "filter": [ 668 | "all", 669 | ["==", "$type", "LineString"], 670 | ["==", "brunnel", "bridge"], 671 | ["in", "class", "primary", "secondary", "tertiary", "trunk"] 672 | ], 673 | "id": "bridge_major case", 674 | "layout": { "line-cap": "butt", "line-join": "miter" }, 675 | "paint": { 676 | "line-color": "#dedede", 677 | "line-gap-width": { 678 | "base": 1.55, 679 | "stops": [ 680 | [4, 0.25], 681 | [20, 30] 682 | ] 683 | }, 684 | "line-width": { 685 | "base": 1.6, 686 | "stops": [ 687 | [12, 0.5], 688 | [20, 10] 689 | ] 690 | } 691 | }, 692 | "source": "openmaptiles", 693 | "source-layer": "transportation", 694 | "type": "line" 695 | }, 696 | { 697 | "filter": [ 698 | "all", 699 | ["==", "$type", "LineString"], 700 | ["==", "brunnel", "bridge"], 701 | ["==", "class", "minor_road"] 702 | ], 703 | "id": "bridge_minor", 704 | "layout": { "line-cap": "round", "line-join": "round" }, 705 | "paint": { 706 | "line-color": "#efefef", 707 | "line-width": { 708 | "base": 1.55, 709 | "stops": [ 710 | [4, 0.25], 711 | [20, 30] 712 | ] 713 | } 714 | }, 715 | "source": "openmaptiles", 716 | "source-layer": "transportation", 717 | "type": "line" 718 | }, 719 | { 720 | "filter": [ 721 | "all", 722 | ["==", "$type", "LineString"], 723 | ["==", "brunnel", "bridge"], 724 | ["in", "class", "primary", "secondary", "tertiary", "trunk"] 725 | ], 726 | "id": "bridge_major", 727 | "layout": { "line-cap": "round", "line-join": "round" }, 728 | "paint": { 729 | "line-color": "#fff", 730 | "line-width": { 731 | "base": 1.4, 732 | "stops": [ 733 | [6, 0.5], 734 | [20, 30] 735 | ] 736 | } 737 | }, 738 | "source": "openmaptiles", 739 | "source-layer": "transportation", 740 | "type": "line" 741 | }, 742 | { 743 | "filter": ["in", "admin_level", 4, 6, 8], 744 | "id": "admin_sub", 745 | "paint": { 746 | "line-color": "hsla(0, 0%, 60%, 0.5)", 747 | "line-dasharray": [2, 1] 748 | }, 749 | "source": "openmaptiles", 750 | "source-layer": "boundary", 751 | "type": "line" 752 | }, 753 | { 754 | "filter": [ 755 | "all", 756 | ["<=", "admin_level", 2], 757 | ["==", "$type", "LineString"] 758 | ], 759 | "id": "admin_country", 760 | "layout": { "line-cap": "round", "line-join": "round" }, 761 | "paint": { 762 | "line-color": "hsl(0, 0%, 60%)", 763 | "line-width": { 764 | "base": 1.3, 765 | "stops": [ 766 | [3, 0.5], 767 | [22, 15] 768 | ] 769 | } 770 | }, 771 | "source": "openmaptiles", 772 | "source-layer": "boundary", 773 | "type": "line" 774 | }, 775 | { 776 | "filter": ["all", ["==", "$type", "Point"], ["==", "rank", 1]], 777 | "id": "poi_label", 778 | "layout": { 779 | "icon-size": 1, 780 | "text-anchor": "top", 781 | "text-field": "{name}", 782 | "text-font": ["Noto Sans Regular"], 783 | "text-max-width": 8, 784 | "text-offset": [0, 0.5], 785 | "text-size": 11 786 | }, 787 | "minzoom": 14, 788 | "paint": { 789 | "text-color": "#666", 790 | "text-halo-blur": 1, 791 | "text-halo-color": "rgba(255,255,255,0.75)", 792 | "text-halo-width": 1 793 | }, 794 | "source": "openmaptiles", 795 | "source-layer": "poi", 796 | "type": "symbol" 797 | }, 798 | { 799 | "filter": ["all", ["has", "iata"]], 800 | "id": "airport-label", 801 | "layout": { 802 | "icon-size": 1, 803 | "text-anchor": "top", 804 | "text-field": "{name}", 805 | "text-font": ["Noto Sans Regular"], 806 | "text-max-width": 8, 807 | "text-offset": [0, 0.5], 808 | "text-size": 11 809 | }, 810 | "minzoom": 10, 811 | "paint": { 812 | "text-color": "#666", 813 | "text-halo-blur": 1, 814 | "text-halo-color": "rgba(255,255,255,0.75)", 815 | "text-halo-width": 1 816 | }, 817 | "source": "openmaptiles", 818 | "source-layer": "aerodrome_label", 819 | "type": "symbol" 820 | }, 821 | { 822 | "filter": ["==", "$type", "LineString"], 823 | "id": "road_major_label", 824 | "layout": { 825 | "symbol-placement": "line", 826 | "text-field": "{name}", 827 | "text-font": ["Noto Sans Regular"], 828 | "text-letter-spacing": 0.1, 829 | "text-rotation-alignment": "map", 830 | "text-size": { 831 | "base": 1.4, 832 | "stops": [ 833 | [10, 8], 834 | [20, 14] 835 | ] 836 | }, 837 | "text-transform": "uppercase" 838 | }, 839 | "paint": { 840 | "text-color": "#000", 841 | "text-halo-color": "hsl(0, 0%, 100%)", 842 | "text-halo-width": 2 843 | }, 844 | "source": "openmaptiles", 845 | "source-layer": "transportation_name", 846 | "type": "symbol" 847 | }, 848 | { 849 | "filter": [ 850 | "all", 851 | ["==", "$type", "Point"], 852 | ["!in", "class", "city", "state", "country", "continent"] 853 | ], 854 | "id": "place_label_other", 855 | "layout": { 856 | "text-anchor": "center", 857 | "text-field": "{name}", 858 | "text-font": ["Noto Sans Regular"], 859 | "text-max-width": 6, 860 | "text-size": { 861 | "stops": [ 862 | [6, 10], 863 | [12, 14] 864 | ] 865 | } 866 | }, 867 | "minzoom": 8, 868 | "paint": { 869 | "text-color": "hsl(0, 0%, 25%)", 870 | "text-halo-blur": 0, 871 | "text-halo-color": "hsl(0, 0%, 100%)", 872 | "text-halo-width": 2 873 | }, 874 | "source": "openmaptiles", 875 | "source-layer": "place", 876 | "type": "symbol" 877 | }, 878 | { 879 | "filter": ["all", ["==", "$type", "Point"], ["==", "class", "city"]], 880 | "id": "place_label_city", 881 | "layout": { 882 | "text-field": "{name}", 883 | "text-font": ["Noto Sans Regular"], 884 | "text-max-width": 10, 885 | "text-size": { 886 | "stops": [ 887 | [3, 12], 888 | [8, 16] 889 | ] 890 | } 891 | }, 892 | "maxzoom": 16, 893 | "paint": { 894 | "text-color": "hsl(0, 0%, 0%)", 895 | "text-halo-blur": 0, 896 | "text-halo-color": "hsla(0, 0%, 100%, 0.75)", 897 | "text-halo-width": 2 898 | }, 899 | "source": "openmaptiles", 900 | "source-layer": "place", 901 | "type": "symbol" 902 | }, 903 | { 904 | "filter": [ 905 | "all", 906 | ["==", "$type", "Point"], 907 | ["==", "class", "country"], 908 | ["!has", "iso_a2"] 909 | ], 910 | "id": "country_label-other", 911 | "layout": { 912 | "text-field": "{name}", 913 | "text-font": ["Noto Sans Regular"], 914 | "text-max-width": 10, 915 | "text-size": { 916 | "stops": [ 917 | [3, 12], 918 | [8, 22] 919 | ] 920 | } 921 | }, 922 | "maxzoom": 12, 923 | "paint": { 924 | "text-color": "hsl(0, 0%, 13%)", 925 | "text-halo-blur": 0, 926 | "text-halo-color": "rgba(255,255,255,0.75)", 927 | "text-halo-width": 2 928 | }, 929 | "source": "openmaptiles", 930 | "source-layer": "place", 931 | "type": "symbol" 932 | }, 933 | { 934 | "filter": [ 935 | "all", 936 | ["==", "$type", "Point"], 937 | ["==", "class", "country"], 938 | ["has", "iso_a2"] 939 | ], 940 | "id": "country_label", 941 | "layout": { 942 | "text-field": "{name}", 943 | "text-font": ["Noto Sans Regular"], 944 | "text-max-width": 10, 945 | "text-size": { 946 | "stops": [ 947 | [3, 12], 948 | [8, 22] 949 | ] 950 | } 951 | }, 952 | "maxzoom": 12, 953 | "paint": { 954 | "text-color": "hsl(0, 0%, 13%)", 955 | "text-halo-blur": 0, 956 | "text-halo-color": "rgba(255,255,255,0.75)", 957 | "text-halo-width": 2 958 | }, 959 | "source": "openmaptiles", 960 | "source-layer": "place", 961 | "type": "symbol" 962 | } 963 | ], 964 | "id": "basic-preview" 965 | } 966 | -------------------------------------------------------------------------------- /configs/styles/sgpm-bright.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "Bright", 4 | "metadata": { 5 | "mapbox:autocomposite": false, 6 | "mapbox:groups": { 7 | "1444849242106.713": {"collapsed": false, "name": "Places"}, 8 | "1444849334699.1902": {"collapsed": true, "name": "Bridges"}, 9 | "1444849345966.4436": {"collapsed": false, "name": "Roads"}, 10 | "1444849354174.1904": {"collapsed": true, "name": "Tunnels"}, 11 | "1444849364238.8171": {"collapsed": false, "name": "Buildings"}, 12 | "1444849382550.77": {"collapsed": false, "name": "Water"}, 13 | "1444849388993.3071": {"collapsed": false, "name": "Land"} 14 | }, 15 | "mapbox:type": "template", 16 | "openmaptiles:mapbox:owner": "openmaptiles", 17 | "openmaptiles:mapbox:source:url": "mapbox://openmaptiles.4qljc88t", 18 | "openmaptiles:version": "3.x" 19 | }, 20 | "center": [0, 0], 21 | "zoom": 1, 22 | "bearing": 0, 23 | "pitch": 0, 24 | "sources": { 25 | "openmaptiles": { 26 | "type": "vector", 27 | "url": "http://localhost:8080/data/v3.json" 28 | } 29 | }, 30 | "sprite": "https://openmaptiles.github.io/osm-bright-gl-style/sprite", 31 | "glyphs": "http://localhost:8080/fonts/{fontstack}/{range}.pbf", 32 | "layers": [ 33 | { 34 | "id": "background", 35 | "type": "background", 36 | "paint": {"background-color": "#f8f4f0"} 37 | }, 38 | { 39 | "id": "landcover-glacier", 40 | "type": "fill", 41 | "metadata": {"mapbox:group": "1444849388993.3071"}, 42 | "source": "openmaptiles", 43 | "source-layer": "landcover", 44 | "filter": ["==", "subclass", "glacier"], 45 | "layout": {"visibility": "visible"}, 46 | "paint": { 47 | "fill-color": "#fff", 48 | "fill-opacity": {"base": 1, "stops": [[0, 0.9], [10, 0.3]]} 49 | } 50 | }, 51 | { 52 | "id": "landuse-residential", 53 | "type": "fill", 54 | "metadata": {"mapbox:group": "1444849388993.3071"}, 55 | "source": "openmaptiles", 56 | "source-layer": "landuse", 57 | "filter": [ 58 | "all", 59 | ["in", "class", "residential", "suburb", "neighbourhood"] 60 | ], 61 | "layout": {"visibility": "visible"}, 62 | "paint": { 63 | "fill-color": { 64 | "base": 1, 65 | "stops": [ 66 | [12, "hsla(30, 19%, 90%, 0.4)"], 67 | [16, "hsla(30, 19%, 90%, 0.2)"] 68 | ] 69 | } 70 | } 71 | }, 72 | { 73 | "id": "landuse-commercial", 74 | "type": "fill", 75 | "metadata": {"mapbox:group": "1444849388993.3071"}, 76 | "source": "openmaptiles", 77 | "source-layer": "landuse", 78 | "filter": [ 79 | "all", 80 | ["==", "$type", "Polygon"], 81 | ["==", "class", "commercial"] 82 | ], 83 | "layout": {"visibility": "visible"}, 84 | "paint": {"fill-color": "hsla(0, 60%, 87%, 0.23)"} 85 | }, 86 | { 87 | "id": "landuse-industrial", 88 | "type": "fill", 89 | "source": "openmaptiles", 90 | "source-layer": "landuse", 91 | "filter": [ 92 | "all", 93 | ["==", "$type", "Polygon"], 94 | ["in", "class", "industrial", "garages", "dam"] 95 | ], 96 | "layout": {"visibility": "visible"}, 97 | "paint": {"fill-color": "hsla(49, 100%, 88%, 0.34)"} 98 | }, 99 | { 100 | "id": "landuse-cemetery", 101 | "type": "fill", 102 | "metadata": {"mapbox:group": "1444849388993.3071"}, 103 | "source": "openmaptiles", 104 | "source-layer": "landuse", 105 | "filter": ["==", "class", "cemetery"], 106 | "paint": {"fill-color": "#e0e4dd"} 107 | }, 108 | { 109 | "id": "landuse-hospital", 110 | "type": "fill", 111 | "metadata": {"mapbox:group": "1444849388993.3071"}, 112 | "source": "openmaptiles", 113 | "source-layer": "landuse", 114 | "filter": ["==", "class", "hospital"], 115 | "paint": {"fill-color": "#fde"} 116 | }, 117 | { 118 | "id": "landuse-school", 119 | "type": "fill", 120 | "metadata": {"mapbox:group": "1444849388993.3071"}, 121 | "source": "openmaptiles", 122 | "source-layer": "landuse", 123 | "filter": ["==", "class", "school"], 124 | "paint": {"fill-color": "#f0e8f8"} 125 | }, 126 | { 127 | "id": "landuse-railway", 128 | "type": "fill", 129 | "metadata": {"mapbox:group": "1444849388993.3071"}, 130 | "source": "openmaptiles", 131 | "source-layer": "landuse", 132 | "filter": ["==", "class", "railway"], 133 | "layout": {"visibility": "visible"}, 134 | "paint": {"fill-color": "hsla(30, 19%, 90%, 0.4)"} 135 | }, 136 | { 137 | "id": "landcover-wood", 138 | "type": "fill", 139 | "metadata": {"mapbox:group": "1444849388993.3071"}, 140 | "source": "openmaptiles", 141 | "source-layer": "landcover", 142 | "filter": ["==", "class", "wood"], 143 | "paint": { 144 | "fill-antialias": {"base": 1, "stops": [[0, false], [9, true]]}, 145 | "fill-color": "#6a4", 146 | "fill-opacity": 0.1, 147 | "fill-outline-color": "hsla(0, 0%, 0%, 0.03)" 148 | } 149 | }, 150 | { 151 | "id": "landcover-grass", 152 | "type": "fill", 153 | "metadata": {"mapbox:group": "1444849388993.3071"}, 154 | "source": "openmaptiles", 155 | "source-layer": "landcover", 156 | "filter": ["==", "class", "grass"], 157 | "paint": {"fill-color": "#d8e8c8", "fill-opacity": 1} 158 | }, 159 | { 160 | "id": "landcover-grass-park", 161 | "type": "fill", 162 | "metadata": {"mapbox:group": "1444849388993.3071"}, 163 | "source": "openmaptiles", 164 | "source-layer": "park", 165 | "filter": ["==", "class", "public_park"], 166 | "paint": {"fill-color": "#d8e8c8", "fill-opacity": 0.8} 167 | }, 168 | { 169 | "id": "waterway_tunnel", 170 | "type": "line", 171 | "source": "openmaptiles", 172 | "source-layer": "waterway", 173 | "minzoom": 14, 174 | "filter": [ 175 | "all", 176 | ["in", "class", "river", "stream", "canal"], 177 | ["==", "brunnel", "tunnel"] 178 | ], 179 | "layout": {"line-cap": "round", "visibility": "visible"}, 180 | "paint": { 181 | "line-color": "#a0c8f0", 182 | "line-dasharray": [2, 4], 183 | "line-width": {"base": 1.3, "stops": [[13, 0.5], [20, 6]]} 184 | } 185 | }, 186 | { 187 | "id": "waterway-other", 188 | "type": "line", 189 | "metadata": {"mapbox:group": "1444849382550.77"}, 190 | "source": "openmaptiles", 191 | "source-layer": "waterway", 192 | "filter": [ 193 | "all", 194 | ["!in", "class", "canal", "river", "stream"], 195 | ["==", "intermittent", 0] 196 | ], 197 | "layout": {"line-cap": "round", "visibility": "visible"}, 198 | "paint": { 199 | "line-color": "#a0c8f0", 200 | "line-width": {"base": 1.3, "stops": [[13, 0.5], [20, 2]]} 201 | } 202 | }, 203 | { 204 | "id": "waterway-other-intermittent", 205 | "type": "line", 206 | "metadata": {"mapbox:group": "1444849382550.77"}, 207 | "source": "openmaptiles", 208 | "source-layer": "waterway", 209 | "filter": [ 210 | "all", 211 | ["!in", "class", "canal", "river", "stream"], 212 | ["==", "intermittent", 1] 213 | ], 214 | "layout": {"line-cap": "round", "visibility": "visible"}, 215 | "paint": { 216 | "line-color": "#a0c8f0", 217 | "line-dasharray": [4, 3], 218 | "line-width": {"base": 1.3, "stops": [[13, 0.5], [20, 2]]} 219 | } 220 | }, 221 | { 222 | "id": "waterway-stream-canal", 223 | "type": "line", 224 | "metadata": {"mapbox:group": "1444849382550.77"}, 225 | "source": "openmaptiles", 226 | "source-layer": "waterway", 227 | "filter": [ 228 | "all", 229 | ["in", "class", "canal", "stream"], 230 | ["!=", "brunnel", "tunnel"], 231 | ["==", "intermittent", 0] 232 | ], 233 | "layout": {"line-cap": "round", "visibility": "visible"}, 234 | "paint": { 235 | "line-color": "#a0c8f0", 236 | "line-width": {"base": 1.3, "stops": [[13, 0.5], [20, 6]]} 237 | } 238 | }, 239 | { 240 | "id": "waterway-stream-canal-intermittent", 241 | "type": "line", 242 | "metadata": {"mapbox:group": "1444849382550.77"}, 243 | "source": "openmaptiles", 244 | "source-layer": "waterway", 245 | "filter": [ 246 | "all", 247 | ["in", "class", "canal", "stream"], 248 | ["!=", "brunnel", "tunnel"], 249 | ["==", "intermittent", 1] 250 | ], 251 | "layout": {"line-cap": "round", "visibility": "visible"}, 252 | "paint": { 253 | "line-color": "#a0c8f0", 254 | "line-dasharray": [4, 3], 255 | "line-width": {"base": 1.3, "stops": [[13, 0.5], [20, 6]]} 256 | } 257 | }, 258 | { 259 | "id": "waterway-river", 260 | "type": "line", 261 | "metadata": {"mapbox:group": "1444849382550.77"}, 262 | "source": "openmaptiles", 263 | "source-layer": "waterway", 264 | "filter": [ 265 | "all", 266 | ["==", "class", "river"], 267 | ["!=", "brunnel", "tunnel"], 268 | ["==", "intermittent", 0] 269 | ], 270 | "layout": {"line-cap": "round", "visibility": "visible"}, 271 | "paint": { 272 | "line-color": "#a0c8f0", 273 | "line-width": {"base": 1.2, "stops": [[10, 0.8], [20, 6]]} 274 | } 275 | }, 276 | { 277 | "id": "waterway-river-intermittent", 278 | "type": "line", 279 | "metadata": {"mapbox:group": "1444849382550.77"}, 280 | "source": "openmaptiles", 281 | "source-layer": "waterway", 282 | "filter": [ 283 | "all", 284 | ["==", "class", "river"], 285 | ["!=", "brunnel", "tunnel"], 286 | ["==", "intermittent", 1] 287 | ], 288 | "layout": {"line-cap": "round", "visibility": "visible"}, 289 | "paint": { 290 | "line-color": "#a0c8f0", 291 | "line-dasharray": [3, 2.5], 292 | "line-width": {"base": 1.2, "stops": [[10, 0.8], [20, 6]]} 293 | } 294 | }, 295 | { 296 | "id": "water-offset", 297 | "type": "fill", 298 | "metadata": {"mapbox:group": "1444849382550.77"}, 299 | "source": "openmaptiles", 300 | "source-layer": "water", 301 | "maxzoom": 8, 302 | "filter": ["==", "$type", "Polygon"], 303 | "layout": {"visibility": "visible"}, 304 | "paint": { 305 | "fill-color": "#a0c8f0", 306 | "fill-opacity": 1, 307 | "fill-translate": {"base": 1, "stops": [[6, [2, 0]], [8, [0, 0]]]} 308 | } 309 | }, 310 | { 311 | "id": "water", 312 | "type": "fill", 313 | "metadata": {"mapbox:group": "1444849382550.77"}, 314 | "source": "openmaptiles", 315 | "source-layer": "water", 316 | "filter": ["all", ["!=", "intermittent", 1], ["!=", "brunnel", "tunnel"]], 317 | "layout": {"visibility": "visible"}, 318 | "paint": {"fill-color": "hsl(210, 67%, 85%)"} 319 | }, 320 | { 321 | "id": "water-intermittent", 322 | "type": "fill", 323 | "metadata": {"mapbox:group": "1444849382550.77"}, 324 | "source": "openmaptiles", 325 | "source-layer": "water", 326 | "filter": ["all", ["==", "intermittent", 1]], 327 | "layout": {"visibility": "visible"}, 328 | "paint": {"fill-color": "hsl(210, 67%, 85%)", "fill-opacity": 0.7} 329 | }, 330 | { 331 | "id": "water-pattern", 332 | "type": "fill", 333 | "metadata": {"mapbox:group": "1444849382550.77"}, 334 | "source": "openmaptiles", 335 | "source-layer": "water", 336 | "filter": ["all"], 337 | "layout": {"visibility": "visible"}, 338 | "paint": {"fill-pattern": "wave", "fill-translate": [0, 2.5]} 339 | }, 340 | { 341 | "id": "landcover-ice-shelf", 342 | "type": "fill", 343 | "metadata": {"mapbox:group": "1444849382550.77"}, 344 | "source": "openmaptiles", 345 | "source-layer": "landcover", 346 | "filter": ["==", "subclass", "ice_shelf"], 347 | "layout": {"visibility": "visible"}, 348 | "paint": { 349 | "fill-color": "#fff", 350 | "fill-opacity": {"base": 1, "stops": [[0, 0.9], [10, 0.3]]} 351 | } 352 | }, 353 | { 354 | "id": "landcover-sand", 355 | "type": "fill", 356 | "metadata": {"mapbox:group": "1444849382550.77"}, 357 | "source": "openmaptiles", 358 | "source-layer": "landcover", 359 | "filter": ["all", ["==", "class", "sand"]], 360 | "layout": {"visibility": "visible"}, 361 | "paint": {"fill-color": "rgba(245, 238, 188, 1)", "fill-opacity": 1} 362 | }, 363 | { 364 | "id": "building", 365 | "type": "fill", 366 | "metadata": {"mapbox:group": "1444849364238.8171"}, 367 | "source": "openmaptiles", 368 | "source-layer": "building", 369 | "paint": { 370 | "fill-antialias": true, 371 | "fill-color": {"base": 1, "stops": [[15.5, "#f2eae2"], [16, "#dfdbd7"]]} 372 | } 373 | }, 374 | { 375 | "id": "building-top", 376 | "type": "fill", 377 | "metadata": {"mapbox:group": "1444849364238.8171"}, 378 | "source": "openmaptiles", 379 | "source-layer": "building", 380 | "layout": {"visibility": "visible"}, 381 | "paint": { 382 | "fill-color": "#f2eae2", 383 | "fill-opacity": {"base": 1, "stops": [[13, 0], [16, 1]]}, 384 | "fill-outline-color": "#dfdbd7", 385 | "fill-translate": {"base": 1, "stops": [[14, [0, 0]], [16, [-2, -2]]]} 386 | } 387 | }, 388 | { 389 | "id": "tunnel-service-track-casing", 390 | "type": "line", 391 | "metadata": {"mapbox:group": "1444849354174.1904"}, 392 | "source": "openmaptiles", 393 | "source-layer": "transportation", 394 | "filter": [ 395 | "all", 396 | ["==", "brunnel", "tunnel"], 397 | ["in", "class", "service", "track"] 398 | ], 399 | "layout": {"line-join": "round"}, 400 | "paint": { 401 | "line-color": "#cfcdca", 402 | "line-dasharray": [0.5, 0.25], 403 | "line-width": {"base": 1.2, "stops": [[15, 1], [16, 4], [20, 11]]} 404 | } 405 | }, 406 | { 407 | "id": "tunnel-motorway-link-casing", 408 | "type": "line", 409 | "metadata": {"mapbox:group": "1444849354174.1904"}, 410 | "source": "openmaptiles", 411 | "source-layer": "transportation", 412 | "filter": [ 413 | "all", 414 | ["==", "brunnel", "tunnel"], 415 | ["==", "class", "motorway"], 416 | ["==", "ramp", 1] 417 | ], 418 | "layout": {"line-join": "round", "visibility": "visible"}, 419 | "paint": { 420 | "line-color": "rgba(200, 147, 102, 1)", 421 | "line-dasharray": [0.5, 0.25], 422 | "line-width": { 423 | "base": 1.2, 424 | "stops": [[12, 1], [13, 3], [14, 4], [20, 15]] 425 | } 426 | } 427 | }, 428 | { 429 | "id": "tunnel-minor-casing", 430 | "type": "line", 431 | "metadata": {"mapbox:group": "1444849354174.1904"}, 432 | "source": "openmaptiles", 433 | "source-layer": "transportation", 434 | "filter": ["all", ["==", "brunnel", "tunnel"], ["==", "class", "minor"]], 435 | "layout": {"line-join": "round"}, 436 | "paint": { 437 | "line-color": "#cfcdca", 438 | "line-opacity": {"stops": [[12, 0], [12.5, 1]]}, 439 | "line-width": { 440 | "base": 1.2, 441 | "stops": [[12, 0.5], [13, 1], [14, 4], [20, 15]] 442 | }, 443 | "line-dasharray": [0.5, 0.25] 444 | } 445 | }, 446 | { 447 | "id": "tunnel-link-casing", 448 | "type": "line", 449 | "metadata": {"mapbox:group": "1444849354174.1904"}, 450 | "source": "openmaptiles", 451 | "source-layer": "transportation", 452 | "filter": [ 453 | "all", 454 | ["==", "brunnel", "tunnel"], 455 | ["in", "class", "trunk", "primary", "secondary", "tertiary"], 456 | ["==", "ramp", 1] 457 | ], 458 | "layout": {"line-join": "round"}, 459 | "paint": { 460 | "line-color": "#e9ac77", 461 | "line-opacity": 1, 462 | "line-width": { 463 | "base": 1.2, 464 | "stops": [[12, 1], [13, 3], [14, 4], [20, 15]] 465 | }, 466 | "line-dasharray": [0.5, 0.25] 467 | } 468 | }, 469 | { 470 | "id": "tunnel-secondary-tertiary-casing", 471 | "type": "line", 472 | "metadata": {"mapbox:group": "1444849354174.1904"}, 473 | "source": "openmaptiles", 474 | "source-layer": "transportation", 475 | "filter": [ 476 | "all", 477 | ["==", "brunnel", "tunnel"], 478 | ["in", "class", "secondary", "tertiary"], 479 | ["!=", "ramp", 1] 480 | ], 481 | "layout": {"line-join": "round"}, 482 | "paint": { 483 | "line-color": "#e9ac77", 484 | "line-opacity": 1, 485 | "line-width": {"base": 1.2, "stops": [[8, 1.5], [20, 17]]}, 486 | "line-dasharray": [0.5, 0.25] 487 | } 488 | }, 489 | { 490 | "id": "tunnel-trunk-primary-casing", 491 | "type": "line", 492 | "metadata": {"mapbox:group": "1444849354174.1904"}, 493 | "source": "openmaptiles", 494 | "source-layer": "transportation", 495 | "filter": [ 496 | "all", 497 | ["==", "brunnel", "tunnel"], 498 | ["in", "class", "primary", "trunk"], 499 | ["!=", "ramp", 1] 500 | ], 501 | "layout": {"line-join": "round"}, 502 | "paint": { 503 | "line-color": "#e9ac77", 504 | "line-width": { 505 | "base": 1.2, 506 | "stops": [[5, 0.4], [6, 0.6], [7, 1.5], [20, 22]] 507 | } 508 | } 509 | }, 510 | { 511 | "id": "tunnel-motorway-casing", 512 | "type": "line", 513 | "metadata": {"mapbox:group": "1444849354174.1904"}, 514 | "source": "openmaptiles", 515 | "source-layer": "transportation", 516 | "filter": [ 517 | "all", 518 | ["==", "brunnel", "tunnel"], 519 | ["==", "class", "motorway"], 520 | ["!=", "ramp", 1] 521 | ], 522 | "layout": {"line-join": "round", "visibility": "visible"}, 523 | "paint": { 524 | "line-color": "#e9ac77", 525 | "line-dasharray": [0.5, 0.25], 526 | "line-width": { 527 | "base": 1.2, 528 | "stops": [[5, 0.4], [6, 0.6], [7, 1.5], [20, 22]] 529 | } 530 | } 531 | }, 532 | { 533 | "id": "tunnel-path", 534 | "type": "line", 535 | "metadata": {"mapbox:group": "1444849354174.1904"}, 536 | "source": "openmaptiles", 537 | "source-layer": "transportation", 538 | "filter": [ 539 | "all", 540 | ["==", "$type", "LineString"], 541 | ["==", "brunnel", "tunnel"], 542 | ["==", "class", "path"] 543 | ], 544 | "paint": { 545 | "line-color": "#cba", 546 | "line-dasharray": [1.5, 0.75], 547 | "line-width": {"base": 1.2, "stops": [[15, 1.2], [20, 4]]} 548 | } 549 | }, 550 | { 551 | "id": "tunnel-motorway-link", 552 | "type": "line", 553 | "metadata": {"mapbox:group": "1444849354174.1904"}, 554 | "source": "openmaptiles", 555 | "source-layer": "transportation", 556 | "filter": [ 557 | "all", 558 | ["==", "brunnel", "tunnel"], 559 | ["==", "class", "motorway"], 560 | ["==", "ramp", 1] 561 | ], 562 | "layout": {"line-join": "round", "visibility": "visible"}, 563 | "paint": { 564 | "line-color": "rgba(244, 209, 158, 1)", 565 | "line-width": { 566 | "base": 1.2, 567 | "stops": [[12.5, 0], [13, 1.5], [14, 2.5], [20, 11.5]] 568 | } 569 | } 570 | }, 571 | { 572 | "id": "tunnel-service-track", 573 | "type": "line", 574 | "metadata": {"mapbox:group": "1444849354174.1904"}, 575 | "source": "openmaptiles", 576 | "source-layer": "transportation", 577 | "filter": [ 578 | "all", 579 | ["==", "brunnel", "tunnel"], 580 | ["in", "class", "service", "track"] 581 | ], 582 | "layout": {"line-join": "round"}, 583 | "paint": { 584 | "line-color": "#fff", 585 | "line-width": {"base": 1.2, "stops": [[15.5, 0], [16, 2], [20, 7.5]]} 586 | } 587 | }, 588 | { 589 | "id": "tunnel-link", 590 | "type": "line", 591 | "metadata": {"mapbox:group": "1444849354174.1904"}, 592 | "source": "openmaptiles", 593 | "source-layer": "transportation", 594 | "filter": [ 595 | "all", 596 | ["==", "brunnel", "tunnel"], 597 | ["in", "class", "trunk", "primary", "secondary", "tertiary"], 598 | ["==", "ramp", 1] 599 | ], 600 | "layout": {"line-join": "round"}, 601 | "paint": { 602 | "line-color": "#fff4c6", 603 | "line-width": { 604 | "base": 1.2, 605 | "stops": [[12.5, 0], [13, 1.5], [14, 2.5], [20, 11.5]] 606 | } 607 | } 608 | }, 609 | { 610 | "id": "tunnel-minor", 611 | "type": "line", 612 | "metadata": {"mapbox:group": "1444849354174.1904"}, 613 | "source": "openmaptiles", 614 | "source-layer": "transportation", 615 | "filter": ["all", ["==", "brunnel", "tunnel"], ["==", "class", "minor"]], 616 | "layout": {"line-join": "round"}, 617 | "paint": { 618 | "line-color": "#fff", 619 | "line-opacity": 1, 620 | "line-width": {"base": 1.2, "stops": [[13.5, 0], [14, 2.5], [20, 11.5]]} 621 | } 622 | }, 623 | { 624 | "id": "tunnel-secondary-tertiary", 625 | "type": "line", 626 | "metadata": {"mapbox:group": "1444849354174.1904"}, 627 | "source": "openmaptiles", 628 | "source-layer": "transportation", 629 | "filter": [ 630 | "all", 631 | ["==", "brunnel", "tunnel"], 632 | ["in", "class", "secondary", "tertiary"], 633 | ["!=", "ramp", 1] 634 | ], 635 | "layout": {"line-join": "round"}, 636 | "paint": { 637 | "line-color": "#fff4c6", 638 | "line-width": {"base": 1.2, "stops": [[6.5, 0], [7, 0.5], [20, 10]]} 639 | } 640 | }, 641 | { 642 | "id": "tunnel-trunk-primary", 643 | "type": "line", 644 | "metadata": {"mapbox:group": "1444849354174.1904"}, 645 | "source": "openmaptiles", 646 | "source-layer": "transportation", 647 | "filter": [ 648 | "all", 649 | ["==", "brunnel", "tunnel"], 650 | ["in", "class", "primary", "trunk"], 651 | ["!=", "ramp", 1] 652 | ], 653 | "layout": {"line-join": "round"}, 654 | "paint": { 655 | "line-color": "#fff4c6", 656 | "line-width": {"base": 1.2, "stops": [[6.5, 0], [7, 0.5], [20, 18]]} 657 | } 658 | }, 659 | { 660 | "id": "tunnel-motorway", 661 | "type": "line", 662 | "metadata": {"mapbox:group": "1444849354174.1904"}, 663 | "source": "openmaptiles", 664 | "source-layer": "transportation", 665 | "filter": [ 666 | "all", 667 | ["==", "brunnel", "tunnel"], 668 | ["==", "class", "motorway"], 669 | ["!=", "ramp", 1] 670 | ], 671 | "layout": {"line-join": "round", "visibility": "visible"}, 672 | "paint": { 673 | "line-color": "#ffdaa6", 674 | "line-width": {"base": 1.2, "stops": [[6.5, 0], [7, 0.5], [20, 18]]} 675 | } 676 | }, 677 | { 678 | "id": "tunnel-railway", 679 | "type": "line", 680 | "metadata": {"mapbox:group": "1444849354174.1904"}, 681 | "source": "openmaptiles", 682 | "source-layer": "transportation", 683 | "filter": ["all", ["==", "brunnel", "tunnel"], ["==", "class", "rail"]], 684 | "paint": { 685 | "line-color": "#bbb", 686 | "line-dasharray": [2, 2], 687 | "line-width": {"base": 1.4, "stops": [[14, 0.4], [15, 0.75], [20, 2]]} 688 | } 689 | }, 690 | { 691 | "id": "ferry", 692 | "type": "line", 693 | "source": "openmaptiles", 694 | "source-layer": "transportation", 695 | "filter": ["all", ["in", "class", "ferry"]], 696 | "layout": {"line-join": "round", "visibility": "visible"}, 697 | "paint": { 698 | "line-color": "rgba(108, 159, 182, 1)", 699 | "line-dasharray": [2, 2], 700 | "line-width": 1.1 701 | } 702 | }, 703 | { 704 | "id": "aeroway-taxiway-casing", 705 | "type": "line", 706 | "metadata": {"mapbox:group": "1444849345966.4436"}, 707 | "source": "openmaptiles", 708 | "source-layer": "aeroway", 709 | "minzoom": 12, 710 | "filter": ["all", ["in", "class", "taxiway"]], 711 | "layout": { 712 | "line-cap": "round", 713 | "line-join": "round", 714 | "visibility": "visible" 715 | }, 716 | "paint": { 717 | "line-color": "rgba(153, 153, 153, 1)", 718 | "line-opacity": 1, 719 | "line-width": {"base": 1.5, "stops": [[11, 2], [17, 12]]} 720 | } 721 | }, 722 | { 723 | "id": "aeroway-runway-casing", 724 | "type": "line", 725 | "metadata": {"mapbox:group": "1444849345966.4436"}, 726 | "source": "openmaptiles", 727 | "source-layer": "aeroway", 728 | "minzoom": 12, 729 | "filter": ["all", ["in", "class", "runway"]], 730 | "layout": { 731 | "line-cap": "round", 732 | "line-join": "round", 733 | "visibility": "visible" 734 | }, 735 | "paint": { 736 | "line-color": "rgba(153, 153, 153, 1)", 737 | "line-opacity": 1, 738 | "line-width": {"base": 1.5, "stops": [[11, 5], [17, 55]]} 739 | } 740 | }, 741 | { 742 | "id": "aeroway-area", 743 | "type": "fill", 744 | "metadata": {"mapbox:group": "1444849345966.4436"}, 745 | "source": "openmaptiles", 746 | "source-layer": "aeroway", 747 | "minzoom": 4, 748 | "filter": [ 749 | "all", 750 | ["==", "$type", "Polygon"], 751 | ["in", "class", "runway", "taxiway"] 752 | ], 753 | "layout": {"visibility": "visible"}, 754 | "paint": { 755 | "fill-color": "rgba(255, 255, 255, 1)", 756 | "fill-opacity": {"base": 1, "stops": [[13, 0], [14, 1]]} 757 | } 758 | }, 759 | { 760 | "id": "aeroway-taxiway", 761 | "type": "line", 762 | "metadata": {"mapbox:group": "1444849345966.4436"}, 763 | "source": "openmaptiles", 764 | "source-layer": "aeroway", 765 | "minzoom": 4, 766 | "filter": [ 767 | "all", 768 | ["in", "class", "taxiway"], 769 | ["==", "$type", "LineString"] 770 | ], 771 | "layout": { 772 | "line-cap": "round", 773 | "line-join": "round", 774 | "visibility": "visible" 775 | }, 776 | "paint": { 777 | "line-color": "rgba(255, 255, 255, 1)", 778 | "line-opacity": {"base": 1, "stops": [[11, 0], [12, 1]]}, 779 | "line-width": {"base": 1.5, "stops": [[11, 1], [17, 10]]} 780 | } 781 | }, 782 | { 783 | "id": "aeroway-runway", 784 | "type": "line", 785 | "metadata": {"mapbox:group": "1444849345966.4436"}, 786 | "source": "openmaptiles", 787 | "source-layer": "aeroway", 788 | "minzoom": 4, 789 | "filter": [ 790 | "all", 791 | ["in", "class", "runway"], 792 | ["==", "$type", "LineString"] 793 | ], 794 | "layout": { 795 | "line-cap": "round", 796 | "line-join": "round", 797 | "visibility": "visible" 798 | }, 799 | "paint": { 800 | "line-color": "rgba(255, 255, 255, 1)", 801 | "line-opacity": {"base": 1, "stops": [[11, 0], [12, 1]]}, 802 | "line-width": {"base": 1.5, "stops": [[11, 4], [17, 50]]} 803 | } 804 | }, 805 | { 806 | "id": "road_area_pier", 807 | "type": "fill", 808 | "metadata": {}, 809 | "source": "openmaptiles", 810 | "source-layer": "transportation", 811 | "filter": ["all", ["==", "$type", "Polygon"], ["==", "class", "pier"]], 812 | "layout": {"visibility": "visible"}, 813 | "paint": {"fill-antialias": true, "fill-color": "#f8f4f0"} 814 | }, 815 | { 816 | "id": "road_pier", 817 | "type": "line", 818 | "metadata": {}, 819 | "source": "openmaptiles", 820 | "source-layer": "transportation", 821 | "filter": ["all", ["==", "$type", "LineString"], ["in", "class", "pier"]], 822 | "layout": {"line-cap": "round", "line-join": "round"}, 823 | "paint": { 824 | "line-color": "#f8f4f0", 825 | "line-width": {"base": 1.2, "stops": [[15, 1], [17, 4]]} 826 | } 827 | }, 828 | { 829 | "id": "highway-area", 830 | "type": "fill", 831 | "metadata": {"mapbox:group": "1444849345966.4436"}, 832 | "source": "openmaptiles", 833 | "source-layer": "transportation", 834 | "filter": ["all", ["==", "$type", "Polygon"], ["!in", "class", "pier"]], 835 | "layout": {"visibility": "visible"}, 836 | "paint": { 837 | "fill-antialias": false, 838 | "fill-color": "hsla(0, 0%, 89%, 0.56)", 839 | "fill-opacity": 0.9, 840 | "fill-outline-color": "#cfcdca" 841 | } 842 | }, 843 | { 844 | "id": "highway-motorway-link-casing", 845 | "type": "line", 846 | "metadata": {"mapbox:group": "1444849345966.4436"}, 847 | "source": "openmaptiles", 848 | "source-layer": "transportation", 849 | "minzoom": 12, 850 | "filter": [ 851 | "all", 852 | ["!in", "brunnel", "bridge", "tunnel"], 853 | ["==", "class", "motorway"], 854 | ["==", "ramp", 1] 855 | ], 856 | "layout": {"line-cap": "round", "line-join": "round"}, 857 | "paint": { 858 | "line-color": "#e9ac77", 859 | "line-opacity": 1, 860 | "line-width": { 861 | "base": 1.2, 862 | "stops": [[12, 1], [13, 3], [14, 4], [20, 15]] 863 | } 864 | } 865 | }, 866 | { 867 | "id": "highway-link-casing", 868 | "type": "line", 869 | "metadata": {"mapbox:group": "1444849345966.4436"}, 870 | "source": "openmaptiles", 871 | "source-layer": "transportation", 872 | "minzoom": 13, 873 | "filter": [ 874 | "all", 875 | ["!in", "brunnel", "bridge", "tunnel"], 876 | ["in", "class", "trunk", "primary", "secondary", "tertiary"], 877 | ["==", "ramp", 1] 878 | ], 879 | "layout": { 880 | "line-cap": "round", 881 | "line-join": "round", 882 | "visibility": "visible" 883 | }, 884 | "paint": { 885 | "line-color": "#e9ac77", 886 | "line-opacity": 1, 887 | "line-width": { 888 | "base": 1.2, 889 | "stops": [[12, 1], [13, 3], [14, 4], [20, 15]] 890 | } 891 | } 892 | }, 893 | { 894 | "id": "highway-minor-casing", 895 | "type": "line", 896 | "metadata": {"mapbox:group": "1444849345966.4436"}, 897 | "source": "openmaptiles", 898 | "source-layer": "transportation", 899 | "filter": [ 900 | "all", 901 | ["==", "$type", "LineString"], 902 | ["!=", "brunnel", "tunnel"], 903 | ["in", "class", "minor", "service", "track"] 904 | ], 905 | "layout": {"line-cap": "round", "line-join": "round"}, 906 | "paint": { 907 | "line-color": "#cfcdca", 908 | "line-opacity": {"stops": [[12, 0], [12.5, 1]]}, 909 | "line-width": { 910 | "base": 1.2, 911 | "stops": [[12, 0.5], [13, 1], [14, 4], [20, 15]] 912 | } 913 | } 914 | }, 915 | { 916 | "id": "highway-secondary-tertiary-casing", 917 | "type": "line", 918 | "metadata": {"mapbox:group": "1444849345966.4436"}, 919 | "source": "openmaptiles", 920 | "source-layer": "transportation", 921 | "filter": [ 922 | "all", 923 | ["!in", "brunnel", "bridge", "tunnel"], 924 | ["in", "class", "secondary", "tertiary"], 925 | ["!=", "ramp", 1] 926 | ], 927 | "layout": { 928 | "line-cap": "butt", 929 | "line-join": "round", 930 | "visibility": "visible" 931 | }, 932 | "paint": { 933 | "line-color": "#e9ac77", 934 | "line-opacity": 1, 935 | "line-width": {"base": 1.2, "stops": [[8, 1.5], [20, 17]]} 936 | } 937 | }, 938 | { 939 | "id": "highway-primary-casing", 940 | "type": "line", 941 | "metadata": {"mapbox:group": "1444849345966.4436"}, 942 | "source": "openmaptiles", 943 | "source-layer": "transportation", 944 | "minzoom": 5, 945 | "filter": [ 946 | "all", 947 | ["!in", "brunnel", "bridge", "tunnel"], 948 | ["in", "class", "primary"], 949 | ["!=", "ramp", 1] 950 | ], 951 | "layout": { 952 | "line-cap": "butt", 953 | "line-join": "round", 954 | "visibility": "visible" 955 | }, 956 | "paint": { 957 | "line-color": "#e9ac77", 958 | "line-opacity": {"stops": [[7, 0], [8, 1]]}, 959 | "line-width": { 960 | "base": 1.2, 961 | "stops": [[7, 0], [8, 0.6], [9, 1.5], [20, 22]] 962 | } 963 | } 964 | }, 965 | { 966 | "id": "highway-trunk-casing", 967 | "type": "line", 968 | "metadata": {"mapbox:group": "1444849345966.4436"}, 969 | "source": "openmaptiles", 970 | "source-layer": "transportation", 971 | "minzoom": 5, 972 | "filter": [ 973 | "all", 974 | ["!in", "brunnel", "bridge", "tunnel"], 975 | ["in", "class", "trunk"], 976 | ["!=", "ramp", 1] 977 | ], 978 | "layout": { 979 | "line-cap": "butt", 980 | "line-join": "round", 981 | "visibility": "visible" 982 | }, 983 | "paint": { 984 | "line-color": "#e9ac77", 985 | "line-opacity": {"stops": [[5, 0], [6, 1]]}, 986 | "line-width": { 987 | "base": 1.2, 988 | "stops": [[5, 0], [6, 0.6], [7, 1.5], [20, 22]] 989 | } 990 | } 991 | }, 992 | { 993 | "id": "highway-motorway-casing", 994 | "type": "line", 995 | "metadata": {"mapbox:group": "1444849345966.4436"}, 996 | "source": "openmaptiles", 997 | "source-layer": "transportation", 998 | "minzoom": 4, 999 | "filter": [ 1000 | "all", 1001 | ["!in", "brunnel", "bridge", "tunnel"], 1002 | ["==", "class", "motorway"], 1003 | ["!=", "ramp", 1] 1004 | ], 1005 | "layout": { 1006 | "line-cap": "butt", 1007 | "line-join": "round", 1008 | "visibility": "visible" 1009 | }, 1010 | "paint": { 1011 | "line-color": "#e9ac77", 1012 | "line-opacity": {"stops": [[4, 0], [5, 1]]}, 1013 | "line-width": { 1014 | "base": 1.2, 1015 | "stops": [[4, 0], [5, 0.4], [6, 0.6], [7, 1.5], [20, 22]] 1016 | } 1017 | } 1018 | }, 1019 | { 1020 | "id": "highway-path", 1021 | "type": "line", 1022 | "metadata": {"mapbox:group": "1444849345966.4436"}, 1023 | "source": "openmaptiles", 1024 | "source-layer": "transportation", 1025 | "filter": [ 1026 | "all", 1027 | ["==", "$type", "LineString"], 1028 | ["!in", "brunnel", "bridge", "tunnel"], 1029 | ["==", "class", "path"] 1030 | ], 1031 | "paint": { 1032 | "line-color": "#cba", 1033 | "line-dasharray": [1.5, 0.75], 1034 | "line-width": {"base": 1.2, "stops": [[15, 1.2], [20, 4]]} 1035 | } 1036 | }, 1037 | { 1038 | "id": "highway-motorway-link", 1039 | "type": "line", 1040 | "metadata": {"mapbox:group": "1444849345966.4436"}, 1041 | "source": "openmaptiles", 1042 | "source-layer": "transportation", 1043 | "minzoom": 12, 1044 | "filter": [ 1045 | "all", 1046 | ["!in", "brunnel", "bridge", "tunnel"], 1047 | ["==", "class", "motorway"], 1048 | ["==", "ramp", 1] 1049 | ], 1050 | "layout": {"line-cap": "round", "line-join": "round"}, 1051 | "paint": { 1052 | "line-color": "#fc8", 1053 | "line-width": { 1054 | "base": 1.2, 1055 | "stops": [[12.5, 0], [13, 1.5], [14, 2.5], [20, 11.5]] 1056 | } 1057 | } 1058 | }, 1059 | { 1060 | "id": "highway-link", 1061 | "type": "line", 1062 | "metadata": {"mapbox:group": "1444849345966.4436"}, 1063 | "source": "openmaptiles", 1064 | "source-layer": "transportation", 1065 | "minzoom": 13, 1066 | "filter": [ 1067 | "all", 1068 | ["!in", "brunnel", "bridge", "tunnel"], 1069 | ["in", "class", "trunk", "primary", "secondary", "tertiary"], 1070 | ["==", "ramp", 1] 1071 | ], 1072 | "layout": { 1073 | "line-cap": "round", 1074 | "line-join": "round", 1075 | "visibility": "visible" 1076 | }, 1077 | "paint": { 1078 | "line-color": "#fea", 1079 | "line-width": { 1080 | "base": 1.2, 1081 | "stops": [[12.5, 0], [13, 1.5], [14, 2.5], [20, 11.5]] 1082 | } 1083 | } 1084 | }, 1085 | { 1086 | "id": "highway-minor", 1087 | "type": "line", 1088 | "metadata": {"mapbox:group": "1444849345966.4436"}, 1089 | "source": "openmaptiles", 1090 | "source-layer": "transportation", 1091 | "filter": [ 1092 | "all", 1093 | ["==", "$type", "LineString"], 1094 | ["!=", "brunnel", "tunnel"], 1095 | ["in", "class", "minor", "service", "track"] 1096 | ], 1097 | "layout": {"line-cap": "round", "line-join": "round"}, 1098 | "paint": { 1099 | "line-color": "#fff", 1100 | "line-opacity": 1, 1101 | "line-width": {"base": 1.2, "stops": [[13.5, 0], [14, 2.5], [20, 11.5]]} 1102 | } 1103 | }, 1104 | { 1105 | "id": "highway-secondary-tertiary", 1106 | "type": "line", 1107 | "metadata": {"mapbox:group": "1444849345966.4436"}, 1108 | "source": "openmaptiles", 1109 | "source-layer": "transportation", 1110 | "filter": [ 1111 | "all", 1112 | ["!in", "brunnel", "bridge", "tunnel"], 1113 | ["in", "class", "secondary", "tertiary"], 1114 | ["!=", "ramp", 1] 1115 | ], 1116 | "layout": { 1117 | "line-cap": "round", 1118 | "line-join": "round", 1119 | "visibility": "visible" 1120 | }, 1121 | "paint": { 1122 | "line-color": "#fea", 1123 | "line-width": {"base": 1.2, "stops": [[6.5, 0], [8, 0.5], [20, 13]]} 1124 | } 1125 | }, 1126 | { 1127 | "id": "highway-primary", 1128 | "type": "line", 1129 | "metadata": {"mapbox:group": "1444849345966.4436"}, 1130 | "source": "openmaptiles", 1131 | "source-layer": "transportation", 1132 | "filter": [ 1133 | "all", 1134 | ["==", "$type", "LineString"], 1135 | ["!in", "brunnel", "bridge", "tunnel"], 1136 | ["in", "class", "primary"], 1137 | ["!=", "ramp", 1] 1138 | ], 1139 | "layout": { 1140 | "line-cap": "round", 1141 | "line-join": "round", 1142 | "visibility": "visible" 1143 | }, 1144 | "paint": { 1145 | "line-color": "#fea", 1146 | "line-width": {"base": 1.2, "stops": [[8.5, 0], [9, 0.5], [20, 18]]} 1147 | } 1148 | }, 1149 | { 1150 | "id": "highway-trunk", 1151 | "type": "line", 1152 | "metadata": {"mapbox:group": "1444849345966.4436"}, 1153 | "source": "openmaptiles", 1154 | "source-layer": "transportation", 1155 | "filter": [ 1156 | "all", 1157 | ["==", "$type", "LineString"], 1158 | ["!in", "brunnel", "bridge", "tunnel"], 1159 | ["in", "class", "trunk"], 1160 | ["!=", "ramp", 1] 1161 | ], 1162 | "layout": { 1163 | "line-cap": "round", 1164 | "line-join": "round", 1165 | "visibility": "visible" 1166 | }, 1167 | "paint": { 1168 | "line-color": "#fea", 1169 | "line-width": {"base": 1.2, "stops": [[6.5, 0], [7, 0.5], [20, 18]]} 1170 | } 1171 | }, 1172 | { 1173 | "id": "highway-motorway", 1174 | "type": "line", 1175 | "metadata": {"mapbox:group": "1444849345966.4436"}, 1176 | "source": "openmaptiles", 1177 | "source-layer": "transportation", 1178 | "minzoom": 5, 1179 | "filter": [ 1180 | "all", 1181 | ["==", "$type", "LineString"], 1182 | ["!in", "brunnel", "bridge", "tunnel"], 1183 | ["==", "class", "motorway"], 1184 | ["!=", "ramp", 1] 1185 | ], 1186 | "layout": { 1187 | "line-cap": "round", 1188 | "line-join": "round", 1189 | "visibility": "visible" 1190 | }, 1191 | "paint": { 1192 | "line-color": "#fc8", 1193 | "line-width": {"base": 1.2, "stops": [[6.5, 0], [7, 0.5], [20, 18]]} 1194 | } 1195 | }, 1196 | { 1197 | "id": "railway-transit", 1198 | "type": "line", 1199 | "metadata": {"mapbox:group": "1444849345966.4436"}, 1200 | "source": "openmaptiles", 1201 | "source-layer": "transportation", 1202 | "filter": [ 1203 | "all", 1204 | ["==", "$type", "LineString"], 1205 | ["==", "class", "transit"], 1206 | ["!in", "brunnel", "tunnel"] 1207 | ], 1208 | "layout": {"visibility": "visible"}, 1209 | "paint": { 1210 | "line-color": "hsla(0, 0%, 73%, 0.77)", 1211 | "line-width": {"base": 1.4, "stops": [[14, 0.4], [20, 1]]} 1212 | } 1213 | }, 1214 | { 1215 | "id": "railway-transit-hatching", 1216 | "type": "line", 1217 | "metadata": {"mapbox:group": "1444849345966.4436"}, 1218 | "source": "openmaptiles", 1219 | "source-layer": "transportation", 1220 | "filter": [ 1221 | "all", 1222 | ["==", "$type", "LineString"], 1223 | ["==", "class", "transit"], 1224 | ["!in", "brunnel", "tunnel"] 1225 | ], 1226 | "layout": {"visibility": "visible"}, 1227 | "paint": { 1228 | "line-color": "hsla(0, 0%, 73%, 0.68)", 1229 | "line-dasharray": [0.2, 8], 1230 | "line-width": {"base": 1.4, "stops": [[14.5, 0], [15, 2], [20, 6]]} 1231 | } 1232 | }, 1233 | { 1234 | "id": "railway-service", 1235 | "type": "line", 1236 | "metadata": {"mapbox:group": "1444849345966.4436"}, 1237 | "source": "openmaptiles", 1238 | "source-layer": "transportation", 1239 | "filter": [ 1240 | "all", 1241 | ["==", "$type", "LineString"], 1242 | ["==", "class", "rail"], 1243 | ["has", "service"] 1244 | ], 1245 | "paint": { 1246 | "line-color": "hsla(0, 0%, 73%, 0.77)", 1247 | "line-width": {"base": 1.4, "stops": [[14, 0.4], [20, 1]]} 1248 | } 1249 | }, 1250 | { 1251 | "id": "railway-service-hatching", 1252 | "type": "line", 1253 | "metadata": {"mapbox:group": "1444849345966.4436"}, 1254 | "source": "openmaptiles", 1255 | "source-layer": "transportation", 1256 | "filter": [ 1257 | "all", 1258 | ["==", "$type", "LineString"], 1259 | ["==", "class", "rail"], 1260 | ["has", "service"] 1261 | ], 1262 | "layout": {"visibility": "visible"}, 1263 | "paint": { 1264 | "line-color": "hsla(0, 0%, 73%, 0.68)", 1265 | "line-dasharray": [0.2, 8], 1266 | "line-width": {"base": 1.4, "stops": [[14.5, 0], [15, 2], [20, 6]]} 1267 | } 1268 | }, 1269 | { 1270 | "id": "railway", 1271 | "type": "line", 1272 | "metadata": {"mapbox:group": "1444849345966.4436"}, 1273 | "source": "openmaptiles", 1274 | "source-layer": "transportation", 1275 | "filter": [ 1276 | "all", 1277 | ["==", "$type", "LineString"], 1278 | ["!has", "service"], 1279 | ["!in", "brunnel", "bridge", "tunnel"], 1280 | ["==", "class", "rail"] 1281 | ], 1282 | "paint": { 1283 | "line-color": "#bbb", 1284 | "line-width": {"base": 1.4, "stops": [[14, 0.4], [15, 0.75], [20, 2]]} 1285 | } 1286 | }, 1287 | { 1288 | "id": "railway-hatching", 1289 | "type": "line", 1290 | "metadata": {"mapbox:group": "1444849345966.4436"}, 1291 | "source": "openmaptiles", 1292 | "source-layer": "transportation", 1293 | "filter": [ 1294 | "all", 1295 | ["==", "$type", "LineString"], 1296 | ["!has", "service"], 1297 | ["!in", "brunnel", "bridge", "tunnel"], 1298 | ["==", "class", "rail"] 1299 | ], 1300 | "paint": { 1301 | "line-color": "#bbb", 1302 | "line-dasharray": [0.2, 8], 1303 | "line-width": {"base": 1.4, "stops": [[14.5, 0], [15, 3], [20, 8]]} 1304 | } 1305 | }, 1306 | { 1307 | "id": "bridge-motorway-link-casing", 1308 | "type": "line", 1309 | "metadata": {"mapbox:group": "1444849334699.1902"}, 1310 | "source": "openmaptiles", 1311 | "source-layer": "transportation", 1312 | "filter": [ 1313 | "all", 1314 | ["==", "brunnel", "bridge"], 1315 | ["==", "class", "motorway"], 1316 | ["==", "ramp", 1] 1317 | ], 1318 | "layout": {"line-join": "round"}, 1319 | "paint": { 1320 | "line-color": "#e9ac77", 1321 | "line-opacity": 1, 1322 | "line-width": { 1323 | "base": 1.2, 1324 | "stops": [[12, 1], [13, 3], [14, 4], [20, 19]] 1325 | } 1326 | } 1327 | }, 1328 | { 1329 | "id": "bridge-link-casing", 1330 | "type": "line", 1331 | "metadata": {"mapbox:group": "1444849334699.1902"}, 1332 | "source": "openmaptiles", 1333 | "source-layer": "transportation", 1334 | "filter": [ 1335 | "all", 1336 | ["==", "brunnel", "bridge"], 1337 | ["in", "class", "trunk", "primary", "secondary", "tertiary"], 1338 | ["==", "ramp", 1] 1339 | ], 1340 | "layout": {"line-join": "round"}, 1341 | "paint": { 1342 | "line-color": "#e9ac77", 1343 | "line-opacity": 1, 1344 | "line-width": { 1345 | "base": 1.2, 1346 | "stops": [[12, 1], [13, 3], [14, 4], [20, 19]] 1347 | } 1348 | } 1349 | }, 1350 | { 1351 | "id": "bridge-secondary-tertiary-casing", 1352 | "type": "line", 1353 | "metadata": {"mapbox:group": "1444849334699.1902"}, 1354 | "source": "openmaptiles", 1355 | "source-layer": "transportation", 1356 | "filter": [ 1357 | "all", 1358 | ["==", "brunnel", "bridge"], 1359 | ["in", "class", "secondary", "tertiary"], 1360 | ["!=", "ramp", 1] 1361 | ], 1362 | "layout": {"line-join": "round"}, 1363 | "paint": { 1364 | "line-color": "#e9ac77", 1365 | "line-opacity": 1, 1366 | "line-width": { 1367 | "base": 1.2, 1368 | "stops": [[5, 0.4], [7, 0.6], [8, 1.5], [20, 21]] 1369 | } 1370 | } 1371 | }, 1372 | { 1373 | "id": "bridge-trunk-primary-casing", 1374 | "type": "line", 1375 | "metadata": {"mapbox:group": "1444849334699.1902"}, 1376 | "source": "openmaptiles", 1377 | "source-layer": "transportation", 1378 | "filter": [ 1379 | "all", 1380 | ["==", "brunnel", "bridge"], 1381 | ["in", "class", "primary", "trunk"], 1382 | ["!=", "ramp", 1] 1383 | ], 1384 | "layout": {"line-join": "round"}, 1385 | "paint": { 1386 | "line-color": "hsl(28, 76%, 67%)", 1387 | "line-width": { 1388 | "base": 1.2, 1389 | "stops": [[5, 0.4], [6, 0.6], [7, 1.5], [20, 26]] 1390 | } 1391 | } 1392 | }, 1393 | { 1394 | "id": "bridge-motorway-casing", 1395 | "type": "line", 1396 | "metadata": {"mapbox:group": "1444849334699.1902"}, 1397 | "source": "openmaptiles", 1398 | "source-layer": "transportation", 1399 | "filter": [ 1400 | "all", 1401 | ["==", "brunnel", "bridge"], 1402 | ["==", "class", "motorway"], 1403 | ["!=", "ramp", 1] 1404 | ], 1405 | "layout": {"line-join": "round"}, 1406 | "paint": { 1407 | "line-color": "#e9ac77", 1408 | "line-width": { 1409 | "base": 1.2, 1410 | "stops": [[5, 0.4], [6, 0.6], [7, 1.5], [20, 26]] 1411 | } 1412 | } 1413 | }, 1414 | { 1415 | "id": "bridge-minor-casing", 1416 | "type": "line", 1417 | "metadata": {"mapbox:group": "1444849345966.4436"}, 1418 | "source": "openmaptiles", 1419 | "source-layer": "transportation", 1420 | "filter": [ 1421 | "all", 1422 | ["==", "$type", "LineString"], 1423 | ["==", "brunnel", "bridge"], 1424 | ["in", "class", "minor", "service", "track"] 1425 | ], 1426 | "layout": {"line-cap": "butt", "line-join": "round"}, 1427 | "paint": { 1428 | "line-color": "#cfcdca", 1429 | "line-opacity": {"stops": [[12, 0], [12.5, 1]]}, 1430 | "line-width": { 1431 | "base": 1.2, 1432 | "stops": [[12, 0.5], [13, 1], [14, 6], [20, 24]] 1433 | } 1434 | } 1435 | }, 1436 | { 1437 | "id": "bridge-path-casing", 1438 | "type": "line", 1439 | "metadata": {"mapbox:group": "1444849334699.1902"}, 1440 | "source": "openmaptiles", 1441 | "source-layer": "transportation", 1442 | "filter": [ 1443 | "all", 1444 | ["==", "$type", "LineString"], 1445 | ["==", "brunnel", "bridge"], 1446 | ["==", "class", "path"] 1447 | ], 1448 | "paint": { 1449 | "line-color": "#f8f4f0", 1450 | "line-width": {"base": 1.2, "stops": [[15, 1.2], [20, 18]]} 1451 | } 1452 | }, 1453 | { 1454 | "id": "bridge-path", 1455 | "type": "line", 1456 | "metadata": {"mapbox:group": "1444849334699.1902"}, 1457 | "source": "openmaptiles", 1458 | "source-layer": "transportation", 1459 | "filter": [ 1460 | "all", 1461 | ["==", "$type", "LineString"], 1462 | ["==", "brunnel", "bridge"], 1463 | ["==", "class", "path"] 1464 | ], 1465 | "paint": { 1466 | "line-color": "#cba", 1467 | "line-dasharray": [1.5, 0.75], 1468 | "line-width": {"base": 1.2, "stops": [[15, 1.2], [20, 4]]} 1469 | } 1470 | }, 1471 | { 1472 | "id": "bridge-motorway-link", 1473 | "type": "line", 1474 | "metadata": {"mapbox:group": "1444849334699.1902"}, 1475 | "source": "openmaptiles", 1476 | "source-layer": "transportation", 1477 | "filter": [ 1478 | "all", 1479 | ["==", "brunnel", "bridge"], 1480 | ["==", "class", "motorway"], 1481 | ["==", "ramp", 1] 1482 | ], 1483 | "layout": {"line-join": "round"}, 1484 | "paint": { 1485 | "line-color": "#fc8", 1486 | "line-width": { 1487 | "base": 1.2, 1488 | "stops": [[12.5, 0], [13, 1.5], [14, 2.5], [20, 11.5]] 1489 | } 1490 | } 1491 | }, 1492 | { 1493 | "id": "bridge-link", 1494 | "type": "line", 1495 | "metadata": {"mapbox:group": "1444849334699.1902"}, 1496 | "source": "openmaptiles", 1497 | "source-layer": "transportation", 1498 | "filter": [ 1499 | "all", 1500 | ["==", "brunnel", "bridge"], 1501 | ["in", "class", "trunk", "primary", "secondary", "tertiary"], 1502 | ["==", "ramp", 1] 1503 | ], 1504 | "layout": {"line-join": "round"}, 1505 | "paint": { 1506 | "line-color": "#fea", 1507 | "line-width": { 1508 | "base": 1.2, 1509 | "stops": [[12.5, 0], [13, 1.5], [14, 2.5], [20, 11.5]] 1510 | } 1511 | } 1512 | }, 1513 | { 1514 | "id": "bridge-minor", 1515 | "type": "line", 1516 | "metadata": {"mapbox:group": "1444849345966.4436"}, 1517 | "source": "openmaptiles", 1518 | "source-layer": "transportation", 1519 | "filter": [ 1520 | "all", 1521 | ["==", "$type", "LineString"], 1522 | ["==", "brunnel", "bridge"], 1523 | ["in", "class", "minor", "service", "track"] 1524 | ], 1525 | "layout": {"line-cap": "round", "line-join": "round"}, 1526 | "paint": { 1527 | "line-color": "#fff", 1528 | "line-opacity": 1, 1529 | "line-width": {"base": 1.2, "stops": [[13.5, 0], [14, 2.5], [20, 11.5]]} 1530 | } 1531 | }, 1532 | { 1533 | "id": "bridge-secondary-tertiary", 1534 | "type": "line", 1535 | "metadata": {"mapbox:group": "1444849334699.1902"}, 1536 | "source": "openmaptiles", 1537 | "source-layer": "transportation", 1538 | "filter": [ 1539 | "all", 1540 | ["==", "brunnel", "bridge"], 1541 | ["in", "class", "secondary", "tertiary"], 1542 | ["!=", "ramp", 1] 1543 | ], 1544 | "layout": {"line-join": "round"}, 1545 | "paint": { 1546 | "line-color": "#fea", 1547 | "line-width": {"base": 1.2, "stops": [[6.5, 0], [8, 0.5], [20, 13]]} 1548 | } 1549 | }, 1550 | { 1551 | "id": "bridge-trunk-primary", 1552 | "type": "line", 1553 | "metadata": {"mapbox:group": "1444849334699.1902"}, 1554 | "source": "openmaptiles", 1555 | "source-layer": "transportation", 1556 | "filter": [ 1557 | "all", 1558 | ["==", "brunnel", "bridge"], 1559 | ["in", "class", "primary", "trunk"], 1560 | ["!=", "ramp", 1] 1561 | ], 1562 | "layout": {"line-join": "round"}, 1563 | "paint": { 1564 | "line-color": "#fea", 1565 | "line-width": {"base": 1.2, "stops": [[6.5, 0], [7, 0.5], [20, 18]]} 1566 | } 1567 | }, 1568 | { 1569 | "id": "bridge-motorway", 1570 | "type": "line", 1571 | "metadata": {"mapbox:group": "1444849334699.1902"}, 1572 | "source": "openmaptiles", 1573 | "source-layer": "transportation", 1574 | "filter": [ 1575 | "all", 1576 | ["==", "brunnel", "bridge"], 1577 | ["==", "class", "motorway"], 1578 | ["!=", "ramp", 1] 1579 | ], 1580 | "layout": {"line-join": "round"}, 1581 | "paint": { 1582 | "line-color": "#fc8", 1583 | "line-width": {"base": 1.2, "stops": [[6.5, 0], [7, 0.5], [20, 18]]} 1584 | } 1585 | }, 1586 | { 1587 | "id": "bridge-railway", 1588 | "type": "line", 1589 | "metadata": {"mapbox:group": "1444849334699.1902"}, 1590 | "source": "openmaptiles", 1591 | "source-layer": "transportation", 1592 | "filter": ["all", ["==", "brunnel", "bridge"], ["==", "class", "rail"]], 1593 | "paint": { 1594 | "line-color": "#bbb", 1595 | "line-width": {"base": 1.4, "stops": [[14, 0.4], [15, 0.75], [20, 2]]} 1596 | } 1597 | }, 1598 | { 1599 | "id": "bridge-railway-hatching", 1600 | "type": "line", 1601 | "metadata": {"mapbox:group": "1444849334699.1902"}, 1602 | "source": "openmaptiles", 1603 | "source-layer": "transportation", 1604 | "filter": ["all", ["==", "brunnel", "bridge"], ["==", "class", "rail"]], 1605 | "paint": { 1606 | "line-color": "#bbb", 1607 | "line-dasharray": [0.2, 8], 1608 | "line-width": {"base": 1.4, "stops": [[14.5, 0], [15, 3], [20, 8]]} 1609 | } 1610 | }, 1611 | { 1612 | "id": "cablecar", 1613 | "type": "line", 1614 | "source": "openmaptiles", 1615 | "source-layer": "transportation", 1616 | "minzoom": 13, 1617 | "filter": ["==", "subclass", "cable_car"], 1618 | "layout": {"line-cap": "round", "visibility": "visible"}, 1619 | "paint": { 1620 | "line-color": "hsl(0, 0%, 70%)", 1621 | "line-width": {"base": 1, "stops": [[11, 1], [19, 2.5]]} 1622 | } 1623 | }, 1624 | { 1625 | "id": "cablecar-dash", 1626 | "type": "line", 1627 | "source": "openmaptiles", 1628 | "source-layer": "transportation", 1629 | "minzoom": 13, 1630 | "filter": ["==", "subclass", "cable_car"], 1631 | "layout": {"line-cap": "round", "visibility": "visible"}, 1632 | "paint": { 1633 | "line-color": "hsl(0, 0%, 70%)", 1634 | "line-dasharray": [2, 3], 1635 | "line-width": {"base": 1, "stops": [[11, 3], [19, 5.5]]} 1636 | } 1637 | }, 1638 | { 1639 | "id": "boundary-land-level-4", 1640 | "type": "line", 1641 | "source": "openmaptiles", 1642 | "source-layer": "boundary", 1643 | "minzoom": 2, 1644 | "filter": [ 1645 | "all", 1646 | [">=", "admin_level", 3], 1647 | ["<=", "admin_level", 8], 1648 | ["!=", "maritime", 1] 1649 | ], 1650 | "layout": {"line-join": "round", "visibility": "visible"}, 1651 | "paint": { 1652 | "line-color": "#9e9cab", 1653 | "line-dasharray": [3, 1, 1, 1], 1654 | "line-width": {"base": 1.4, "stops": [[4, 0.4], [5, 1], [12, 3]]} 1655 | } 1656 | }, 1657 | { 1658 | "id": "boundary-land-level-2", 1659 | "type": "line", 1660 | "source": "openmaptiles", 1661 | "source-layer": "boundary", 1662 | "filter": [ 1663 | "all", 1664 | ["==", "admin_level", 2], 1665 | ["!=", "maritime", 1], 1666 | ["!=", "disputed", 1] 1667 | ], 1668 | "layout": { 1669 | "line-cap": "round", 1670 | "line-join": "round", 1671 | "visibility": "visible" 1672 | }, 1673 | "paint": { 1674 | "line-color": "hsl(248, 7%, 66%)", 1675 | "line-width": { 1676 | "base": 1, 1677 | "stops": [[0, 0.6], [4, 1.4], [5, 2], [12, 8]] 1678 | } 1679 | } 1680 | }, 1681 | { 1682 | "id": "boundary-land-disputed", 1683 | "type": "line", 1684 | "source": "openmaptiles", 1685 | "source-layer": "boundary", 1686 | "filter": ["all", ["!=", "maritime", 1], ["==", "disputed", 1]], 1687 | "layout": { 1688 | "line-cap": "round", 1689 | "line-join": "round", 1690 | "visibility": "visible" 1691 | }, 1692 | "paint": { 1693 | "line-color": "hsl(248, 7%, 70%)", 1694 | "line-dasharray": [1, 3], 1695 | "line-width": { 1696 | "base": 1, 1697 | "stops": [[0, 0.6], [4, 1.4], [5, 2], [12, 8]] 1698 | } 1699 | } 1700 | }, 1701 | { 1702 | "id": "boundary-water", 1703 | "type": "line", 1704 | "source": "openmaptiles", 1705 | "source-layer": "boundary", 1706 | "minzoom": 4, 1707 | "filter": ["all", ["in", "admin_level", 2, 4], ["==", "maritime", 1]], 1708 | "layout": { 1709 | "line-cap": "round", 1710 | "line-join": "round", 1711 | "visibility": "visible" 1712 | }, 1713 | "paint": { 1714 | "line-color": "rgba(154, 189, 214, 1)", 1715 | "line-opacity": {"stops": [[6, 0.6], [10, 1]]}, 1716 | "line-width": { 1717 | "base": 1, 1718 | "stops": [[0, 0.6], [4, 1.4], [5, 2], [12, 8]] 1719 | } 1720 | } 1721 | }, 1722 | { 1723 | "id": "waterway-name", 1724 | "type": "symbol", 1725 | "source": "openmaptiles", 1726 | "source-layer": "waterway", 1727 | "minzoom": 13, 1728 | "filter": ["all", ["==", "$type", "LineString"], ["has", "name"]], 1729 | "layout": { 1730 | "symbol-placement": "line", 1731 | "symbol-spacing": 350, 1732 | "text-field": "{name:latin} {name:nonlatin}", 1733 | "text-font": ["Noto Sans Regular"], 1734 | "text-letter-spacing": 0.2, 1735 | "text-max-width": 5, 1736 | "text-rotation-alignment": "map", 1737 | "text-size": 14 1738 | }, 1739 | "paint": { 1740 | "text-color": "#74aee9", 1741 | "text-halo-color": "rgba(255,255,255,0.7)", 1742 | "text-halo-width": 1.5 1743 | } 1744 | }, 1745 | { 1746 | "id": "water-name-lakeline", 1747 | "type": "symbol", 1748 | "source": "openmaptiles", 1749 | "source-layer": "water_name", 1750 | "filter": ["==", "$type", "LineString"], 1751 | "layout": { 1752 | "symbol-placement": "line", 1753 | "symbol-spacing": 350, 1754 | "text-field": "{name:latin}\n{name:nonlatin}", 1755 | "text-font": ["Noto Sans Regular"], 1756 | "text-letter-spacing": 0.2, 1757 | "text-max-width": 5, 1758 | "text-rotation-alignment": "map", 1759 | "text-size": 14 1760 | }, 1761 | "paint": { 1762 | "text-color": "#74aee9", 1763 | "text-halo-color": "rgba(255,255,255,0.7)", 1764 | "text-halo-width": 1.5 1765 | } 1766 | }, 1767 | { 1768 | "id": "water-name-ocean", 1769 | "type": "symbol", 1770 | "source": "openmaptiles", 1771 | "source-layer": "water_name", 1772 | "filter": ["all", ["==", "$type", "Point"], ["==", "class", "ocean"]], 1773 | "layout": { 1774 | "symbol-placement": "point", 1775 | "symbol-spacing": 350, 1776 | "text-field": "{name:latin}", 1777 | "text-font": ["Noto Sans Regular"], 1778 | "text-letter-spacing": 0.2, 1779 | "text-max-width": 5, 1780 | "text-rotation-alignment": "map", 1781 | "text-size": 14 1782 | }, 1783 | "paint": { 1784 | "text-color": "#74aee9", 1785 | "text-halo-color": "rgba(255,255,255,0.7)", 1786 | "text-halo-width": 1.5 1787 | } 1788 | }, 1789 | { 1790 | "id": "water-name-other", 1791 | "type": "symbol", 1792 | "source": "openmaptiles", 1793 | "source-layer": "water_name", 1794 | "filter": ["all", ["==", "$type", "Point"], ["!in", "class", "ocean"]], 1795 | "layout": { 1796 | "symbol-placement": "point", 1797 | "symbol-spacing": 350, 1798 | "text-field": "{name:latin}\n{name:nonlatin}", 1799 | "text-font": ["Noto Sans Regular"], 1800 | "text-letter-spacing": 0.2, 1801 | "text-max-width": 5, 1802 | "text-rotation-alignment": "map", 1803 | "text-size": {"stops": [[0, 10], [6, 14]]}, 1804 | "visibility": "visible" 1805 | }, 1806 | "paint": { 1807 | "text-color": "#74aee9", 1808 | "text-halo-color": "rgba(255,255,255,0.7)", 1809 | "text-halo-width": 1.5 1810 | } 1811 | }, 1812 | { 1813 | "id": "road_oneway", 1814 | "type": "symbol", 1815 | "source": "openmaptiles", 1816 | "source-layer": "transportation", 1817 | "minzoom": 15, 1818 | "filter": [ 1819 | "all", 1820 | ["==", "oneway", 1], 1821 | [ 1822 | "in", 1823 | "class", 1824 | "motorway", 1825 | "trunk", 1826 | "primary", 1827 | "secondary", 1828 | "tertiary", 1829 | "minor", 1830 | "service" 1831 | ] 1832 | ], 1833 | "layout": { 1834 | "icon-image": "oneway", 1835 | "icon-padding": 2, 1836 | "icon-rotate": 90, 1837 | "icon-rotation-alignment": "map", 1838 | "icon-size": {"stops": [[15, 0.5], [19, 1]]}, 1839 | "symbol-placement": "line", 1840 | "symbol-spacing": 75 1841 | }, 1842 | "paint": {"icon-opacity": 0.5} 1843 | }, 1844 | { 1845 | "id": "road_oneway_opposite", 1846 | "type": "symbol", 1847 | "source": "openmaptiles", 1848 | "source-layer": "transportation", 1849 | "minzoom": 15, 1850 | "filter": [ 1851 | "all", 1852 | ["==", "oneway", -1], 1853 | [ 1854 | "in", 1855 | "class", 1856 | "motorway", 1857 | "trunk", 1858 | "primary", 1859 | "secondary", 1860 | "tertiary", 1861 | "minor", 1862 | "service" 1863 | ] 1864 | ], 1865 | "layout": { 1866 | "icon-image": "oneway", 1867 | "icon-padding": 2, 1868 | "icon-rotate": -90, 1869 | "icon-rotation-alignment": "map", 1870 | "icon-size": {"stops": [[15, 0.5], [19, 1]]}, 1871 | "symbol-placement": "line", 1872 | "symbol-spacing": 75 1873 | }, 1874 | "paint": {"icon-opacity": 0.5} 1875 | }, 1876 | { 1877 | "id": "poi-level-3", 1878 | "type": "symbol", 1879 | "source": "openmaptiles", 1880 | "source-layer": "poi", 1881 | "minzoom": 16, 1882 | "filter": [ 1883 | "all", 1884 | ["==", "$type", "Point"], 1885 | [">=", "rank", 25], 1886 | ["any", ["!has", "level"], ["==", "level", 0]] 1887 | ], 1888 | "layout": { 1889 | "icon-image": "{class}_11", 1890 | "text-anchor": "top", 1891 | "text-field": "{name:latin}\n{name:nonlatin}", 1892 | "text-font": ["Noto Sans Regular"], 1893 | "text-max-width": 9, 1894 | "text-offset": [0, 0.6], 1895 | "text-padding": 2, 1896 | "text-size": 12, 1897 | "visibility": "visible" 1898 | }, 1899 | "paint": { 1900 | "text-color": "#666", 1901 | "text-halo-blur": 0.5, 1902 | "text-halo-color": "#ffffff", 1903 | "text-halo-width": 1 1904 | } 1905 | }, 1906 | { 1907 | "id": "poi-level-2", 1908 | "type": "symbol", 1909 | "source": "openmaptiles", 1910 | "source-layer": "poi", 1911 | "minzoom": 15, 1912 | "filter": [ 1913 | "all", 1914 | ["==", "$type", "Point"], 1915 | ["<=", "rank", 24], 1916 | [">=", "rank", 15], 1917 | ["any", ["!has", "level"], ["==", "level", 0]] 1918 | ], 1919 | "layout": { 1920 | "icon-image": "{class}_11", 1921 | "text-anchor": "top", 1922 | "text-field": "{name:latin}\n{name:nonlatin}", 1923 | "text-font": ["Noto Sans Regular"], 1924 | "text-max-width": 9, 1925 | "text-offset": [0, 0.6], 1926 | "text-padding": 2, 1927 | "text-size": 12, 1928 | "visibility": "visible" 1929 | }, 1930 | "paint": { 1931 | "text-color": "#666", 1932 | "text-halo-blur": 0.5, 1933 | "text-halo-color": "#ffffff", 1934 | "text-halo-width": 1 1935 | } 1936 | }, 1937 | { 1938 | "id": "poi-level-1", 1939 | "type": "symbol", 1940 | "source": "openmaptiles", 1941 | "source-layer": "poi", 1942 | "minzoom": 14, 1943 | "filter": [ 1944 | "all", 1945 | ["==", "$type", "Point"], 1946 | ["<=", "rank", 14], 1947 | ["has", "name"], 1948 | ["any", ["!has", "level"], ["==", "level", 0]] 1949 | ], 1950 | "layout": { 1951 | "icon-image": "{class}_11", 1952 | "text-anchor": "top", 1953 | "text-field": "{name:latin}\n{name:nonlatin}", 1954 | "text-font": ["Noto Sans Regular"], 1955 | "text-max-width": 9, 1956 | "text-offset": [0, 0.6], 1957 | "text-padding": 2, 1958 | "text-size": 12, 1959 | "visibility": "visible" 1960 | }, 1961 | "paint": { 1962 | "text-color": "#666", 1963 | "text-halo-blur": 0.5, 1964 | "text-halo-color": "#ffffff", 1965 | "text-halo-width": 1 1966 | } 1967 | }, 1968 | { 1969 | "id": "poi-railway", 1970 | "type": "symbol", 1971 | "source": "openmaptiles", 1972 | "source-layer": "poi", 1973 | "minzoom": 13, 1974 | "filter": [ 1975 | "all", 1976 | ["==", "$type", "Point"], 1977 | ["has", "name"], 1978 | ["==", "class", "railway"], 1979 | ["==", "subclass", "station"] 1980 | ], 1981 | "layout": { 1982 | "icon-allow-overlap": false, 1983 | "icon-ignore-placement": false, 1984 | "icon-image": "{class}_11", 1985 | "icon-optional": false, 1986 | "text-allow-overlap": false, 1987 | "text-anchor": "top", 1988 | "text-field": "{name:latin}\n{name:nonlatin}", 1989 | "text-font": ["Noto Sans Regular"], 1990 | "text-ignore-placement": false, 1991 | "text-max-width": 9, 1992 | "text-offset": [0, 0.6], 1993 | "text-optional": true, 1994 | "text-padding": 2, 1995 | "text-size": 12 1996 | }, 1997 | "paint": { 1998 | "text-color": "#666", 1999 | "text-halo-blur": 0.5, 2000 | "text-halo-color": "#ffffff", 2001 | "text-halo-width": 1 2002 | } 2003 | }, 2004 | { 2005 | "id": "highway-name-path", 2006 | "type": "symbol", 2007 | "source": "openmaptiles", 2008 | "source-layer": "transportation_name", 2009 | "minzoom": 15.5, 2010 | "filter": ["==", "class", "path"], 2011 | "layout": { 2012 | "symbol-placement": "line", 2013 | "text-field": "{name:latin} {name:nonlatin}", 2014 | "text-font": ["Noto Sans Regular"], 2015 | "text-rotation-alignment": "map", 2016 | "text-size": {"base": 1, "stops": [[13, 12], [14, 13]]} 2017 | }, 2018 | "paint": { 2019 | "text-color": "hsl(30, 23%, 62%)", 2020 | "text-halo-color": "#f8f4f0", 2021 | "text-halo-width": 0.5 2022 | } 2023 | }, 2024 | { 2025 | "id": "highway-name-minor", 2026 | "type": "symbol", 2027 | "source": "openmaptiles", 2028 | "source-layer": "transportation_name", 2029 | "minzoom": 15, 2030 | "filter": [ 2031 | "all", 2032 | ["==", "$type", "LineString"], 2033 | ["in", "class", "minor", "service", "track"] 2034 | ], 2035 | "layout": { 2036 | "symbol-placement": "line", 2037 | "text-field": "{name:latin} {name:nonlatin}", 2038 | "text-font": ["Noto Sans Regular"], 2039 | "text-rotation-alignment": "map", 2040 | "text-size": {"base": 1, "stops": [[13, 12], [14, 13]]} 2041 | }, 2042 | "paint": { 2043 | "text-color": "#765", 2044 | "text-halo-blur": 0.5, 2045 | "text-halo-width": 1 2046 | } 2047 | }, 2048 | { 2049 | "id": "highway-name-major", 2050 | "type": "symbol", 2051 | "source": "openmaptiles", 2052 | "source-layer": "transportation_name", 2053 | "minzoom": 12.2, 2054 | "filter": ["in", "class", "primary", "secondary", "tertiary", "trunk"], 2055 | "layout": { 2056 | "symbol-placement": "line", 2057 | "text-field": "{name:latin} {name:nonlatin}", 2058 | "text-font": ["Noto Sans Regular"], 2059 | "text-rotation-alignment": "map", 2060 | "text-size": {"base": 1, "stops": [[13, 12], [14, 13]]} 2061 | }, 2062 | "paint": { 2063 | "text-color": "#765", 2064 | "text-halo-blur": 0.5, 2065 | "text-halo-width": 1 2066 | } 2067 | }, 2068 | { 2069 | "id": "highway-shield", 2070 | "type": "symbol", 2071 | "source": "openmaptiles", 2072 | "source-layer": "transportation_name", 2073 | "minzoom": 8, 2074 | "filter": [ 2075 | "all", 2076 | ["<=", "ref_length", 6], 2077 | ["==", "$type", "LineString"], 2078 | ["!in", "network", "us-interstate", "us-highway", "us-state"] 2079 | ], 2080 | "layout": { 2081 | "icon-image": "road_{ref_length}", 2082 | "icon-rotation-alignment": "viewport", 2083 | "icon-size": 1, 2084 | "symbol-placement": {"base": 1, "stops": [[10, "point"], [11, "line"]]}, 2085 | "symbol-spacing": 200, 2086 | "text-field": "{ref}", 2087 | "text-font": ["Noto Sans Regular"], 2088 | "text-rotation-alignment": "viewport", 2089 | "text-size": 10 2090 | }, 2091 | "paint": {} 2092 | }, 2093 | { 2094 | "id": "highway-shield-us-interstate", 2095 | "type": "symbol", 2096 | "source": "openmaptiles", 2097 | "source-layer": "transportation_name", 2098 | "minzoom": 7, 2099 | "filter": [ 2100 | "all", 2101 | ["<=", "ref_length", 6], 2102 | ["==", "$type", "LineString"], 2103 | ["in", "network", "us-interstate"] 2104 | ], 2105 | "layout": { 2106 | "icon-image": "{network}_{ref_length}", 2107 | "icon-rotation-alignment": "viewport", 2108 | "icon-size": 1, 2109 | "symbol-placement": { 2110 | "base": 1, 2111 | "stops": [[7, "point"], [7, "line"], [8, "line"]] 2112 | }, 2113 | "symbol-spacing": 200, 2114 | "text-field": "{ref}", 2115 | "text-font": ["Noto Sans Regular"], 2116 | "text-rotation-alignment": "viewport", 2117 | "text-size": 10 2118 | }, 2119 | "paint": {"text-color": "rgba(0, 0, 0, 1)"} 2120 | }, 2121 | { 2122 | "id": "highway-shield-us-other", 2123 | "type": "symbol", 2124 | "source": "openmaptiles", 2125 | "source-layer": "transportation_name", 2126 | "minzoom": 9, 2127 | "filter": [ 2128 | "all", 2129 | ["<=", "ref_length", 6], 2130 | ["==", "$type", "LineString"], 2131 | ["in", "network", "us-highway", "us-state"] 2132 | ], 2133 | "layout": { 2134 | "icon-image": "{network}_{ref_length}", 2135 | "icon-rotation-alignment": "viewport", 2136 | "icon-size": 1, 2137 | "symbol-placement": {"base": 1, "stops": [[10, "point"], [11, "line"]]}, 2138 | "symbol-spacing": 200, 2139 | "text-field": "{ref}", 2140 | "text-font": ["Noto Sans Regular"], 2141 | "text-rotation-alignment": "viewport", 2142 | "text-size": 10 2143 | }, 2144 | "paint": {"text-color": "rgba(0, 0, 0, 1)"} 2145 | }, 2146 | { 2147 | "id": "airport-label-major", 2148 | "type": "symbol", 2149 | "source": "openmaptiles", 2150 | "source-layer": "aerodrome_label", 2151 | "minzoom": 10, 2152 | "filter": ["all", ["has", "iata"]], 2153 | "layout": { 2154 | "icon-image": "airport_11", 2155 | "icon-size": 1, 2156 | "text-anchor": "top", 2157 | "text-field": "{name:latin}\n{name:nonlatin}", 2158 | "text-font": ["Noto Sans Regular"], 2159 | "text-max-width": 9, 2160 | "text-offset": [0, 0.6], 2161 | "text-optional": true, 2162 | "text-padding": 2, 2163 | "text-size": 12, 2164 | "visibility": "visible" 2165 | }, 2166 | "paint": { 2167 | "text-color": "#666", 2168 | "text-halo-blur": 0.5, 2169 | "text-halo-color": "#ffffff", 2170 | "text-halo-width": 1 2171 | } 2172 | }, 2173 | { 2174 | "id": "place-other", 2175 | "type": "symbol", 2176 | "metadata": {"mapbox:group": "1444849242106.713"}, 2177 | "source": "openmaptiles", 2178 | "source-layer": "place", 2179 | "filter": [ 2180 | "!in", 2181 | "class", 2182 | "city", 2183 | "town", 2184 | "village", 2185 | "state", 2186 | "country", 2187 | "continent" 2188 | ], 2189 | "layout": { 2190 | "text-field": "{name:latin}\n{name:nonlatin}", 2191 | "text-font": ["Noto Sans Regular"], 2192 | "text-letter-spacing": 0.1, 2193 | "text-max-width": 9, 2194 | "text-size": {"base": 1.2, "stops": [[12, 10], [15, 14]]}, 2195 | "text-transform": "uppercase", 2196 | "visibility": "visible" 2197 | }, 2198 | "paint": { 2199 | "text-color": "#633", 2200 | "text-halo-color": "rgba(255,255,255,0.8)", 2201 | "text-halo-width": 1.2 2202 | } 2203 | }, 2204 | { 2205 | "id": "place-village", 2206 | "type": "symbol", 2207 | "metadata": {"mapbox:group": "1444849242106.713"}, 2208 | "source": "openmaptiles", 2209 | "source-layer": "place", 2210 | "filter": ["==", "class", "village"], 2211 | "layout": { 2212 | "text-field": "{name:latin}\n{name:nonlatin}", 2213 | "text-font": ["Noto Sans Regular"], 2214 | "text-max-width": 8, 2215 | "text-size": {"base": 1.2, "stops": [[10, 12], [15, 22]]}, 2216 | "visibility": "visible" 2217 | }, 2218 | "paint": { 2219 | "text-color": "#333", 2220 | "text-halo-color": "rgba(255,255,255,0.8)", 2221 | "text-halo-width": 1.2 2222 | } 2223 | }, 2224 | { 2225 | "id": "place-town", 2226 | "type": "symbol", 2227 | "metadata": {"mapbox:group": "1444849242106.713"}, 2228 | "source": "openmaptiles", 2229 | "source-layer": "place", 2230 | "filter": ["==", "class", "town"], 2231 | "layout": { 2232 | "text-field": "{name:latin}\n{name:nonlatin}", 2233 | "text-font": ["Noto Sans Regular"], 2234 | "text-max-width": 8, 2235 | "text-size": {"base": 1.2, "stops": [[10, 14], [15, 24]]}, 2236 | "visibility": "visible" 2237 | }, 2238 | "paint": { 2239 | "text-color": "#333", 2240 | "text-halo-color": "rgba(255,255,255,0.8)", 2241 | "text-halo-width": 1.2 2242 | } 2243 | }, 2244 | { 2245 | "id": "place-city", 2246 | "type": "symbol", 2247 | "metadata": {"mapbox:group": "1444849242106.713"}, 2248 | "source": "openmaptiles", 2249 | "source-layer": "place", 2250 | "filter": ["all", ["!=", "capital", 2], ["==", "class", "city"]], 2251 | "layout": { 2252 | "text-field": "{name:latin}\n{name:nonlatin}", 2253 | "text-font": ["Noto Sans Regular"], 2254 | "text-max-width": 8, 2255 | "text-size": {"base": 1.2, "stops": [[7, 14], [11, 24]]}, 2256 | "visibility": "visible" 2257 | }, 2258 | "paint": { 2259 | "text-color": "#333", 2260 | "text-halo-color": "rgba(255,255,255,0.8)", 2261 | "text-halo-width": 1.2 2262 | } 2263 | }, 2264 | { 2265 | "id": "place-city-capital", 2266 | "type": "symbol", 2267 | "metadata": {"mapbox:group": "1444849242106.713"}, 2268 | "source": "openmaptiles", 2269 | "source-layer": "place", 2270 | "filter": ["all", ["==", "capital", 2], ["==", "class", "city"]], 2271 | "layout": { 2272 | "icon-image": "star_11", 2273 | "icon-size": 0.8, 2274 | "text-anchor": "left", 2275 | "text-field": "{name:latin}\n{name:nonlatin}", 2276 | "text-font": ["Noto Sans Regular"], 2277 | "text-max-width": 8, 2278 | "text-offset": [0.4, 0], 2279 | "text-size": {"base": 1.2, "stops": [[7, 14], [11, 24]]}, 2280 | "visibility": "visible" 2281 | }, 2282 | "paint": { 2283 | "text-color": "#333", 2284 | "text-halo-color": "rgba(255,255,255,0.8)", 2285 | "text-halo-width": 1.2 2286 | } 2287 | }, 2288 | { 2289 | "id": "place-state", 2290 | "type": "symbol", 2291 | "metadata": {"mapbox:group": "1444849242106.713"}, 2292 | "source": "openmaptiles", 2293 | "source-layer": "place", 2294 | "filter": ["in", "class", "state"], 2295 | "layout": { 2296 | "text-field": "{name:latin}", 2297 | "text-font": ["Noto Sans Regular"], 2298 | "text-letter-spacing": 0.1, 2299 | "text-max-width": 9, 2300 | "text-size": {"base": 1.2, "stops": [[12, 10], [15, 14]]}, 2301 | "text-transform": "uppercase", 2302 | "visibility": "visible" 2303 | }, 2304 | "paint": { 2305 | "text-color": "#633", 2306 | "text-halo-color": "rgba(255,255,255,0.8)", 2307 | "text-halo-width": 1.2 2308 | } 2309 | }, 2310 | { 2311 | "id": "place-country-other", 2312 | "type": "symbol", 2313 | "metadata": {"mapbox:group": "1444849242106.713"}, 2314 | "source": "openmaptiles", 2315 | "source-layer": "place", 2316 | "filter": [ 2317 | "all", 2318 | ["==", "class", "country"], 2319 | [">=", "rank", 3], 2320 | ["!has", "iso_a2"] 2321 | ], 2322 | "layout": { 2323 | "text-field": "{name:latin}", 2324 | "text-font": ["Noto Sans Regular"], 2325 | "text-max-width": 6.25, 2326 | "text-size": {"stops": [[3, 11], [7, 17]]}, 2327 | "text-transform": "uppercase", 2328 | "visibility": "visible" 2329 | }, 2330 | "paint": { 2331 | "text-color": "#334", 2332 | "text-halo-blur": 1, 2333 | "text-halo-color": "rgba(255,255,255,0.8)", 2334 | "text-halo-width": 2 2335 | } 2336 | }, 2337 | { 2338 | "id": "place-country-3", 2339 | "type": "symbol", 2340 | "metadata": {"mapbox:group": "1444849242106.713"}, 2341 | "source": "openmaptiles", 2342 | "source-layer": "place", 2343 | "filter": [ 2344 | "all", 2345 | ["==", "class", "country"], 2346 | [">=", "rank", 3], 2347 | ["has", "iso_a2"] 2348 | ], 2349 | "layout": { 2350 | "text-field": "{name:latin}", 2351 | "text-font": ["Noto Sans Regular"], 2352 | "text-max-width": 6.25, 2353 | "text-size": {"stops": [[3, 11], [7, 17]]}, 2354 | "text-transform": "uppercase", 2355 | "visibility": "visible" 2356 | }, 2357 | "paint": { 2358 | "text-color": "#334", 2359 | "text-halo-blur": 1, 2360 | "text-halo-color": "rgba(255,255,255,0.8)", 2361 | "text-halo-width": 2 2362 | } 2363 | }, 2364 | { 2365 | "id": "place-country-2", 2366 | "type": "symbol", 2367 | "metadata": {"mapbox:group": "1444849242106.713"}, 2368 | "source": "openmaptiles", 2369 | "source-layer": "place", 2370 | "filter": [ 2371 | "all", 2372 | ["==", "class", "country"], 2373 | ["==", "rank", 2], 2374 | ["has", "iso_a2"] 2375 | ], 2376 | "layout": { 2377 | "text-field": "{name:latin}", 2378 | "text-font": ["Noto Sans Regular"], 2379 | "text-max-width": 6.25, 2380 | "text-size": {"stops": [[2, 11], [5, 17]]}, 2381 | "text-transform": "uppercase", 2382 | "visibility": "visible" 2383 | }, 2384 | "paint": { 2385 | "text-color": "#334", 2386 | "text-halo-blur": 1, 2387 | "text-halo-color": "rgba(255,255,255,0.8)", 2388 | "text-halo-width": 2 2389 | } 2390 | }, 2391 | { 2392 | "id": "place-country-1", 2393 | "type": "symbol", 2394 | "metadata": {"mapbox:group": "1444849242106.713"}, 2395 | "source": "openmaptiles", 2396 | "source-layer": "place", 2397 | "filter": [ 2398 | "all", 2399 | ["==", "class", "country"], 2400 | ["==", "rank", 1], 2401 | ["has", "iso_a2"] 2402 | ], 2403 | "layout": { 2404 | "text-field": "{name:latin}", 2405 | "text-font": ["Noto Sans Regular"], 2406 | "text-max-width": 6.25, 2407 | "text-size": {"stops": [[1, 11], [4, 17]]}, 2408 | "text-transform": "uppercase", 2409 | "visibility": "visible" 2410 | }, 2411 | "paint": { 2412 | "text-color": "#334", 2413 | "text-halo-blur": 1, 2414 | "text-halo-color": "rgba(255,255,255,0.8)", 2415 | "text-halo-width": 2 2416 | } 2417 | }, 2418 | { 2419 | "id": "place-continent", 2420 | "type": "symbol", 2421 | "metadata": {"mapbox:group": "1444849242106.713"}, 2422 | "source": "openmaptiles", 2423 | "source-layer": "place", 2424 | "maxzoom": 1, 2425 | "filter": ["==", "class", "continent"], 2426 | "layout": { 2427 | "text-field": "{name:latin}", 2428 | "text-font": ["Noto Sans Regular"], 2429 | "text-max-width": 6.25, 2430 | "text-size": 14, 2431 | "text-transform": "uppercase", 2432 | "visibility": "visible" 2433 | }, 2434 | "paint": { 2435 | "text-color": "#334", 2436 | "text-halo-blur": 1, 2437 | "text-halo-color": "rgba(255,255,255,0.8)", 2438 | "text-halo-width": 2 2439 | } 2440 | } 2441 | ], 2442 | "id": "bright" 2443 | } -------------------------------------------------------------------------------- /configs/test/TEST_INVALID.json: -------------------------------------------------------------------------------- 1 | { 2 | "pbfFile": "", 3 | "workingDir": "", 4 | "outDir": "", 5 | "excludeOcean": false, 6 | "excludeLanduse": false, 7 | "TilemakerConfig": "", 8 | "TilemakerProcess": "", 9 | "maxRamMb": 0, 10 | "outAsDir": false, 11 | "skipSlicing": false, 12 | "mergeOnly": false, 13 | "skipDownload": false, 14 | "thisIsAnInvalidConfigRequiredForTesting": 0 15 | } 16 | -------------------------------------------------------------------------------- /configs/test/TEST_VALID.json: -------------------------------------------------------------------------------- 1 | { 2 | "pbfFile": "", 3 | "workingDir": "", 4 | "outDir": "", 5 | "excludeOcean": false, 6 | "excludeLanduse": false, 7 | "TilemakerConfig": "", 8 | "TilemakerProcess": "", 9 | "maxRamMb": 0, 10 | "outAsDir": false, 11 | "skipSlicing": false, 12 | "mergeOnly": false, 13 | "skipDownload": false 14 | } 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lambdajack/sequentially-generate-planet-mbtiles 2 | 3 | go 1.18 4 | 5 | require github.com/lambdajack/lj_go v0.0.3 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/lambdajack/lj_go v0.0.3 h1:OOjZCwH4+lHpcrjHZoE9h58NE/DWDZ69fLSfcEWvq4Y= 2 | github.com/lambdajack/lj_go v0.0.3/go.mod h1:MjcZAAAcQKPrP7M98EeVwwEoHo0AGDI51g9c/ZpAd44= 3 | -------------------------------------------------------------------------------- /internal/describeloggers/errlog.go: -------------------------------------------------------------------------------- 1 | package describeloggers 2 | 3 | import ( 4 | "io" 5 | "log" 6 | ) 7 | 8 | func Err(writer *io.Writer) *log.Logger { 9 | errLog := log.New(*writer, "", log.Ldate|log.Ltime|log.Lshortfile) 10 | return errLog 11 | } 12 | -------------------------------------------------------------------------------- /internal/describeloggers/errlog_test.go: -------------------------------------------------------------------------------- 1 | package describeloggers 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "os" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestErr(t *testing.T) { 13 | tmpDir := t.TempDir() 14 | 15 | f, err := os.Create(tmpDir + "/err.log") 16 | if err != nil { 17 | t.Error("Failed to create tmp files for test") 18 | } 19 | 20 | writer := io.Writer(f) 21 | 22 | progLog := Prog(&writer) 23 | 24 | if reflect.TypeOf(progLog).String() != "*log.Logger" { 25 | t.Errorf("got %v, want *log.Logger", reflect.TypeOf(progLog)) 26 | } 27 | 28 | progLog.Println("TEST") 29 | 30 | f.Close() 31 | 32 | f, err = os.Open(tmpDir + "/err.log") 33 | if err != nil { 34 | t.Error("Failed to open log file for reading") 35 | } 36 | defer f.Close() 37 | 38 | scanner := bufio.NewScanner(f) 39 | 40 | for scanner.Scan() { 41 | if strings.HasSuffix(scanner.Text(), "TEST") { 42 | break 43 | } 44 | t.Errorf("got %v, want suffix TEST", scanner.Text()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/describeloggers/proglog.go: -------------------------------------------------------------------------------- 1 | package describeloggers 2 | 3 | import ( 4 | "io" 5 | "log" 6 | ) 7 | 8 | func Prog(writer *io.Writer) *log.Logger { 9 | progLog := log.New(*writer, "", log.Ldate|log.Ltime) 10 | return progLog 11 | } 12 | -------------------------------------------------------------------------------- /internal/describeloggers/proglog_test.go: -------------------------------------------------------------------------------- 1 | package describeloggers 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "os" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestProg(t *testing.T) { 13 | tmpDir := t.TempDir() 14 | 15 | f, err := os.Create(tmpDir + "/prog.log") 16 | if err != nil { 17 | t.Error("Failed to create tmp files for test") 18 | } 19 | 20 | writer := io.Writer(f) 21 | 22 | progLog := Prog(&writer) 23 | 24 | if reflect.TypeOf(progLog).String() != "*log.Logger" { 25 | t.Errorf("got %v, want *log.Logger", reflect.TypeOf(progLog)) 26 | } 27 | 28 | progLog.Println("TEST") 29 | 30 | f.Close() 31 | 32 | f, err = os.Open(tmpDir + "/prog.log") 33 | if err != nil { 34 | t.Error("Failed to open log file for reading") 35 | } 36 | defer f.Close() 37 | 38 | scanner := bufio.NewScanner(f) 39 | 40 | for scanner.Scan() { 41 | if strings.HasSuffix(scanner.Text(), "TEST") { 42 | break 43 | } 44 | t.Errorf("got %v, want suffix TEST", scanner.Text()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/describeloggers/replog.go: -------------------------------------------------------------------------------- 1 | package describeloggers 2 | 3 | import ( 4 | "io" 5 | "log" 6 | ) 7 | 8 | func Rep(writer *io.Writer) *log.Logger { 9 | repLog := log.New(*writer, "", log.Ldate|log.Ltime) 10 | return repLog 11 | } 12 | -------------------------------------------------------------------------------- /internal/describeloggers/replog_test.go: -------------------------------------------------------------------------------- 1 | package describeloggers 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "os" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestRep(t *testing.T) { 13 | tmpDir := t.TempDir() 14 | 15 | f, err := os.Create(tmpDir + "/rep.log") 16 | if err != nil { 17 | t.Error("Failed to create tmp files for test") 18 | } 19 | 20 | writer := io.Writer(f) 21 | 22 | progLog := Rep(&writer) 23 | 24 | if reflect.TypeOf(progLog).String() != "*log.Logger" { 25 | t.Errorf("got %v, want *log.Logger", reflect.TypeOf(progLog)) 26 | } 27 | 28 | progLog.Println("TEST") 29 | 30 | f.Close() 31 | 32 | f, err = os.Open(tmpDir + "/rep.log") 33 | if err != nil { 34 | t.Error("Failed to open log file for reading") 35 | } 36 | defer f.Close() 37 | 38 | scanner := bufio.NewScanner(f) 39 | 40 | for scanner.Scan() { 41 | if strings.HasSuffix(scanner.Text(), "TEST") { 42 | break 43 | } 44 | t.Errorf("got %v, want suffix TEST", scanner.Text()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/docker/docker.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | ) 10 | 11 | type Container struct { 12 | Built bool 13 | Context string 14 | Dockerfile string 15 | Flags []string 16 | Name string 17 | Volumes []Volume 18 | } 19 | 20 | type Volume struct { 21 | Container string 22 | Host string 23 | } 24 | 25 | func (c *Container) Build() error { 26 | if c.Built { 27 | return nil 28 | } 29 | 30 | err := exec.Command("docker", "image", "inspect", c.Name).Run() 31 | if err == nil { 32 | log.Printf("docker image %s already built - proceeding without cached rebuild", c.Name) 33 | c.Built = true 34 | return nil 35 | } 36 | 37 | if c.Name == "" { 38 | return fmt.Errorf("container name is empty") 39 | } 40 | 41 | if c.Dockerfile == "" { 42 | return fmt.Errorf("dockerfile is empty") 43 | } 44 | 45 | if c.Context == "" { 46 | return fmt.Errorf("context is empty") 47 | } 48 | 49 | cmd := exec.Command("docker", "build", "-t", c.Name, "-f", c.Dockerfile, c.Context) 50 | cmd.Stdout = os.Stdout 51 | cmd.Stderr = os.Stderr 52 | if err := cmd.Run(); err != nil { 53 | return err 54 | } 55 | cmd.Wait() 56 | c.Built = true 57 | return nil 58 | } 59 | 60 | func (c *Container) Execute(command []string) error { 61 | 62 | if !c.Built { 63 | err := c.Build() 64 | if err != nil { 65 | return err 66 | } 67 | } 68 | 69 | ex := []string{"docker", "run", "--name", c.Name} 70 | ex = append(ex, c.Flags...) 71 | for _, v := range c.Volumes { 72 | ex = append(ex, "-v", v.Host+":"+v.Container) 73 | } 74 | ex = append(ex, c.Name) 75 | ex = append(ex, command...) 76 | 77 | cmd := exec.Command(ex[0], ex[1:]...) 78 | cmd.Stdout = os.Stdout 79 | cmd.Stderr = os.Stderr 80 | if err := cmd.Run(); err != nil { 81 | return err 82 | } 83 | cmd.Wait() 84 | return nil 85 | } 86 | 87 | func (c Container) Clean() error { 88 | 89 | out, err := exec.Command("docker", "ps", "-q", "--filter", "name="+c.Name).Output() 90 | if err != nil { 91 | return err 92 | } 93 | if len(out) < 1 { 94 | return nil 95 | } 96 | 97 | log.Printf("attempting to clean up container: %s, ( %s)\n", c.Name, strings.ReplaceAll(string(out), "\n", " ")) 98 | 99 | cmd := exec.Command("docker", "stop", c.Name) 100 | cmd.Stdout = os.Stdout 101 | cmd.Stderr = os.Stderr 102 | if err := cmd.Run(); err != nil { 103 | 104 | return err 105 | } 106 | cmd.Wait() 107 | return nil 108 | } 109 | 110 | // Returns a container with the --rm flag set by default. 111 | func New(c Container) *Container { 112 | rm := false 113 | for _, f := range c.Flags { 114 | if f == "--rm" { 115 | rm = true 116 | } 117 | } 118 | 119 | if !rm { 120 | c.Flags = append(c.Flags, "--rm") 121 | } 122 | 123 | c.Built = false 124 | return &c 125 | } 126 | -------------------------------------------------------------------------------- /internal/extract/extract.go: -------------------------------------------------------------------------------- 1 | package extract 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "github.com/lambdajack/sequentially-generate-planet-mbtiles/internal/docker" 8 | ) 9 | 10 | func Extract(src, dst, bbox string, osmium *docker.Container) (string, error) { 11 | src, err := filepath.Abs(src) 12 | if err != nil { 13 | return "", err 14 | } 15 | dst, err = filepath.Abs(dst) 16 | if err != nil { 17 | return "", err 18 | } 19 | 20 | osmium.Volumes = []docker.Volume{ 21 | { 22 | Container: "/pbf", 23 | Host: filepath.Dir(src), 24 | }, 25 | { 26 | Container: "/out", 27 | Host: filepath.Dir(dst), 28 | }, 29 | } 30 | 31 | err = osmium.Execute([]string{"osmium", "extract", "-b", bbox, "--set-bounds", "--overwrite", "/pbf/" + filepath.Base(src), "-o", "/out/" + filepath.Base(dst)}) 32 | if err != nil { 33 | return "", fmt.Errorf("osmboundaryextract.go | Failed to extract %s from %s \n %v", bbox, filepath.Base(src), err) 34 | } 35 | 36 | return dst, nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/extract/tree.go: -------------------------------------------------------------------------------- 1 | package extract 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "regexp" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/lambdajack/sequentially-generate-planet-mbtiles/internal/docker" 15 | ) 16 | 17 | func TreeSlicer(src, dstDir, workingDir string, targetSize uint64, gdal, osmium *docker.Container, elg, plg, rlg *log.Logger) { 18 | log.Printf("operating on: %s", src) 19 | 20 | src, err := filepath.Abs(src) 21 | if err != nil { 22 | elg.Fatal(err) 23 | } 24 | dstDir, err = filepath.Abs(dstDir) 25 | if err != nil { 26 | elg.Fatal(err) 27 | } 28 | workingDir, err = filepath.Abs(workingDir) 29 | if err != nil { 30 | elg.Fatal(err) 31 | } 32 | 33 | minX, minY, maxX, maxY, err := getExtent(src, gdal.Name) 34 | if err != nil { 35 | elg.Fatalf("extract.go | Slicer | Failed to get extent: %v", err) 36 | } 37 | 38 | lsp := leftSlicePoint(minX, maxX) 39 | lbb := formatBoundingBox(minX, minY, lsp, maxY) 40 | lp := slice(src, workingDir, lbb, osmium, elg, plg, rlg) 41 | 42 | rsp := rightSlicePoint(minX, maxX) 43 | rbb := formatBoundingBox(rsp, minY, maxX, maxY) 44 | rp := slice(src, workingDir, rbb, osmium, elg, plg, rlg) 45 | 46 | if strings.Contains(filepath.Base(src), "-tmp") { 47 | os.Remove(src) 48 | } else { 49 | log.Printf("sparing %s's life", filepath.Base(src)) 50 | } 51 | 52 | if size(lp, targetSize, elg, plg) { 53 | os.Rename(lp, filepath.Join(dstDir, filepath.Base(lp))) 54 | } else { 55 | TreeSlicer(lp, dstDir, workingDir, targetSize, gdal, osmium, elg, plg, rlg) 56 | } 57 | 58 | if size(rp, targetSize, elg, plg) { 59 | os.Rename(rp, filepath.Join(dstDir, filepath.Base(rp))) 60 | } else { 61 | TreeSlicer(rp, dstDir, workingDir, targetSize, gdal, osmium, elg, plg, rlg) 62 | } 63 | } 64 | 65 | func size(src string, targetMb uint64, elg, plg *log.Logger) bool { 66 | f, err := os.Stat(src) 67 | if err != nil { 68 | elg.Fatalf("slicer failed to get file info: %v\n", err) 69 | } 70 | 71 | if f.Size() > int64(targetMb*1024*1024) { 72 | plg.Printf("SLICE: target %s requires further slicing", filepath.Base(src)) 73 | return false 74 | } 75 | 76 | plg.Printf("SLICE: %s has reached target size; moving to safety.", filepath.Base(src)) 77 | return true 78 | } 79 | 80 | func IncompleteProgress(originalSrc, progressDir string, gdal *docker.Container, elg, rlg *log.Logger) string { 81 | minX, minY, maxX, maxY, err := getExtent(originalSrc, gdal.Name) 82 | if err != nil { 83 | elg.Println("failed to get extent for original source; cannot utilise previous progress") 84 | return "" 85 | } 86 | 87 | err = filepath.Walk(progressDir, func(path string, info os.FileInfo, err error) error { 88 | if !info.IsDir() { 89 | _, _, mx, _, err := getExtent(path, gdal.Name) 90 | if err != nil { 91 | elg.Printf("failed to get extent for %s source; cannot utilise previous progress\n", path) 92 | return err 93 | } 94 | if mx > minX { 95 | minX = mx 96 | } 97 | } 98 | return nil 99 | }) 100 | if err != nil { 101 | return "" 102 | } 103 | 104 | rlg.Printf("previously incomplete: %f, %f, %f, %f\n", minX, minY, maxX, maxY) 105 | 106 | return formatBoundingBox(minX, minY, maxX, maxY) 107 | } 108 | 109 | func slice(src, dst, bb string, osmium *docker.Container, elg, plg, rlg *log.Logger) string { 110 | f, err := os.CreateTemp(dst, "*-tmp.osm.pbf") 111 | if err != nil { 112 | elg.Fatalf("extract.go | Slicer | Failed to create temp file: %v", err) 113 | } 114 | defer f.Close() 115 | 116 | rlg.Printf("slicing %s >>> %s (%s)", filepath.Base(src), filepath.Base(f.Name()), bb) 117 | lp, err := Extract(src, f.Name(), bb, osmium) 118 | if err != nil { 119 | elg.Fatalf("extract.go | Slicer | Failed to extract slice: %v", err) 120 | } 121 | plg.Printf("SLICE: %s >>> %s (%s) success", filepath.Base(src), filepath.Base(f.Name()), bb) 122 | 123 | return lp 124 | } 125 | 126 | func formatBoundingBox(minX, minY, maxX, maxY float64) string { 127 | return fmt.Sprintf("%f,%f,%f,%f", minX, minY, maxX, maxY) 128 | } 129 | 130 | func getExtent(filePath, ogrContainerName string) (minX, minY, maxX, maxY float64, err error) { 131 | 132 | ap, err := filepath.Abs(filePath) 133 | if err != nil { 134 | return 0, 0, 0, 0, err 135 | } 136 | 137 | cmd := exec.Command("docker", "run", "--rm", "--mount", "type=bind,source="+ap+",target=/data", ogrContainerName, "ogrinfo", "-so", "-al", "/data") 138 | out, err := cmd.Output() 139 | if err != nil { 140 | return 0, 0, 0, 0, err 141 | } 142 | 143 | scanner := bufio.NewScanner(strings.NewReader(string(out))) 144 | 145 | var extent string 146 | 147 | for scanner.Scan() { 148 | if strings.HasPrefix(scanner.Text(), "Extent") { 149 | extent = scanner.Text() 150 | break 151 | } 152 | } 153 | 154 | if extent == "" { 155 | log.Println("extent not found") 156 | return 0, 0, 0, 0, fmt.Errorf("extent \"Extent\" not found") 157 | } 158 | 159 | re := regexp.MustCompile("[+-]?([0-9]*[.])?[0-9]+") 160 | coords := re.FindAllString(extent, -1) 161 | 162 | minX, _ = strconv.ParseFloat(coords[0], 64) 163 | minY, _ = strconv.ParseFloat(coords[1], 64) 164 | maxX, _ = strconv.ParseFloat(coords[2], 64) 165 | maxY, _ = strconv.ParseFloat(coords[3], 64) 166 | 167 | return minX, minY, maxX, maxY, nil 168 | } 169 | 170 | // returns the mid point for the box (which should be used to generate the next slice) 171 | func rightSlicePoint(minX, maxX float64) float64 { 172 | slicePoint := (minX + maxX) / 2 173 | // 0.01 is taken away to ensure no data is lost during the slicing process 174 | return slicePoint - 0.01 175 | } 176 | 177 | func leftSlicePoint(minX, maxX float64) float64 { 178 | slicePoint := (minX + maxX) / 2 179 | // 0.01 is added to ensure no data is lost during the slicing process 180 | return slicePoint + 0.01 181 | } 182 | -------------------------------------------------------------------------------- /internal/filesystem/filesystem.go: -------------------------------------------------------------------------------- 1 | package filesystem 2 | 3 | // Recursively walk through dir and append s to the front of each filename 4 | func AppendFilenameFrontDir(dir, s string) { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /internal/git/git.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/exec" 7 | ) 8 | 9 | type Repo struct { 10 | Url string 11 | Dst string 12 | } 13 | 14 | func (r Repo) Clone() error { 15 | if _, err := os.Stat(r.Dst); err == nil { 16 | log.Printf("git repo %s already exists, skipping clone", r.Dst) 17 | return nil 18 | } 19 | 20 | cmd := exec.Command("git", "clone", r.Url, r.Dst) 21 | cmd.Stdout = os.Stdout 22 | cmd.Stderr = os.Stderr 23 | if err := cmd.Run(); err != nil { 24 | return err 25 | } 26 | cmd.Wait() 27 | 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /internal/mbtiles/mbtiles.go: -------------------------------------------------------------------------------- 1 | package mbtiles 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math/rand" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/lambdajack/sequentially-generate-planet-mbtiles/internal/docker" 11 | ) 12 | 13 | func Generate(src, dst, coastline, landcover, config, process string, outAsDir bool, tilemaker *docker.Container, elg, plg, rlg *log.Logger) { 14 | src, err := filepath.Abs(src) 15 | if err != nil { 16 | elg.Fatal(err) 17 | } 18 | dst, err = filepath.Abs(dst) 19 | if err != nil { 20 | elg.Fatal(err) 21 | } 22 | 23 | if !outAsDir { 24 | var sb strings.Builder 25 | r := rand.Perm(10) 26 | for _, o := range r { 27 | sb.WriteString(fmt.Sprintf("%d", o)) 28 | } 29 | 30 | dst = filepath.Join(dst, sb.String()+".mbtiles") 31 | } 32 | 33 | tilemaker.Volumes = []docker.Volume{ 34 | { 35 | Container: "/src", 36 | Host: filepath.Dir(src), 37 | }, 38 | { 39 | Container: "/dst", 40 | Host: filepath.Dir(dst), 41 | }, 42 | { 43 | Container: "/coastline", 44 | Host: coastline, 45 | }, 46 | { 47 | Container: "/landcover", 48 | Host: landcover, 49 | }, 50 | { 51 | Container: "/config", 52 | Host: filepath.Dir(config), 53 | }, 54 | { 55 | Container: "/process", 56 | Host: filepath.Dir(process), 57 | }, 58 | } 59 | 60 | rlg.Printf("generating temporary mbtiles: %s from %s; directory output: %v\n", dst, src, outAsDir) 61 | err = tilemaker.Execute([]string{"--input", "/src/" + filepath.Base(src), "--output", "/dst/" + filepath.Base(dst), "--config", "/config/" + filepath.Base(config), "--process", "/process/" + filepath.Base(process)}) 62 | if err != nil { 63 | elg.Fatal(err) 64 | } 65 | plg.Printf("TILES: successfully generated %s from %s; directory output: %v\n", dst, src, outAsDir) 66 | } 67 | -------------------------------------------------------------------------------- /internal/planet/planet.go: -------------------------------------------------------------------------------- 1 | package planet 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/lambdajack/sequentially-generate-planet-mbtiles/internal/docker" 10 | ) 11 | 12 | func Generate(src, dst string, tippecanoe *docker.Container, elg, plg, rlg *log.Logger) string { 13 | fi, err := os.ReadDir(src) 14 | if err != nil { 15 | elg.Fatal(err) 16 | } 17 | 18 | var b []string 19 | for _, f := range fi { 20 | if !f.IsDir() { 21 | b = append(b, "/data/"+f.Name()) 22 | } 23 | } 24 | 25 | if len(b) == 0 { 26 | elg.Fatalf("cannot find any tiles to merge in %s - have you generated any?", src) 27 | } 28 | 29 | if _, err := os.Stat(filepath.Join(dst, "planet.mbtiles")); os.IsNotExist(err) { 30 | f, err := os.Create(filepath.Join(dst, "planet.mbtiles")) 31 | if err != nil { 32 | elg.Fatal(err) 33 | } 34 | f.Close() 35 | } 36 | 37 | tippecanoe.Volumes = []docker.Volume{ 38 | { 39 | Container: "/data", 40 | Host: src, 41 | }, 42 | { 43 | Container: "/merged", 44 | Host: dst, 45 | }, 46 | } 47 | 48 | rlg.Println("merging: ", strings.ReplaceAll(strings.Join(b, " "), "/data/", "...")) 49 | 50 | err = tippecanoe.Execute(append([]string{"tile-join", "--force", "--output=/merged/planet.mbtiles"}, b...)) 51 | if err != nil { 52 | elg.Fatalf("failed to merge mbtiles: %v", err) 53 | } 54 | plg.Println("MERGING: successfully merged: ", strings.ReplaceAll(strings.Join(b, " "), "/data/", "...")) 55 | 56 | return filepath.Join(dst, "planet.mbtiles") 57 | } 58 | -------------------------------------------------------------------------------- /internal/system/system.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/exec" 7 | "os/user" 8 | "runtime" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | func SetUserOwner(path string) error { 14 | u, err := user.Current() 15 | if err != nil { 16 | return err 17 | } 18 | 19 | if u.Name == "root" && runtime.GOOS == "linux" { 20 | u := os.Getenv("SUDO_UID") 21 | if u == "" { 22 | return err 23 | } 24 | ui, err := strconv.Atoi(u) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | g := os.Getenv("SUDO_GID") 30 | if g == "" { 31 | return err 32 | } 33 | gi, err := strconv.Atoi(g) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | err = os.Chown(path, ui, gi) 39 | if err != nil { 40 | log.Println("failed to set permissions for directories", path, err) 41 | } 42 | } 43 | return nil 44 | } 45 | 46 | func UserHomeDir() string { 47 | if runtime.GOOS == "linux" { 48 | if u, err := user.Current(); err == nil { 49 | if u.Username != "root" { 50 | return u.HomeDir 51 | } 52 | username := os.Getenv("SUDO_USER") 53 | if username != "" { 54 | u, err := user.Lookup(username) 55 | if err != nil { 56 | return "" 57 | } 58 | return u.HomeDir 59 | } 60 | } 61 | } 62 | d, err := os.UserHomeDir() 63 | if err != nil { 64 | return "" 65 | } 66 | return d 67 | } 68 | 69 | func UserCacheDir() string { 70 | if runtime.GOOS == "linux" { 71 | if UserHomeDir() != "" { 72 | return UserHomeDir() + "/.cache" 73 | } 74 | return "" 75 | } 76 | d, err := os.UserCacheDir() 77 | if err != nil { 78 | return "" 79 | } 80 | return d 81 | } 82 | 83 | func DockerIsSnap() bool { 84 | if runtime.GOOS == "linux" { 85 | sl, err := exec.Command("snap", "list", "docker").CombinedOutput() 86 | if err != nil { 87 | return false 88 | } 89 | return strings.Contains(string(sl), "docker") 90 | } 91 | return false 92 | } 93 | -------------------------------------------------------------------------------- /internal/validator/validator.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | type Validator struct { 4 | Errors map[string]string 5 | } 6 | 7 | func New() *Validator { 8 | return &Validator{Errors: make(map[string]string)} 9 | } 10 | 11 | func (v *Validator) Valid() bool { 12 | return len(v.Errors) == 0 13 | } 14 | 15 | func (v *Validator) AddError(key, message string) { 16 | if _, exists := v.Errors[key]; !exists { 17 | v.Errors[key] = message 18 | } 19 | } 20 | 21 | func (v *Validator) Check(ok bool, key, message string) { 22 | if !ok { 23 | v.AddError(key, message) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "os" 6 | 7 | sequentiallygenerateplanetmbtiles "github.com/lambdajack/sequentially-generate-planet-mbtiles/cmd/sequentially-generate-planet-mbtiles" 8 | ) 9 | 10 | //go:embed build/osmium/Dockerfile 11 | var df []byte 12 | 13 | func main() { 14 | os.Exit(sequentiallygenerateplanetmbtiles.EntryPoint(df)) 15 | } 16 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMain(m *testing.M) { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /pkg/execute/execute.go: -------------------------------------------------------------------------------- 1 | package execute 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | ) 9 | 10 | func OutputToConsole(command string) error { 11 | comArgs := strings.Split(strings.Trim(command, " "), " ") 12 | if len(comArgs) < 2 { 13 | return fmt.Errorf("execute.go | OutputToConsole | Invalid command string: %s", command) 14 | } 15 | 16 | cmd := exec.Command(comArgs[0], comArgs[1:]...) 17 | cmd.Stdout = os.Stdout 18 | cmd.Stderr = os.Stderr 19 | 20 | if err := cmd.Run(); err != nil { 21 | return err 22 | } 23 | cmd.Wait() 24 | return nil 25 | } 26 | --------------------------------------------------------------------------------