From 3f3dc3bb1761d73f6ecae0f83b0b143df20b38cd Mon Sep 17 00:00:00 2001 From: Ian Dees Date: Sat, 21 Dec 2024 21:38:30 -0600 Subject: [PATCH 1/5] Use a postgis image that is multi-arch --- Dockerfile | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4c46729..99ef22b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,17 @@ -FROM postgis/postgis:15-3.5 AS development_build +FROM ghcr.io/baosystems/postgis:15-3.5 AS development_build -RUN apt-get update \ -&& apt-get install -y --no-install-recommends \ +RUN apt-get update --quiet \ +&& apt-get install --quiet -y --no-install-recommends \ ca-certificates gnupg lsb-release locales \ wget curl \ git-core unzip \ netcat \ -&& locale-gen $LANG && update-locale LANG=$LANG +&& locale-gen $LANG && update-locale LANG=$LANG # Get packages -RUN apt-get update \ -&& apt-get install -y --no-install-recommends \ +RUN apt-get update --quiet \ +&& apt-get install --quiet -y --no-install-recommends \ make \ fonts-hanazono \ fonts-noto-cjk \ @@ -41,10 +41,10 @@ RUN apt-get update \ && apt-get autoremove --yes \ && rm -rf /var/lib/{apt,dpkg,cache,log}/ -RUN wget https://downloads.sourceforge.net/gs-fonts/ghostscript-fonts-std-8.11.tar.gz -RUN tar xvf ghostscript-fonts-std-8.11.tar.gz -RUN mkdir -p /usr/share/fonts/type1/ -RUN mv fonts/ /usr/share/fonts/type1/gsfonts +RUN wget --quiet https://downloads.sourceforge.net/gs-fonts/ghostscript-fonts-std-8.11.tar.gz \ +&& tar xf ghostscript-fonts-std-8.11.tar.gz \ +&& mkdir -p /usr/share/fonts/type1/ \ +&& mv fonts/ /usr/share/fonts/type1/gsfonts # Install python libraries @@ -61,7 +61,7 @@ RUN usermod -u 1000 postgres RUN chown -R 1000 ${HOME} USER postgres -RUN mkdir -p ${HOME}/openstreetmap-carto/data +RUN mkdir -p ${HOME}/openstreetmap-carto/data RUN mkdir -p ${HOME}/output RUN mkdir -p ${HOME}/pgdata WORKDIR ${HOME} From 724f3fff59aab89e94aa9c31b16ccd53139799d3 Mon Sep 17 00:00:00 2001 From: Ian Dees Date: Sat, 21 Dec 2024 21:40:06 -0600 Subject: [PATCH 2/5] Add num_frames field in the Python notebook --- make-images.ipynb | 73 ++++++++++++++++++++--------------------------- 1 file changed, 31 insertions(+), 42 deletions(-) diff --git a/make-images.ipynb b/make-images.ipynb index 0e07465..7d5b19e 100644 --- a/make-images.ipynb +++ b/make-images.ipynb @@ -11,53 +11,36 @@ "\n", "## Getting started\n", "\n", - "### If this is your first time using a notebook like this: \n", + "### If this is your first time using a notebook like this:\n", "\n", "1. You can use the up/down arrow keys of your keyboard to move between the different \"cells\" or regions of this notebook. The currently selected one will be highlighed on the left with a colour bar.\n", "2. You can \"run\" each of these cells/regions by either pressing the \"play\" button in the menu above, or by pressing CTRL+ENTER on your keyboard.\n", - "3. If you are in the \"editing\" mode where you type into a cell, pressing ESC will take you back to the mode where the arrow keys work. \n", + "3. If you are in the \"editing\" mode where you type into a cell, pressing ESC will take you back to the mode where the arrow keys work.\n", "\n", "### Uploading the OSM history file\n", "\n", - "Before we can do the rest of the steps in this notebook, you will have to upload an OSM history file that covers your region of interest. You can find the corresponding files on [Geofabrik's internal download server](https://osm-internal.download.geofabrik.de/?landing_page=true). Login with your OSM account to get to the `internal` files, and then find the page for the your region of interest. Ideally, find the smallest region that covers your area of interest to minimize the filesize you need to upload. \n", + "Before we can do the rest of the steps in this notebook, you will have to upload an OSM history file that covers your region of interest. You can find the corresponding files on [Geofabrik's internal download server](https://osm-internal.download.geofabrik.de/?landing_page=true). Login with your OSM account to get to the `internal` files, and then find the page for the your region of interest. Ideally, find the smallest region that covers your area of interest to minimize the filesize you need to upload.\n", "\n", "**The right file**: When you've picked your region, you will likely see two similarly named files listed: `regionname-internal.osh.pbf` and `regionname-latest-internal.osm.pbf`. The file you will want to download is the `regionname-internal.osh.pbf` (i.e. the one that includes `osh` and **no `latest`**).\n", "\n", "Once you have downloaded your file onto your computer, you need to upload it into your container:\n", "\n", - "* If you've launched this notebook in _MyBinder_, you can then open up the filebrowser on the top of the left hand side (The _file_ icon, or by pressing CTRL+SHIFT+F). \n", - "* If you've launched this notebook locally, you're likely in the _notebook_ interface, switch back to the _tree_ view and upload your file there (or put it into the shared folder which is accessible by both the container and your normal operating system). \n", + "* If you've launched this notebook in _MyBinder_, you can then open up the filebrowser on the top of the left hand side (The _file_ icon, or by pressing CTRL+SHIFT+F).\n", + "* If you've launched this notebook locally, you're likely in the _notebook_ interface, switch back to the _tree_ view and upload your file there (or put it into the shared folder which is accessible by both the container and your normal operating system).\n", "\n", "\n", - "## Starting to make the comparison images. \n", + "## Starting to make the comparison images.\n", "\n", - "When you run the code-cell below, it will generate some \"forms\" that will feel familiar form other web-forms, which you can use to enter all the necessary parameters to create your images. These forms allow you to add the _history file_ you have just uploaded, the two time-points you want to compare, the bounding box (i.e. the area you're interested in, which you can find e.g. [via bboxfinder](https://bboxfinder.com)) and the minimum/maximum zoom levels, if you want to create images at different zoom levels. \n", + "When you run the code-cell below, it will generate some \"forms\" that will feel familiar form other web-forms, which you can use to enter all the necessary parameters to create your images. These forms allow you to add the _history file_ you have just uploaded, the two time-points you want to compare, the bounding box (i.e. the area you're interested in, which you can find e.g. [via bboxfinder](https://bboxfinder.com)) and the minimum/maximum zoom levels, if you want to create images at different zoom levels.\n", "\n", "Now, run the cell below and then enter all the necessary information in the forms:" ] }, { - "cell_type": "code", - "execution_count": 3, - "id": "cf6cc023-ea39-46eb-885e-86a184dc1850", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "405e31685af24471838cd6587a52f032", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(Dropdown(description='File:', options=('external/argentina-internal.osh.pbf', 'external/argenti…" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "import ipywidgets as widgets\n", "import glob\n", @@ -76,7 +59,7 @@ "bbox = widgets.Text(\n", " placeholder='-61.975228,-33.749067,-61.953167,-33.733014',\n", " description='BBox:',\n", - " disabled=False \n", + " disabled=False\n", ")\n", "\n", "\n", @@ -100,7 +83,13 @@ " disabled=False,\n", ")\n", "\n", - "\n", + "num_frames = widgets.BoundedIntText(\n", + " min=2,\n", + " step=1,\n", + " value=2,\n", + " description='Number of frames:',\n", + " disabled=False\n", + ")\n", "\n", "widgets.VBox([\n", " filename,\n", @@ -110,19 +99,22 @@ " end_date,\n", " date_label,\n", " min_zoom_level,\n", - " max_zoom_level])" - ] + " max_zoom_level,\n", + " num_frames,\n", + "])" + ], + "id": "1b57c91aa6a007a6" }, { "cell_type": "markdown", "id": "0fa54f42-e3f7-428b-b604-9450f4c659d7", "metadata": {}, "source": [ - "Once you have set your parameters, you can run the cell below in the same way that you ran the cell above for creating the forms. \n", + "Once you have set your parameters, you can run the cell below in the same way that you ran the cell above for creating the forms.\n", "\n", - "Running the cell below will take a while, in particular if it's the first time you execute it, as it needs to download some external resources first. Subsequent runs will be faster. You can re-run it as many times as you want, and change any of the parameters using the forms above. \n", + "Running the cell below will take a while, in particular if it's the first time you execute it, as it needs to download some external resources first. Subsequent runs will be faster. You can re-run it as many times as you want, and change any of the parameters using the forms above.\n", "\n", - "Once you run the cell below, it will print a lot of output into this notebook, including some warnings. If everything works, the last things printed out will read *Generating comparison image for zoom XXX*, according to the zoom levels you picked. \n", + "Once you run the cell below, it will print a lot of output into this notebook, including some warnings. If everything works, the last things printed out will read *Generating comparison image for zoom XXX*, according to the zoom levels you picked.\n", "\n", "The resulting images will be saved as \"progress.filename.*parameters*.png/gif." ] @@ -160,13 +152,13 @@ "}\n", "\n", "\n", - "\u001b[K\u001b[?25h \u001b[27m] | reify:lodash: \u001b[32;40mhttp\u001b[0m \u001b[35mfetch\u001b[0m GET 200 https://registry.npmjs.\u001b[0m\u001b[K.o\u001b[0m\u001b[K\n", + "\u001B[K\u001B[?25h \u001B[27m] | reify:lodash: \u001B[32;40mhttp\u001B[0m \u001B[35mfetch\u001B[0m GET 200 https://registry.npmjs.\u001B[0m\u001B[K.o\u001B[0m\u001B[K\n", "added 64 packages, and audited 65 packages in 5s\n", "\n", "1 package is looking for funding\n", " run `npm fund` for details\n", "\n", - "\u001b[31m\u001b[1m5\u001b[22m\u001b[39m vulnerabilities (2 \u001b[33m\u001b[1mmoderate\u001b[22m\u001b[39m, 3 \u001b[31m\u001b[1mhigh\u001b[22m\u001b[39m)\n", + "\u001B[31m\u001B[1m5\u001B[22m\u001B[39m vulnerabilities (2 \u001B[33m\u001B[1mmoderate\u001B[22m\u001B[39m, 3 \u001B[31m\u001B[1mhigh\u001B[22m\u001B[39m)\n", "\n", "To address all issues, run:\n", " npm audit fix\n", @@ -1562,9 +1554,7 @@ ] } ], - "source": [ - "!./make.sh {filename.value} {start_date.value.isoformat()}Z {end_date.value.isoformat()}Z {bbox.value} {min_zoom_level.value} {max_zoom_level.value}" - ] + "source": "!./make.sh {filename.value} {start_date.value.isoformat()}Z {end_date.value.isoformat()}Z {bbox.value} {min_zoom_level.value} {max_zoom_level.value} {num_frames.value}" }, { "cell_type": "markdown", @@ -1573,12 +1563,11 @@ "source": [ "Enjoy mapping and visualizing!\n", "\n", - "If this all worked you should be able to see the output files in the filebrowser/file tree, named `progress.filename.parameters.png/gif.`. \n", + "If this all worked you should be able to see the output files in the filebrowser/file tree, named `progress.filename.parameters.png/gif.`.\n", "\n", "If you are running this in the MyBinder, you should download them to your computer to keep them permanently.\n", "\n", - "If you are running this locally, you can do this as well or alternatively move them into the shared \"output\" folder that is accessible from your host computer. \n", - "\n" + "If you are running this locally, you can do this as well or alternatively move them into the shared \"output\" folder that is accessible from your host computer." ] } ], From ca2dec34ba7102c7ceef279ec5c1118a1b29e033 Mon Sep 17 00:00:00 2001 From: Ian Dees Date: Sat, 21 Dec 2024 21:40:28 -0600 Subject: [PATCH 3/5] Adjust the make script to support multiple frames in the gif --- make.sh | 255 +++++++++++++++++++++++++++----------------------------- 1 file changed, 124 insertions(+), 131 deletions(-) diff --git a/make.sh b/make.sh index ada77bb..7ba5d55 100755 --- a/make.sh +++ b/make.sh @@ -3,10 +3,12 @@ set -o errexit -o nounset -o pipefail if [ $# -ge 1 ] && [ "$1" = "-h" ] ; then cat <<-END - Usage: $0 INPUT.osh.pbf BEFORETIME AFTERTIME BBOX - - TIME1 & TIME2 are ISO timestamps - bboxes can be found via http://bboxfinder.com/ + Usage: $0 INPUT.osh.pbf BEFORETIME AFTERTIME BBOX [MIN_ZOOM] [MAX_ZOOM] [NUM_FRAMES] + + BEFORETIME & AFTERTIME are ISO-8601 timestamps + BBOX is a comma-separated long/lat bounding box (left,bottom,right,top) and can be found via http://bboxfinder.com/ + MIN_ZOOM and MAX_ZOOM are optional zoom levels (default: 6 and 12) + NUM_FRAMES is the number of frames to generate for the GIF (default: 2) END exit 0 fi @@ -15,159 +17,150 @@ INPUT_FILE=$(realpath "${1:?Arg 1 should be the path to the pbf file}") TIME_BEFORE=${2:?Arg 2 should be the ISO timestamp for the before time} TIME_AFTER=${3:?Arg 3 should be the ISO timestamp for the after time} BBOX=${4:-"world"} +BBOX_COMMA="${BBOX// /,}" +BBOX_SPACE="${BBOX//,/ }" +MIN_ZOOM=${5:-6} +MAX_ZOOM=${6:-12} +NUM_FRAMES=${7:-2} + # for planet-latest.osm.obf we calculate the "planet" part PREFIX=$(basename "$INPUT_FILE") PREFIX=${PREFIX%%.osh.pbf} PREFIX=${PREFIX%%-latest} PREFIX=${PREFIX%%-internal} PREFIX=${PREFIX//-/_} -MIN_ZOOM=${5:-6} -MAX_ZOOM=${6:-12} ROOT="$(realpath "$(dirname "$0")")" cd "$ROOT" || exit -BBOX_COMMA="${BBOX// /,}" -BBOX_SPACE="${BBOX//,/ }" - -if [ ! -s "$INPUT_FILE" ] ; then - echo "Input file $INPUT_FILE not found" 1>&2 - exit 1 -fi - -if [ "$BBOX" = "world" ] ; then - PBF_FILE="$INPUT_FILE" -else - PBF_FILE="$(realpath "$PREFIX.$BBOX.osh.pbf")" - if [ "$INPUT_FILE" -nt "$PBF_FILE" ] ; then - echo "Extracting the OSM history for just this bounding box $BBOX" - NEWFILE=$(mktemp -p . "tmp.extract.${PREFIX}.XXXXXX.osm.pbf") - osmium extract --with-history --overwrite -o "$NEWFILE" --bbox "$BBOX_COMMA" "$INPUT_FILE" - mv "$NEWFILE" "$PBF_FILE" - fi -fi - -BEFORE_FILENAME="$(realpath "${PREFIX}.$TIME_BEFORE.$BBOX_COMMA.osm.pbf")" -if [ "$PBF_FILE" -nt "$BEFORE_FILENAME" ] ; then - NEWFILE=$(mktemp -p . tmp.time1.XXXXXX.osm.pbf) - echo "Extracting 'before' data..." - osmium time-filter --overwrite -o "$NEWFILE" "$PBF_FILE" "$TIME_BEFORE" - mv "$NEWFILE" "$BEFORE_FILENAME" -fi - -AFTER_FILENAME="$(realpath "${PREFIX}.$TIME_AFTER.$BBOX_COMMA.osm.pbf")" -if [ "$PBF_FILE" -nt "$AFTER_FILENAME" ] ; then - NEWFILE=$(mktemp -p . tmp.time2.XXXXXX.osm.pbf) - echo "Extracting 'after' data..." - osmium time-filter --overwrite -o "$NEWFILE" "$PBF_FILE" "$TIME_AFTER" - mv "$NEWFILE" "$AFTER_FILENAME" +PBF_FILE="$(realpath "$PREFIX.$BBOX.osh.pbf")" +if [ "$INPUT_FILE" -nt "$PBF_FILE" ] ; then + echo "Extracting the OSM history for just this bounding box $BBOX" + NEWFILE=$(mktemp -p . "tmp.extract.${PREFIX}.XXXXXX.osm.pbf") + osmium extract --with-history --overwrite -o "$NEWFILE" --bbox "$BBOX_COMMA" "$INPUT_FILE" + mv "$NEWFILE" "$PBF_FILE" fi - if [ ! -s "$ROOT/openstreetmap-carto/node_modules/.bin/carto" ] ; then - cd "$ROOT/openstreetmap-carto" - echo "Installing carto into $ROOT/openstreetmap-carto/node_modules with npm..." - npm init -y - npm install carto -q + cd "$ROOT/openstreetmap-carto" + echo "Installing carto into $ROOT/openstreetmap-carto/node_modules with npm..." + npm init -y + npm install carto -q fi -if [ ! -s "$ROOT/openstreetmap-carto/project.xml" ] ; then - cd "$ROOT" - if [ ! -e "$ROOT/openstreetmap-carto" ] ; then - git submodule update - fi - cd "$ROOT/openstreetmap-carto" - if [ ! -s project.xml ] || [ project.mml -nt project.xml ] ; then - TMP=$(mktemp -p . tmp.project.XXXXXX.xml) - ./node_modules/.bin/carto -a 3.0.0 project.mml > "$TMP" - mv "$TMP" project.xml - fi +if [ ! -s "$ROOT/openstreetmap-carto/project.xml" ] ; then + cd "$ROOT" + if [ ! -e "$ROOT/openstreetmap-carto" ] ; then + git submodule update + fi + cd "$ROOT/openstreetmap-carto" + if [ ! -s project.xml ] || [ project.mml -nt project.xml ] ; then + TMP=$(mktemp -p . tmp.project.XXXXXX.xml) + ./node_modules/.bin/carto -a 3.0.0 project.mml > "$TMP" + mv "$TMP" project.xml + fi fi if [ "$(psql -At -c "select count(*) from pg_database where datname = 'gis';")" = "0" ] ; then - echo "Creating gis database..." - createdb gis - psql -d gis -c "create extension postgis;" - psql -d gis -c "create extension hstore;" + echo "Creating gis database..." + createdb gis + psql -d gis -c "create extension postgis;" + psql -d gis -c "create extension hstore;" fi if [ ! -e "$ROOT/openstreetmap-carto/data/.external-data-done" ] ; then - cd "$ROOT/openstreetmap-carto/" - echo "Downloading external datasets..." - ./scripts/get-external-data.py - touch data/.external-data-done - cd "$ROOT" -fi - -if [ "$BEFORE_FILENAME" -nt "$ROOT/.$PREFIX.$TIME_BEFORE.$BBOX_COMMA.generated" ] ; then - cd "$ROOT/openstreetmap-carto" - osm2pgsql -G --hstore --style openstreetmap-carto.style --tag-transform-script openstreetmap-carto.lua -d gis "$BEFORE_FILENAME" - psql -d gis -f indexes.sql - touch "$ROOT/.$PREFIX.$TIME_BEFORE.$BBOX_COMMA.generated" + cd "$ROOT/openstreetmap-carto/" + echo "Downloading external datasets..." + ./scripts/get-external-data.py + touch data/.external-data-done + cd "$ROOT" fi -cd "$ROOT" - -for ZOOM in $(seq "$MIN_ZOOM" "$MAX_ZOOM") ; do - if [ "$ROOT/.$PREFIX.$TIME_BEFORE.$BBOX_COMMA.generated" -nt "$PREFIX.$TIME_BEFORE.$BBOX_COMMA.z${ZOOM}.png" ] ; then - # eventually the image is too large, so then just break out of the loop - echo "Generating zoom ${ZOOM} level" - nik4.py openstreetmap-carto/project.xml "$PREFIX.$TIME_BEFORE.$BBOX_COMMA.z${ZOOM}.png" -b $BBOX_SPACE -z "$ZOOM" || break - NEW="$(mktemp tmp.XXXXXX.png)" - #gm convert "$PREFIX.$TIME_BEFORE.$BBOX_COMMA.z${ZOOM}.png" -background white -label "${TIME_BEFORE}" -gravity center -append "$NEW" - #mv "$NEW" "$PREFIX.$TIME_BEFORE.$BBOX_COMMA.z${ZOOM}.png" - gm convert "$PREFIX.$TIME_BEFORE.$BBOX_COMMA.z${ZOOM}.png" -background white -label "${TIME_BEFORE}, data © OpenStreetMap contributors, ODbL" -gravity center -append "$NEW" - mv "$NEW" "$PREFIX.$TIME_BEFORE.$BBOX_COMMA.z${ZOOM}.png" - fi -done - -if [ "$AFTER_FILENAME" -nt "$ROOT/.$PREFIX.$TIME_AFTER.$BBOX_COMMA.generated" ] ; then - cd "$ROOT/openstreetmap-carto" - osm2pgsql -G --hstore --style openstreetmap-carto.style --tag-transform-script openstreetmap-carto.lua -d gis "$AFTER_FILENAME" - psql -d gis -f indexes.sql - touch "$ROOT/.$PREFIX.$TIME_AFTER.$BBOX_COMMA.generated" -fi - -cd "$ROOT" -for ZOOM in $(seq "$MIN_ZOOM" "$MAX_ZOOM") ; do - if [ "$ROOT/.$PREFIX.$TIME_AFTER.$BBOX_COMMA.generated" -nt "$PREFIX.$TIME_AFTER.$BBOX_COMMA.z${ZOOM}.png" ] ; then - # eventually the image is too large, so then just break out of the loop - echo "Generating zoom ${ZOOM} level" - nik4.py openstreetmap-carto/project.xml "$PREFIX.$TIME_AFTER.$BBOX_COMMA.z${ZOOM}.png" -b $BBOX_SPACE -z "$ZOOM" || break - NEW="$(mktemp tmp.XXXXXX.png)" - #gm convert "$PREFIX.$TIME_AFTER.$BBOX_COMMA.z${ZOOM}.png" -background white -label "$TIME_AFTER" -gravity center -append "$NEW" - #mv "$NEW" "$PREFIX.$TIME_AFTER.$BBOX_COMMA.z${ZOOM}.png" - gm convert "$PREFIX.$TIME_AFTER.$BBOX_COMMA.z${ZOOM}.png" -background white -label "${TIME_AFTER}, data © OpenStreetMap contributors, ODbL" -gravity center -append "$NEW" - mv "$NEW" "$PREFIX.$TIME_AFTER.$BBOX_COMMA.z${ZOOM}.png" - fi +# Function to generate ISO-8601 timestamps between two times +generate_timestamps() { + local start_time=$1 + local end_time=$2 + local num_stops=$3 + python3 - < Date: Sun, 22 Dec 2024 08:58:46 -0600 Subject: [PATCH 4/5] Switch back to postgis/postgis image --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 99ef22b..232a932 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/baosystems/postgis:15-3.5 AS development_build +FROM postgis/postgis:15-3.5 AS development_build RUN apt-get update --quiet \ && apt-get install --quiet -y --no-install-recommends \ From 5411e30b801018b1d2166bae59c77792e900cd5c Mon Sep 17 00:00:00 2001 From: Ian Dees Date: Sun, 22 Dec 2024 09:02:07 -0600 Subject: [PATCH 5/5] Be consistent about using tabs in the usage message --- make.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/make.sh b/make.sh index 7ba5d55..c86aeb6 100755 --- a/make.sh +++ b/make.sh @@ -7,8 +7,8 @@ if [ $# -ge 1 ] && [ "$1" = "-h" ] ; then BEFORETIME & AFTERTIME are ISO-8601 timestamps BBOX is a comma-separated long/lat bounding box (left,bottom,right,top) and can be found via http://bboxfinder.com/ - MIN_ZOOM and MAX_ZOOM are optional zoom levels (default: 6 and 12) - NUM_FRAMES is the number of frames to generate for the GIF (default: 2) + MIN_ZOOM and MAX_ZOOM are optional zoom levels (default: 6 and 12) + NUM_FRAMES is the number of frames to generate for the GIF (default: 2) END exit 0 fi