Peter Yang commited on
Commit
dc5de08
Β·
1 Parent(s): ce4dfeb

add application files

Browse files
Dockerfile ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.10
3
+
4
+ # Set the working directory in the container
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies
8
+ RUN apt-get update \
9
+ && apt-get install -y --no-install-recommends \
10
+ libpq-dev \
11
+ gcc \
12
+ gdal-bin \
13
+ libgdal-dev \
14
+ python3-gdal \
15
+ && rm -rf /var/lib/apt/lists/*
16
+
17
+ # Set environment variables for GDAL
18
+ ENV GDAL_CONFIG=/usr/bin/gdal-config
19
+ ENV CPLUS_INCLUDE_PATH=/usr/include/gdal
20
+ ENV C_INCLUDE_PATH=/usr/include/gdal
21
+
22
+ # Copy the local files to the container
23
+ COPY . .
24
+
25
+ # Install Python dependencies
26
+ RUN pip install --no-cache-dir --upgrade pip \
27
+ && pip install --no-cache-dir -r requirements.txt
28
+
29
+ # Expose the port Streamlit will run on
30
+ EXPOSE 8501
31
+
32
+ # Run Streamlit
33
+ CMD ["streamlit", "run", "app.py", "--server.address=0.0.0.0"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Qiusheng Wu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
Procfile ADDED
@@ -0,0 +1 @@
 
 
1
+ web: sh setup.sh && streamlit run Home.py
README.md CHANGED
@@ -1,13 +1,23 @@
1
- ---
2
- title: Nextdrought
3
- emoji: 🐒
4
- colorFrom: pink
5
- colorTo: purple
6
- sdk: streamlit
7
- sdk_version: 1.21.0
8
- app_file: app.py
9
- pinned: false
10
- license: apache-2.0
11
- ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
1
+ # streamlit-geospatial
2
+
3
+ A streamlit multipage app for geospatial applications. It can be deployed to [Streamlit Cloud](https://streamlit.io/cloud), [Heroku](https://heroku.com/), or [MyBinder](https://mybinder.org/).
4
+
5
+ [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/giswqs/streamlit-geospatial/master?urlpath=proxy/8501/)
6
+
7
+ - Web app: <https://streamlit.geemap.org>
8
+ - Source code: <https://github.com/giswqs/streamlit-geospatial>
9
+
10
+ ## Instructions
11
+
12
+ 1. For the GitHub repository to your GitHub account.
13
+ 2. Customize the sidebar by changing the sidebar text and logo in each Python file.
14
+ 3. Find your favorite emoji from https://emojipedia.org.
15
+ 4. Add a new app to the `pages/` directory with an emoji in the file name, e.g., 1_πŸš€_Chart.py.
16
+
17
+ ## Demo
18
+
19
+ ![](https://i.imgur.com/6lj0oAO.png)
20
+
21
+ ## Real Estate Data and Market Trends
22
+
23
+ ![](https://i.imgur.com/Z3dk6Tr.gif)
app.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import leafmap.foliumap as leafmap
3
+
4
+ st.set_page_config(layout="wide")
5
+
6
+
7
+ st.title("Streamlit for Geospatial Applications")
8
+
9
+ st.markdown(
10
+ """
11
+ This multi-page web app demonstrates various interactive web apps created using [streamlit](https://streamlit.io) and open-source mapping libraries,
12
+ such as [leafmap](https://leafmap.org), [geemap](https://geemap.org), [pydeck](https://deckgl.readthedocs.io), and [kepler.gl](https://docs.kepler.gl/docs/keplergl-jupyter).
13
+ This is an open-source project and you are very welcome to contribute your comments, questions, resources, and apps as [issues](https://github.com/giswqs/streamlit-geospatial/issues) or
14
+ [pull requests](https://github.com/giswqs/streamlit-geospatial/pulls) to the [GitHub repository](https://github.com/giswqs/streamlit-geospatial).
15
+
16
+ """
17
+ )
18
+
19
+ st.info("Click on the left sidebar menu to navigate to the different apps.")
20
+
21
+ st.subheader("Timelapse of Satellite Imagery")
22
+ st.markdown(
23
+ """
24
+ The following timelapse animations were created using the Timelapse web app. Click `Timelapse` on the left sidebar menu to create your own timelapse for any location around the globe.
25
+ """
26
+ )
27
+
28
+ row1_col1, row1_col2 = st.columns(2)
29
+ with row1_col1:
30
+ st.image("https://github.com/giswqs/data/raw/main/timelapse/spain.gif")
31
+ st.image("https://github.com/giswqs/data/raw/main/timelapse/las_vegas.gif")
32
+
33
+ with row1_col2:
34
+ st.image("https://github.com/giswqs/data/raw/main/timelapse/goes.gif")
35
+ st.image("https://github.com/giswqs/data/raw/main/timelapse/fire.gif")
data/cog_files.txt ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ https://www.maxar.com/open-data/california-colorado-fires
2
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2018-02-16/pine-gulch-fire20/1030010076004E00.tif
3
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2018-08-18/pine-gulch-fire20/1040010041D3B300.tif
4
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2018-11-13/grizzly-creek-fire20/1040010045785200.tif
5
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2018-11-13/grizzly-creek-fire20/10400100443AEC00.tif
6
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2019-02-06/czu-lightning-complex-fire/104001004941E100.tif
7
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2019-02-18/cameron-peak-fire20/103001008DA5B500.tif
8
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2019-02-22/czu-lightning-complex-fire/103001008DB2E200.tif
9
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2019-04-01/grizzly-creek-fire20/104001004881EF00.tif
10
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2019-04-17/czu-lightning-complex-fire/103001008F905300.tif
11
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2019-04-17/czu-lightning-complex-fire/1030010092B22200.tif
12
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2019-06-27/czu-lightning-complex-fire/1030010094A52300.tif
13
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2019-09-08/czu-lightning-complex-fire/103001009C9FBB00.tif
14
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2019-09-24/lnu-lightning-complex-fire/103001009A079B00.tif
15
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2019-10-05/czu-lightning-complex-fire/103001009C10F800.tif
16
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2019-10-05/czu-lightning-complex-fire/103001009A266800.tif
17
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2019-11-04/czu-lightning-complex-fire/1050010019917900.tif
18
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2019-11-04/czu-lightning-complex-fire/1050010019917800.tif
19
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2019-11-18/czu-lightning-complex-fire/1050010019C2F600.tif
20
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2019-11-28/cameron-peak-fire20/103001009D72E000.tif
21
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2019-12-10/czu-lightning-complex-fire/105001001A3A8700.tif
22
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2019-12-28/lnu-lightning-complex-fire/10300100A1972700.tif
23
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2019-12-28/lnu-lightning-complex-fire/103001009F5D6B00.tif
24
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2020-01-15/cameron-peak-fire20/1040010057992100.tif
25
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2020-04-15/lnu-lightning-complex-fire/10300100A4B23600.tif
26
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2020-04-23/czu-lightning-complex-fire/10300100A589D100.tif
27
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2020-05-09/lnu-lightning-complex-fire/10300100A332EE00.tif
28
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2020-05-23/river-carmel-fires/10300100A77E9400.tif
29
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2020-05-23/river-carmel-fires/10300100A500A500.tif
30
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2020-05-24/river-carmel-fires/105001001D64E200.tif
31
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2020-06-27/lnu-lightning-complex-fire/10300100A8663800.tif
32
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2020-06-30/river-carmel-fires/10300100A9D60C00.tif
33
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2020-06-30/czu-lightning-complex-fire/10300100A8C66400.tif
34
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2020-06-30/czu-lightning-complex-fire/10300100A8892900.tif
35
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2020-07-11/czu-lightning-complex-fire/10300100AB381200.tif
36
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2020-07-11/czu-lightning-complex-fire/10300100AA180600.tif
37
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2020-07-13/pine-gulch-fire20/10300100AA57D700.tif
38
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2020-07-20/lnu-lightning-complex-fire/104001005C529000.tif
39
+ https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2020-07-28/pine-gulch-fire20/104001005DB06E00.tif
40
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-14/pine-gulch-fire20/10300100AAC8DD00.tif
41
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-16/pine-gulch-fire20/104001005D4A6100.tif
42
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-17/grizzly-creek-fire20/10300100ACCA3700.tif
43
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-17/cameron-peak-fire20/10300100AB4ED400.tif
44
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-20/swir-cog/104A0100606FFE00.tif
45
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-20/pine-gulch-fire20/10300100ACD06200.tif
46
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-20/pine-gulch-fire20/10300100AAD4A000.tif
47
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-20/pine-gulch-fire20/10300100AA293800.tif
48
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-20/lnu-lightning-complex-fire/10400100606FFE00.tif
49
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-21/river-carmel-fires/10300100ACBA2B00.tif
50
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-21/river-carmel-fires/10300100AA49F600.tif
51
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-21/lnu-lightning-complex-fire/104001005C1AC900.tif
52
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-21/river-carmel-fires/104001005F9F5300.tif
53
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-21/river-carmel-fires/104001005F453300.tif
54
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-21/river-carmel-fires/10300100ADC14400.tif
55
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-21/czu-lightning-complex-fire/104001005F43D400.tif
56
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-23/grizzly-creek-fire20/104001005FA09C00.tif
57
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-23/grizzly-creek-fire20/104001005DC71000.tif
58
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-26/river-carmel-fires/105001001F58F000.tif
59
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-26/lnu-lightning-complex-fire/10300100AC163A00.tif
60
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-29/river-carmel-fires/10300100AAD27500.tif
61
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-08-29/river-carmel-fires/10300100A9C75A00.tif
62
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-09-03/cameron-peak-fire20/1040010060188800.tif
63
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-09-03/cameron-peak-fire20/104001005F7E6500.tif
64
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-09-03/cameron-peak-fire20/10300100AE685A00.tif
65
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-09-04/cameron-peak-fire20/1040010060761C00.tif
66
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-10-05/cameron-peak-fire20/104001006113B700.tif
67
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-10-05/cameron-peak-fire20/10400100610CD400.tif
68
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-10-12/cameron-peak-fire20/1040010062B14C00.tif
69
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-10-12/cameron-peak-fire20/10400100626BFA00.tif
70
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-10-12/cameron-peak-fire20/10400100622A6600.tif
71
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-10-12/cameron-peak-fire20/10400100606B6300.tif
72
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-10-12/cameron-peak-fire20/104001005F908800.tif
73
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-10-15/cameron-peak-fire20/10500100205EDA00.tif
74
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-10-15/cameron-peak-fire20/10500100205ED900.tif
75
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-10-22/east-troublesome-fire20/10300100B0004A00.tif
76
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-10-22/east-troublesome-fire20/10300100AD0D1200.tif
77
+ https://opendata.digitalglobe.com/events/california-fire-2020/post-event/2020-10-22/east-troublesome-fire20/10300100AD0CA600.tif
data/html/sfo_buildings.html ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <!-- Include the CesiumJS JavaScript and CSS files -->
6
+ <script src="https://cesium.com/downloads/cesiumjs/releases/1.88/Build/Cesium/Cesium.js"></script>
7
+ <link href="https://cesium.com/downloads/cesiumjs/releases/1.88/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
8
+ </head>
9
+ <body>
10
+ <div id="cesiumContainer"></div>
11
+ <script>
12
+ // Your access token can be found at: https://cesium.com/ion/tokens.
13
+ // Replace `your_access_token` with your Cesium ion access token.
14
+
15
+ Cesium.Ion.defaultAccessToken = 'your_access_token';
16
+
17
+ // Initialize the Cesium Viewer in the HTML element with the `cesiumContainer` ID.
18
+ const viewer = new Cesium.Viewer('cesiumContainer', {
19
+ terrainProvider: Cesium.createWorldTerrain()
20
+ });
21
+ // Add Cesium OSM Buildings, a global 3D buildings layer.
22
+ const buildingTileset = viewer.scene.primitives.add(Cesium.createOsmBuildings());
23
+ // Fly the camera to San Francisco at the given longitude, latitude, and height.
24
+ viewer.camera.flyTo({
25
+ destination : Cesium.Cartesian3.fromDegrees(-122.4175, 37.655, 400),
26
+ orientation : {
27
+ heading : Cesium.Math.toRadians(0.0),
28
+ pitch : Cesium.Math.toRadians(-15.0),
29
+ }
30
+ });
31
+ </script>
32
+ </div>
33
+ </body>
34
+ </html>
data/realtor_data_dict.csv ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Name,Label,Description
2
+ median_listing_price,Median Listing Price,The median listing price within the specified geography during the specified month.
3
+ median_listing_price_mm,Median Listing Price M/M,The percentage change in the median listing price from the previous month.
4
+ median_listing_price_yy,Median Listing Price Y/Y,The percentage change in the median listing price from the same month in the previous year.
5
+ active_listing_count,Active Listing Count,"The count of active listings within the specified geography during the specified month. The active listing count tracks the number of for sale properties on the market, excluding pending listings where a pending status is available. This is a snapshot measure of how many active listings can be expected on any given day of the specified month."
6
+ active_listing_count_mm,Active Listing Count M/M,The percentage change in the active listing count from the previous month.
7
+ active_listing_count_yy,Active Listing Count Y/Y,The percentage change in the active listing count from the same month in the previous year.
8
+ median_days_on_market,Days on Market,The median number of days property listings spend on the market within the specified geography during the specified month. Time spent on the market is defined as the time between the initial listing of a property and either its closing date or the date it is taken off the market.
9
+ median_days_on_market_mm,Days on Market M/M,The percentage change in the median days on market from the previous month.
10
+ median_days_on_market_yy,Days on Market Y/Y,The percentage change in the median days on market from the same month in the previous year.
11
+ new_listing_count,New Listing Count,The count of new listings added to the market within the specified geography. The new listing count represents a typical week’s worth of new listings in a given month. The new listing count can be multiplied by the number of weeks in a month to produce a monthly new listing count.
12
+ new_listing_count_mm,New Listing Count M/M,The percentage change in the new listing count from the previous month.
13
+ new_listing_count_yy,New Listing Count Y/Y,The percentage change in the new listing count from the same month in the previous year.
14
+ price_increased_count,Price Increase Count,The count of listings which have had their price increased within the specified geography. The price increase count represents a typical week’s worth of listings which have had their price increased in a given month. The price increase count can be multiplied by the number of weeks in a month to produce a monthly price increase count.
15
+ price_increased_count_mm,Price Increase Count M/M,The percentage change in the price increase count from the previous month.
16
+ price_increased_count_yy,Price Increase Count Y/Y,The percentage change in the price increase count from the same month in the previous year.
17
+ price_reduced_count,Price Decrease Count,The count of listings which have had their price reduced within the specified geography. The price decrease count represents a typical week’s worth of listings which have had their price reduced in a given month. The price decrease count can be multiplied by the number of weeks in a month to produce a monthly price decrease count.
18
+ price_reduced_count_mm,Price Decrease Count M/M,The percentage change in the price decrease count from the previous month.
19
+ price_reduced_count_yy,Price Decrease Count Y/Y,The percentage change in the price decrease count from the same month in the previous year.
20
+ pending_listing_count,Pending Listing Count,"The count of pending listings within the specified geography during the specified month, if a pending definition is available for that geography. This is a snapshot measure of how many pending listings can be expected on any given day of the specified month."
21
+ pending_listing_count_mm,Pending Listing Count M/M,The percentage change in the pending listing count from the previous month.
22
+ pending_listing_count_yy,Pending Listing Count Y/Y,The percentage change in the pending listing count from the same month in the previous year.
23
+ median_listing_price_per_square_foot,Median List Price Per Sqft,The median listing price per square foot within the specified geography during the specified month.
24
+ median_listing_price_per_square_foot_mm,Median List Price Per Sqft M/M,The percentage change in the median listing price per square foot from the previous month.
25
+ median_listing_price_per_square_foot_yy,Median List Price Per Sqft Y/Y,The percentage change in the median listing price per square foot from the same month in the previous year.
26
+ median_square_feet,Median Listing Sqft,The median listing square feet within the specified geography during the specified month.
27
+ median_square_feet_mm,Median Listing Sqft M/M,The percentage change in the median listing square feet from the previous month.
28
+ median_square_feet_yy,Median Listing Sqft Y/Y,The percentage change in the median listing square feet from the same month in the previous year.
29
+ average_listing_price,Avg Listing Price,The average listing price within the specified geography during the specified month.
30
+ average_listing_price_mm,Avg Listing Price M/M,The percentage change in the average listing price from the previous month.
31
+ average_listing_price_yy,Avg Listing Price Y/Y,The percentage change in the average listing price from the same month in the previous year.
32
+ total_listing_count,Total Listing Count,The total of both active listings and pending listings within the specified geography during the specified month. This is a snapshot measure of how many total listings can be expected on any given day of the specified month.
33
+ total_listing_count_mm,Total Listing Count M/M,The percentage change in the total listing count from the previous month.
34
+ total_listing_count_yy,Total Listing Count Y/Y,The percentage change in the total listing count from the same month in the previous year.
35
+ pending_ratio,Pending Ratio,The ratio of the pending listing count to the active listing count within the specified geography during the specified month.
36
+ pending_ratio_mm,Pending Ratio M/M,The change in the pending ratio from the previous month.
37
+ pending_ratio_yy,Pending Ratio Y/Y,The change in the pending ratio from the same month in the previous year.
data/scotland_xyz.tsv ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Name URL
2
+ Ordnance Survey - Air Photos, 1944-1950 - 1:10,560 https://geo.nls.uk/maps/air-photos/{z}/{x}/{y}.png
3
+ Ordnance Survey - Six Inch Scotland, 1843-1882 - 1:10,560 https://mapseries-tilesets.s3.amazonaws.com/os/6inchfirst/{z}/{x}/{y}.png
4
+ War Office, Great Britain 1:25,000. GSGS 3906, 1940-43 https://mapseries-tilesets.s3.amazonaws.com/gsgs3906/{z}/{x}/{y}.png
5
+ Roy - Roy Highlands, 1747-1752 - 1:36000 https://mapseries-tilesets.s3.amazonaws.com/roy/highlands/{z}/{x}/{y}.png
6
+ Roy - Roy Lowlands, 1752-1755 - 1:36000 https://mapseries-tilesets.s3.amazonaws.com/roy/lowlands/{z}/{x}/{y}.png
7
+ Great Britain - OS 1:10,560, 1949-1970 https://mapseries-tilesets.s3.amazonaws.com/os/britain10knatgrid/{z}/{x}/{y}.png
8
+ Great Britain - Bartholomew Half Inch, 1897-1907 https://mapseries-tilesets.s3.amazonaws.com/bartholomew_great_britain/{z}/{x}/{y}.png
9
+ OS 25 inch, 1892-1914 - Scotland South https://mapseries-tilesets.s3.amazonaws.com/25_inch/scotland_1/{z}/{x}/{y}.png
10
+ OS 25 inch, 1892-1914 - Scotland North https://mapseries-tilesets.s3.amazonaws.com/25_inch/scotland_2/{z}/{x}/{y}.png
11
+ OS 25 inch, 1892-1914 - Bedfordshire https://mapseries-tilesets.s3.amazonaws.com/25_inch/bedfordshire/{z}/{x}/{y}.png
12
+ OS 25 inch, 1892-1914 - Berkshire https://mapseries-tilesets.s3.amazonaws.com/25_inch/berkshire/{z}/{x}/{y}.png
13
+ OS 25 inch, 1892-1914 - Buckinghamshire https://mapseries-tilesets.s3.amazonaws.com/25_inch/buckingham/{z}/{x}/{y}.png
14
+ OS 25 inch, 1892-1914 - Cambridgeshire https://mapseries-tilesets.s3.amazonaws.com/25_inch/cambridge/{z}/{x}/{y}.png
15
+ OS 25 inch, 1892-1914 - Cheshire https://mapseries-tilesets.s3.amazonaws.com/25_inch/cheshire/{z}/{x}/{y}.png
16
+ OS 25 inch, 1892-1914 - Cornwall https://mapseries-tilesets.s3.amazonaws.com/25_inch/cornwall/{z}/{x}/{y}.png
17
+ OS 25 inch, 1892-1914 - Cumberland https://mapseries-tilesets.s3.amazonaws.com/25_inch/cumberland/{z}/{x}/{y}.png
18
+ OS 25 inch, 1892-1914 - Devon https://mapseries-tilesets.s3.amazonaws.com/25_inch/devon/{z}/{x}/{y}.png
19
+ OS 25 inch, 1892-1914 - Dorset https://mapseries-tilesets.s3.amazonaws.com/25_inch/dorset/{z}/{x}/{y}.png
20
+ OS 25 inch, 1892-1914 - Durham https://mapseries-tilesets.s3.amazonaws.com/25_inch/durham/{z}/{x}/{y}.png
21
+ OS 25 inch, 1892-1914 - Essex https://mapseries-tilesets.s3.amazonaws.com/25_inch/essex/{z}/{x}/{y}.png
22
+ OS 25 inch, 1892-1914 - Gloucestershire https://mapseries-tilesets.s3.amazonaws.com/25_inch/gloucestershire/{z}/{x}/{y}.png
23
+ OS 25 inch, 1892-1914 - Hampshire https://mapseries-tilesets.s3.amazonaws.com/25_inch/hampshire/{z}/{x}/{y}.png
24
+ OS 25 inch, 1892-1914 - Herefordshire https://mapseries-tilesets.s3.amazonaws.com/25_inch/herefordshire/{z}/{x}/{y}.png
25
+ OS 25 inch, 1892-1914 - Hertfordshire https://mapseries-tilesets.s3.amazonaws.com/25_inch/hertfordshire/{z}/{x}/{y}.png
26
+ OS 25 inch, 1892-1914 - Huntingdon https://mapseries-tilesets.s3.amazonaws.com/25_inch/huntingdon/{z}/{x}/{y}.png
27
+ OS 25 inch, 1892-1914 - Kent https://mapseries-tilesets.s3.amazonaws.com/25_inch/kent/{z}/{x}/{y}.png
28
+ OS 25 inch, 1892-1914 - Lancashire https://mapseries-tilesets.s3.amazonaws.com/25_inch/lancashire/{z}/{x}/{y}.png
29
+ OS 25 inch, 1892-1914 - Leicestershire https://mapseries-tilesets.s3.amazonaws.com/25_inch/leicestershire/{z}/{x}/{y}.png
30
+ OS 25 inch, 1892-1914 - Lincolnshire https://mapseries-tilesets.s3.amazonaws.com/25_inch/lincolnshire/{z}/{x}/{y}.png
31
+ OS 25 inch, 1892-1914 - London https://mapseries-tilesets.s3.amazonaws.com/25_inch/london/{z}/{x}/{y}.png
32
+ OS 25 inch, 1892-1914 - Middlesex https://mapseries-tilesets.s3.amazonaws.com/25_inch/middlesex/{z}/{x}/{y}.png
33
+ OS 25 inch, 1892-1914 - Norfolk https://mapseries-tilesets.s3.amazonaws.com/25_inch/norfolk/{z}/{x}/{y}.png
34
+ OS 25 inch, 1892-1914 - Northamptonshire https://mapseries-tilesets.s3.amazonaws.com/25_inch/northampton/{z}/{x}/{y}.png
35
+ OS 25 inch, 1892-1914 - Northumberland https://mapseries-tilesets.s3.amazonaws.com/25_inch/northumberland/{z}/{x}/{y}.png
36
+ OS 25 inch, 1892-1914 - Nottinghamshire https://mapseries-tilesets.s3.amazonaws.com/25_inch/nottinghamshire/{z}/{x}/{y}.png
37
+ OS 25 inch, 1892-1914 - Oxford https://mapseries-tilesets.s3.amazonaws.com/25_inch/oxford/{z}/{x}/{y}.png
38
+ OS 25 inch, 1892-1914 - Rutland https://mapseries-tilesets.s3.amazonaws.com/25_inch/rutland/{z}/{x}/{y}.png
39
+ OS 25 inch, 1892-1914 - Shropshire / Derbyshire https://mapseries-tilesets.s3.amazonaws.com/25_inch/Shrop_Derby/{z}/{x}/{y}.png
40
+ OS 25 inch, 1892-1914 - Somerset https://mapseries-tilesets.s3.amazonaws.com/25_inch/somerset/{z}/{x}/{y}.png
41
+ OS 25 inch, 1892-1914 - Stafford https://mapseries-tilesets.s3.amazonaws.com/25_inch/stafford/{z}/{x}/{y}.png
42
+ OS 25 inch, 1892-1914 - Suffolk https://mapseries-tilesets.s3.amazonaws.com/25_inch/suffolk/{z}/{x}/{y}.png
43
+ OS 25 inch, 1892-1914 - Surrey https://mapseries-tilesets.s3.amazonaws.com/25_inch/surrey/{z}/{x}/{y}.png
44
+ OS 25 inch, 1892-1914 - Sussex https://mapseries-tilesets.s3.amazonaws.com/25_inch/sussex/{z}/{x}/{y}.png
45
+ OS 25 inch, 1892-1914 - Wales https://mapseries-tilesets.s3.amazonaws.com/25_inch/wales/{z}/{x}/{y}.png
46
+ OS 25 inch, 1892-1914 - Warwick https://mapseries-tilesets.s3.amazonaws.com/25_inch/warwick/{z}/{x}/{y}.png
47
+ OS 25 inch, 1892-1914 - Westmorland https://mapseries-tilesets.s3.amazonaws.com/25_inch/westmorland/{z}/{x}/{y}.png
48
+ OS 25 inch, 1892-1914 - Wiltshire https://mapseries-tilesets.s3.amazonaws.com/25_inch/wiltshire2nd/{z}/{x}/{y}.png
49
+ OS 25 inch, 1892-1914 - Worcestershire https://mapseries-tilesets.s3.amazonaws.com/25_inch/Worcestershire/{z}/{x}/{y}.png
50
+ OS 25 inch, 1892-1914 - Yorkshire https://mapseries-tilesets.s3.amazonaws.com/25_inch/yorkshire/{z}/{x}/{y}.png
51
+ OS 25 inch, 1892-1914 'Holes' (fills gaps in series) https://geo.nls.uk/mapdata3/os/25_inch_holes_england/{z}/{x}/{y}.png
data/us_counties.geojson ADDED
The diff for this file is too large to render. See raw diff
 
data/us_metro_areas.geojson ADDED
The diff for this file is too large to render. See raw diff
 
data/us_nation.geojson ADDED
The diff for this file is too large to render. See raw diff
 
data/us_states.geojson ADDED
The diff for this file is too large to render. See raw diff
 
environment-bk.yml ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: geo
2
+ channels:
3
+ - conda-forge
4
+ dependencies:
5
+ - gdal=3.4.3
6
+ - pip
7
+ - pip:
8
+ - geopandas
9
+ - keplergl
10
+ - streamlit
11
+ - localtileserver
12
+ - palettable
13
+ - streamlit-folium
14
+ - streamlit-keplergl
15
+ - streamlit-bokeh-events
16
+ - git+https://github.com/giswqs/leafmap
17
+ - git+https://github.com/giswqs/geemap
multiapp.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Frameworks for running multiple Streamlit applications as a single app.
2
+ """
3
+ import streamlit as st
4
+
5
+ # app_state = st.experimental_get_query_params()
6
+ # app_state = {k: v[0] if isinstance(v, list) else v for k, v in app_state.items()} # fetch the first item in each query string as we don't have multiple values for each query string key in this example
7
+
8
+
9
+ class MultiApp:
10
+ """Framework for combining multiple streamlit applications.
11
+ Usage:
12
+ def foo():
13
+ st.title("Hello Foo")
14
+ def bar():
15
+ st.title("Hello Bar")
16
+ app = MultiApp()
17
+ app.add_app("Foo", foo)
18
+ app.add_app("Bar", bar)
19
+ app.run()
20
+ It is also possible keep each application in a separate file.
21
+ import foo
22
+ import bar
23
+ app = MultiApp()
24
+ app.add_app("Foo", foo.app)
25
+ app.add_app("Bar", bar.app)
26
+ app.run()
27
+ """
28
+
29
+ def __init__(self):
30
+ self.apps = []
31
+
32
+ def add_app(self, title, func):
33
+ """Adds a new application.
34
+ Parameters
35
+ ----------
36
+ func:
37
+ the python function to render this app.
38
+ title:
39
+ title of the app. Appears in the dropdown in the sidebar.
40
+ """
41
+ self.apps.append({"title": title, "function": func})
42
+
43
+ def run(self):
44
+ app_state = st.experimental_get_query_params()
45
+ app_state = {
46
+ k: v[0] if isinstance(v, list) else v for k, v in app_state.items()
47
+ } # fetch the first item in each query string as we don't have multiple values for each query string key in this example
48
+
49
+ # st.write('before', app_state)
50
+
51
+ titles = [a["title"] for a in self.apps]
52
+ functions = [a["function"] for a in self.apps]
53
+ default_radio = titles.index(app_state["page"]) if "page" in app_state else 0
54
+
55
+ st.sidebar.title("Navigation")
56
+
57
+ title = st.sidebar.radio("Go To", titles, index=default_radio, key="radio")
58
+
59
+ app_state["page"] = st.session_state.radio
60
+ # st.write('after', app_state)
61
+
62
+ st.experimental_set_query_params(**app_state)
63
+ # st.experimental_set_query_params(**st.session_state.to_dict())
64
+ functions[titles.index(title)]()
packages.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ ffmpeg
2
+ gifsicle
3
+ build-essential
4
+ python3-dev
5
+ gdal-bin
6
+ libgdal-dev
7
+ libproj-dev
8
+ libgeos-dev
9
+ proj-bin
pages/10_🌍_Earth_Engine_Datasets.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ee
2
+ import streamlit as st
3
+ import geemap.foliumap as geemap
4
+
5
+ st.set_page_config(layout="wide")
6
+
7
+
8
+
9
+
10
+ def nlcd():
11
+
12
+ # st.header("National Land Cover Database (NLCD)")
13
+
14
+ row1_col1, row1_col2 = st.columns([3, 1])
15
+ width = 950
16
+ height = 600
17
+
18
+ Map = geemap.Map(center=[40, -100], zoom=4)
19
+
20
+ # Select the seven NLCD epoches after 2000.
21
+ years = ["2001", "2004", "2006", "2008", "2011", "2013", "2016", "2019"]
22
+
23
+ # Get an NLCD image by year.
24
+ def getNLCD(year):
25
+ # Import the NLCD collection.
26
+ dataset = ee.ImageCollection("USGS/NLCD_RELEASES/2019_REL/NLCD")
27
+
28
+ # Filter the collection by year.
29
+ nlcd = dataset.filter(ee.Filter.eq("system:index", year)).first()
30
+
31
+ # Select the land cover band.
32
+ landcover = nlcd.select("landcover")
33
+ return landcover
34
+
35
+ with row1_col2:
36
+ selected_year = st.multiselect("Select a year", years)
37
+ add_legend = st.checkbox("Show legend")
38
+
39
+ if selected_year:
40
+ for year in selected_year:
41
+ Map.addLayer(getNLCD(year), {}, "NLCD " + year)
42
+
43
+ if add_legend:
44
+ Map.add_legend(
45
+ legend_title="NLCD Land Cover Classification", builtin_legend="NLCD"
46
+ )
47
+ with row1_col1:
48
+ Map.to_streamlit(width=width, height=height)
49
+
50
+ else:
51
+ with row1_col1:
52
+ Map.to_streamlit(width=width, height=height)
53
+
54
+
55
+ def search_data():
56
+
57
+ # st.header("Search Earth Engine Data Catalog")
58
+
59
+ Map = geemap.Map()
60
+
61
+ if "ee_assets" not in st.session_state:
62
+ st.session_state["ee_assets"] = None
63
+ if "asset_titles" not in st.session_state:
64
+ st.session_state["asset_titles"] = None
65
+
66
+ col1, col2 = st.columns([2, 1])
67
+
68
+ dataset = None
69
+ with col2:
70
+ keyword = st.text_input(
71
+ "Enter a keyword to search (e.g., elevation)", "")
72
+ if keyword:
73
+ ee_assets = geemap.search_ee_data(keyword)
74
+ asset_titles = [x["title"] for x in ee_assets]
75
+ asset_types = [x["type"] for x in ee_assets]
76
+
77
+ translate = {
78
+ "image_collection": "ee.ImageCollection('",
79
+ "image": "ee.Image('",
80
+ "table": "ee.FeatureCollection('",
81
+ "table_collection": "ee.FeatureCollection('",
82
+ }
83
+
84
+ dataset = st.selectbox("Select a dataset", asset_titles)
85
+ if len(ee_assets) > 0:
86
+ st.session_state["ee_assets"] = ee_assets
87
+ st.session_state["asset_titles"] = asset_titles
88
+
89
+ if dataset is not None:
90
+ with st.expander("Show dataset details", True):
91
+ index = asset_titles.index(dataset)
92
+
93
+ html = geemap.ee_data_html(
94
+ st.session_state["ee_assets"][index])
95
+ html = html.replace("\n", "")
96
+ st.markdown(html, True)
97
+
98
+ ee_id = ee_assets[index]["id"]
99
+ uid = ee_assets[index]["uid"]
100
+ st.markdown(f"""**Earth Engine Snippet:** `{ee_id}`""")
101
+ ee_asset = f"{translate[asset_types[index]]}{ee_id}')"
102
+ vis_params = st.text_input(
103
+ "Enter visualization parameters as a dictionary", {}
104
+ )
105
+ layer_name = st.text_input("Enter a layer name", uid)
106
+ button = st.button("Add dataset to map")
107
+ if button:
108
+ vis = {}
109
+ try:
110
+ if vis_params.strip() == "":
111
+ # st.error("Please enter visualization parameters")
112
+ vis_params = "{}"
113
+ vis = eval(vis_params)
114
+ if not isinstance(vis, dict):
115
+ st.error(
116
+ "Visualization parameters must be a dictionary")
117
+ try:
118
+ Map.addLayer(eval(ee_asset), vis, layer_name)
119
+ except Exception as e:
120
+ st.error(f"Error adding layer: {e}")
121
+ except Exception as e:
122
+ st.error(f"Invalid visualization parameters: {e}")
123
+
124
+ with col1:
125
+ Map.to_streamlit()
126
+ else:
127
+ with col1:
128
+ Map.to_streamlit()
129
+
130
+
131
+ def app():
132
+ st.title("Earth Engine Data Catalog")
133
+
134
+ apps = ["Search Earth Engine Data Catalog",
135
+ "National Land Cover Database (NLCD)"]
136
+
137
+ selected_app = st.selectbox("Select an app", apps)
138
+
139
+ if selected_app == "National Land Cover Database (NLCD)":
140
+ nlcd()
141
+ elif selected_app == "Search Earth Engine Data Catalog":
142
+ search_data()
143
+
144
+
145
+ app()
pages/11_🧱_Ordnance_Survey.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import folium
2
+ import pandas as pd
3
+ import streamlit as st
4
+ import leafmap.foliumap as leafmap
5
+ import folium.plugins as plugins
6
+
7
+ st.set_page_config(layout="wide")
8
+
9
+
10
+
11
+ st.title("National Library of Scotland XYZ Layers")
12
+ df = pd.read_csv("data/scotland_xyz.tsv", sep="\t")
13
+ basemaps = leafmap.basemaps
14
+ names = df["Name"].values.tolist() + list(basemaps.keys())
15
+ links = df["URL"].values.tolist() + list(basemaps.values())
16
+
17
+ col1, col2, col3, col4, col5, col6, col7 = st.columns([3, 3, 1, 1, 1, 1.5, 1.5])
18
+ with col1:
19
+ left_name = st.selectbox(
20
+ "Select the left layer",
21
+ names,
22
+ index=names.index("Great Britain - Bartholomew Half Inch, 1897-1907"),
23
+ )
24
+
25
+ with col2:
26
+ right_name = st.selectbox(
27
+ "Select the right layer",
28
+ names,
29
+ index=names.index("HYBRID"),
30
+ )
31
+
32
+ with col3:
33
+ # lat = st.slider('Latitude', -90.0, 90.0, 55.68, step=0.01)
34
+ lat = st.text_input("Latitude", " 55.68")
35
+
36
+ with col4:
37
+ # lon = st.slider('Longitude', -180.0, 180.0, -2.98, step=0.01)
38
+ lon = st.text_input("Longitude", "-2.98")
39
+
40
+ with col5:
41
+ # zoom = st.slider('Zoom', 1, 24, 6, step=1)
42
+ zoom = st.text_input("Zoom", "6")
43
+
44
+ with col6:
45
+ checkbox = st.checkbox("Add OS 25 inch")
46
+
47
+ # with col7:
48
+ with st.expander("Acknowledgements"):
49
+ markdown = """
50
+ The map tile access is by kind arrangement of the National Library of Scotland on the understanding that re-use is for personal purposes. They host most of the map layers except these:
51
+ - The Roy Maps are owned by the British Library.
52
+ - The Great Britain – OS maps 1:25,000, 1937-61 and One Inch 7th series, 1955-61 are hosted by MapTiler.
53
+
54
+ If you wish you use these layers within a website, or for a commercial or public purpose, please view the [National Library of Scotland Historic Maps Subscription API](https://maps.nls.uk/projects/subscription-api/) or contact them at [email protected].
55
+ """
56
+ st.markdown(markdown, unsafe_allow_html=True)
57
+
58
+ m = leafmap.Map(
59
+ center=[float(lat), float(lon)],
60
+ zoom=int(zoom),
61
+ locate_control=True,
62
+ draw_control=False,
63
+ measure_control=False,
64
+ )
65
+ measure = plugins.MeasureControl(position="bottomleft", active_color="orange")
66
+ measure.add_to(m)
67
+
68
+ if left_name in basemaps:
69
+ left_layer = basemaps[left_name]
70
+ else:
71
+ left_layer = folium.TileLayer(
72
+ tiles=links[names.index(left_name)],
73
+ name=left_name,
74
+ attr="National Library of Scotland",
75
+ overlay=True,
76
+ )
77
+
78
+ if right_name in basemaps:
79
+ right_layer = basemaps[right_name]
80
+ else:
81
+ right_layer = folium.TileLayer(
82
+ tiles=links[names.index(right_name)],
83
+ name=right_name,
84
+ attr="National Library of Scotland",
85
+ overlay=True,
86
+ )
87
+
88
+ if checkbox:
89
+ for index, name in enumerate(names):
90
+ if "OS 25 inch" in name:
91
+ m.add_tile_layer(
92
+ links[index], name, attribution="National Library of Scotland"
93
+ )
94
+
95
+ m.split_map(left_layer, right_layer)
96
+ m.to_streamlit(height=600)
pages/12_🌲_Land_Cover_Mapping.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import ee
3
+ import streamlit as st
4
+ import geemap.foliumap as geemap
5
+
6
+ st.set_page_config(layout="wide")
7
+
8
+
9
+
10
+ st.title("Comparing Global Land Cover Maps")
11
+
12
+ col1, col2 = st.columns([4, 1])
13
+
14
+ Map = geemap.Map()
15
+ Map.add_basemap("ESA WorldCover 2020 S2 FCC")
16
+ Map.add_basemap("ESA WorldCover 2020 S2 TCC")
17
+ Map.add_basemap("HYBRID")
18
+
19
+ esa = ee.ImageCollection("ESA/WorldCover/v100").first()
20
+ esa_vis = {"bands": ["Map"]}
21
+
22
+
23
+ esri = ee.ImageCollection(
24
+ "projects/sat-io/open-datasets/landcover/ESRI_Global-LULC_10m"
25
+ ).mosaic()
26
+ esri_vis = {
27
+ "min": 1,
28
+ "max": 10,
29
+ "palette": [
30
+ "#1A5BAB",
31
+ "#358221",
32
+ "#A7D282",
33
+ "#87D19E",
34
+ "#FFDB5C",
35
+ "#EECFA8",
36
+ "#ED022A",
37
+ "#EDE9E4",
38
+ "#F2FAFF",
39
+ "#C8C8C8",
40
+ ],
41
+ }
42
+
43
+
44
+ markdown = """
45
+ - [Dynamic World Land Cover](https://developers.google.com/earth-engine/datasets/catalog/GOOGLE_DYNAMICWORLD_V1?hl=en)
46
+ - [ESA Global Land Cover](https://developers.google.com/earth-engine/datasets/catalog/ESA_WorldCover_v100)
47
+ - [ESRI Global Land Cover](https://samapriya.github.io/awesome-gee-community-datasets/projects/esrilc2020)
48
+
49
+ """
50
+
51
+ with col2:
52
+
53
+ longitude = st.number_input("Longitude", -180.0, 180.0, -89.3998)
54
+ latitude = st.number_input("Latitude", -90.0, 90.0, 43.0886)
55
+ zoom = st.number_input("Zoom", 0, 20, 11)
56
+
57
+ Map.setCenter(longitude, latitude, zoom)
58
+
59
+ start = st.date_input("Start Date for Dynamic World", datetime.date(2020, 1, 1))
60
+ end = st.date_input("End Date for Dynamic World", datetime.date(2021, 1, 1))
61
+
62
+ start_date = start.strftime("%Y-%m-%d")
63
+ end_date = end.strftime("%Y-%m-%d")
64
+
65
+ region = ee.Geometry.BBox(-179, -89, 179, 89)
66
+ dw = geemap.dynamic_world(region, start_date, end_date, return_type="hillshade")
67
+
68
+ layers = {
69
+ "Dynamic World": geemap.ee_tile_layer(dw, {}, "Dynamic World Land Cover"),
70
+ "ESA Land Cover": geemap.ee_tile_layer(esa, esa_vis, "ESA Land Cover"),
71
+ "ESRI Land Cover": geemap.ee_tile_layer(esri, esri_vis, "ESRI Land Cover"),
72
+ }
73
+
74
+ options = list(layers.keys())
75
+ left = st.selectbox("Select a left layer", options, index=1)
76
+ right = st.selectbox("Select a right layer", options, index=0)
77
+
78
+ left_layer = layers[left]
79
+ right_layer = layers[right]
80
+
81
+ Map.split_map(left_layer, right_layer)
82
+
83
+ legend = st.selectbox("Select a legend", options, index=options.index(right))
84
+ if legend == "Dynamic World":
85
+ Map.add_legend(
86
+ title="Dynamic World Land Cover",
87
+ builtin_legend="Dynamic_World",
88
+ )
89
+ elif legend == "ESA Land Cover":
90
+ Map.add_legend(title="ESA Land Cover", builtin_legend="ESA_WorldCover")
91
+ elif legend == "ESRI Land Cover":
92
+ Map.add_legend(title="ESRI Land Cover", builtin_legend="ESRI_LandCover")
93
+
94
+ with st.expander("Data sources"):
95
+ st.markdown(markdown)
96
+
97
+
98
+ with col1:
99
+ Map.to_streamlit(height=750)
pages/13_🏘️_Global_Building_Footprints.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ee
2
+ import geemap.foliumap as geemap
3
+ import geopandas as gpd
4
+ import streamlit as st
5
+
6
+ st.set_page_config(layout="wide")
7
+
8
+
9
+ @st.cache(persist=True)
10
+ def ee_authenticate(token_name="EARTHENGINE_TOKEN"):
11
+ geemap.ee_initialize(token_name=token_name)
12
+
13
+
14
+
15
+
16
+ st.title("Global Building Footprints")
17
+
18
+ col1, col2 = st.columns([8, 2])
19
+
20
+
21
+ @st.cache_data
22
+ def read_data(url):
23
+ return gpd.read_file(url)
24
+
25
+
26
+ countries = 'https://github.com/giswqs/geemap/raw/master/examples/data/countries.geojson'
27
+ states = 'https://github.com/giswqs/geemap/raw/master/examples/data/us_states.json'
28
+
29
+ countries_gdf = read_data(countries)
30
+ states_gdf = read_data(states)
31
+
32
+ country_names = countries_gdf['NAME'].values.tolist()
33
+ country_names.remove('United States of America')
34
+ country_names.append('USA')
35
+ country_names.sort()
36
+ country_names = [name.replace('.', '').replace(' ', '_')
37
+ for name in country_names]
38
+
39
+ state_names = states_gdf['name'].values.tolist()
40
+
41
+ basemaps = list(geemap.basemaps)
42
+
43
+ Map = geemap.Map()
44
+
45
+ with col2:
46
+
47
+ basemap = st.selectbox("Select a basemap", basemaps,
48
+ index=basemaps.index('HYBRID'))
49
+ Map.add_basemap(basemap)
50
+
51
+ country = st.selectbox('Select a country', country_names,
52
+ index=country_names.index('USA'))
53
+
54
+ if country == 'USA':
55
+ state = st.selectbox('Select a state', state_names,
56
+ index=state_names.index('Florida'))
57
+ layer_name = state
58
+
59
+ try:
60
+ fc = ee.FeatureCollection(
61
+ f'projects/sat-io/open-datasets/MSBuildings/US/{state}')
62
+ except:
63
+ st.error('No data available for the selected state.')
64
+
65
+ else:
66
+ try:
67
+ fc = ee.FeatureCollection(
68
+ f'projects/sat-io/open-datasets/MSBuildings/{country}')
69
+ except:
70
+ st.error('No data available for the selected country.')
71
+
72
+ layer_name = country
73
+
74
+ color = st.color_picker('Select a color', '#FF5500')
75
+
76
+ style = {'fillColor': '00000000', 'color': color}
77
+
78
+ split = st.checkbox("Split-panel map")
79
+
80
+ if split:
81
+ left = geemap.ee_tile_layer(fc.style(**style), {}, 'Left')
82
+ right = left
83
+ Map.split_map(left, right)
84
+ else:
85
+ Map.addLayer(fc.style(**style), {}, layer_name)
86
+
87
+ Map.centerObject(fc.first(), zoom=16)
88
+
89
+ with st.expander("Data Sources"):
90
+ st.info(
91
+ """
92
+ [Microsoft Building Footprints](https://gee-community-catalog.org/projects/msbuildings/)
93
+ """
94
+ )
95
+
96
+
97
+ with col1:
98
+
99
+ Map.to_streamlit(height=1000)
pages/14_πŸ“ˆ_Table_Data_Visualization.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import seaborn as sns
4
+ import matplotlib.pyplot as plt
5
+
6
+ st.set_page_config(layout="wide")
7
+
8
+ # Function for the CSV Visualization App
9
+ def app():
10
+ st.title('CSV Data Visualization')
11
+
12
+ # File uploader allows user to add their own CSV
13
+ uploaded_file = st.file_uploader("Upload your input CSV file", type=["csv"])
14
+
15
+ # Pandas DataFrame is created from the CSV file
16
+ if uploaded_file is not None:
17
+ df = pd.read_csv(uploaded_file)
18
+ st.write(df) # Display the dataframe on the app
19
+
20
+ # Create a selectbox for user to choose the column to visualize
21
+ columns = df.columns.tolist()
22
+ selected_column = st.selectbox('Select a column to visualize', columns)
23
+
24
+ # Using seaborn to create a count plot
25
+ fig, ax = plt.subplots()
26
+ sns.countplot(data=df, x=selected_column, ax=ax)
27
+ plt.xticks(rotation=45) # Rotate X-axis labels to 45 degrees
28
+ # Show the plot
29
+ st.pyplot(fig)
30
+
31
+ app()
pages/1_πŸ“·_Timelapse.py ADDED
@@ -0,0 +1,1515 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ee
2
+ import os
3
+ import warnings
4
+ import datetime
5
+ import fiona
6
+ import geopandas as gpd
7
+ import folium
8
+ import streamlit as st
9
+ import geemap.colormaps as cm
10
+ import geemap.foliumap as geemap
11
+ from datetime import date
12
+ from shapely.geometry import Polygon
13
+
14
+ st.set_page_config(layout="wide")
15
+ warnings.filterwarnings("ignore")
16
+
17
+
18
+ @st.cache_data
19
+ def ee_authenticate(token_name="EARTHENGINE_TOKEN"):
20
+ geemap.ee_initialize(token_name=token_name)
21
+
22
+
23
+
24
+
25
+ goes_rois = {
26
+ "Creek Fire, CA (2020-09-05)": {
27
+ "region": Polygon(
28
+ [
29
+ [-121.003418, 36.848857],
30
+ [-121.003418, 39.049052],
31
+ [-117.905273, 39.049052],
32
+ [-117.905273, 36.848857],
33
+ [-121.003418, 36.848857],
34
+ ]
35
+ ),
36
+ "start_time": "2020-09-05T15:00:00",
37
+ "end_time": "2020-09-06T02:00:00",
38
+ },
39
+ "Bomb Cyclone (2021-10-24)": {
40
+ "region": Polygon(
41
+ [
42
+ [-159.5954, 60.4088],
43
+ [-159.5954, 24.5178],
44
+ [-114.2438, 24.5178],
45
+ [-114.2438, 60.4088],
46
+ ]
47
+ ),
48
+ "start_time": "2021-10-24T14:00:00",
49
+ "end_time": "2021-10-25T01:00:00",
50
+ },
51
+ "Hunga Tonga Volcanic Eruption (2022-01-15)": {
52
+ "region": Polygon(
53
+ [
54
+ [-192.480469, -32.546813],
55
+ [-192.480469, -8.754795],
56
+ [-157.587891, -8.754795],
57
+ [-157.587891, -32.546813],
58
+ [-192.480469, -32.546813],
59
+ ]
60
+ ),
61
+ "start_time": "2022-01-15T03:00:00",
62
+ "end_time": "2022-01-15T07:00:00",
63
+ },
64
+ "Hunga Tonga Volcanic Eruption Closer Look (2022-01-15)": {
65
+ "region": Polygon(
66
+ [
67
+ [-178.901367, -22.958393],
68
+ [-178.901367, -17.85329],
69
+ [-171.452637, -17.85329],
70
+ [-171.452637, -22.958393],
71
+ [-178.901367, -22.958393],
72
+ ]
73
+ ),
74
+ "start_time": "2022-01-15T03:00:00",
75
+ "end_time": "2022-01-15T07:00:00",
76
+ },
77
+ }
78
+
79
+
80
+ landsat_rois = {
81
+ "Aral Sea": Polygon(
82
+ [
83
+ [57.667236, 43.834527],
84
+ [57.667236, 45.996962],
85
+ [61.12793, 45.996962],
86
+ [61.12793, 43.834527],
87
+ [57.667236, 43.834527],
88
+ ]
89
+ ),
90
+ "Dubai": Polygon(
91
+ [
92
+ [54.541626, 24.763044],
93
+ [54.541626, 25.427152],
94
+ [55.632019, 25.427152],
95
+ [55.632019, 24.763044],
96
+ [54.541626, 24.763044],
97
+ ]
98
+ ),
99
+ "Hong Kong International Airport": Polygon(
100
+ [
101
+ [113.825226, 22.198849],
102
+ [113.825226, 22.349758],
103
+ [114.085121, 22.349758],
104
+ [114.085121, 22.198849],
105
+ [113.825226, 22.198849],
106
+ ]
107
+ ),
108
+ "Las Vegas, NV": Polygon(
109
+ [
110
+ [-115.554199, 35.804449],
111
+ [-115.554199, 36.558188],
112
+ [-113.903503, 36.558188],
113
+ [-113.903503, 35.804449],
114
+ [-115.554199, 35.804449],
115
+ ]
116
+ ),
117
+ "Pucallpa, Peru": Polygon(
118
+ [
119
+ [-74.672699, -8.600032],
120
+ [-74.672699, -8.254983],
121
+ [-74.279938, -8.254983],
122
+ [-74.279938, -8.600032],
123
+ ]
124
+ ),
125
+ "Sierra Gorda, Chile": Polygon(
126
+ [
127
+ [-69.315491, -22.837104],
128
+ [-69.315491, -22.751488],
129
+ [-69.190006, -22.751488],
130
+ [-69.190006, -22.837104],
131
+ [-69.315491, -22.837104],
132
+ ]
133
+ ),
134
+ }
135
+
136
+ modis_rois = {
137
+ "World": Polygon(
138
+ [
139
+ [-171.210938, -57.136239],
140
+ [-171.210938, 79.997168],
141
+ [177.539063, 79.997168],
142
+ [177.539063, -57.136239],
143
+ [-171.210938, -57.136239],
144
+ ]
145
+ ),
146
+ "Africa": Polygon(
147
+ [
148
+ [-18.6983, 38.1446],
149
+ [-18.6983, -36.1630],
150
+ [52.2293, -36.1630],
151
+ [52.2293, 38.1446],
152
+ ]
153
+ ),
154
+ "USA": Polygon(
155
+ [
156
+ [-127.177734, 23.725012],
157
+ [-127.177734, 50.792047],
158
+ [-66.269531, 50.792047],
159
+ [-66.269531, 23.725012],
160
+ [-127.177734, 23.725012],
161
+ ]
162
+ ),
163
+ }
164
+
165
+ ocean_rois = {
166
+ "Gulf of Mexico": Polygon(
167
+ [
168
+ [-101.206055, 15.496032],
169
+ [-101.206055, 32.361403],
170
+ [-75.673828, 32.361403],
171
+ [-75.673828, 15.496032],
172
+ [-101.206055, 15.496032],
173
+ ]
174
+ ),
175
+ "North Atlantic Ocean": Polygon(
176
+ [
177
+ [-85.341797, 24.046464],
178
+ [-85.341797, 45.02695],
179
+ [-55.810547, 45.02695],
180
+ [-55.810547, 24.046464],
181
+ [-85.341797, 24.046464],
182
+ ]
183
+ ),
184
+ "World": Polygon(
185
+ [
186
+ [-171.210938, -57.136239],
187
+ [-171.210938, 79.997168],
188
+ [177.539063, 79.997168],
189
+ [177.539063, -57.136239],
190
+ [-171.210938, -57.136239],
191
+ ]
192
+ ),
193
+ }
194
+
195
+
196
+ @st.cache_data
197
+ def uploaded_file_to_gdf(data):
198
+ import tempfile
199
+ import os
200
+ import uuid
201
+
202
+ _, file_extension = os.path.splitext(data.name)
203
+ file_id = str(uuid.uuid4())
204
+ file_path = os.path.join(tempfile.gettempdir(), f"{file_id}{file_extension}")
205
+
206
+ with open(file_path, "wb") as file:
207
+ file.write(data.getbuffer())
208
+
209
+ if file_path.lower().endswith(".kml"):
210
+ fiona.drvsupport.supported_drivers["KML"] = "rw"
211
+ gdf = gpd.read_file(file_path, driver="KML")
212
+ else:
213
+ gdf = gpd.read_file(file_path)
214
+
215
+ return gdf
216
+
217
+
218
+ def app():
219
+
220
+ today = date.today()
221
+
222
+ st.title("Create Satellite Timelapse")
223
+
224
+ st.markdown(
225
+ """
226
+ An interactive web app for creating [Landsat](https://developers.google.com/earth-engine/datasets/catalog/landsat)/[GOES](https://jstnbraaten.medium.com/goes-in-earth-engine-53fbc8783c16) timelapse for any location around the globe.
227
+ The app was built using [streamlit](https://streamlit.io), [geemap](https://geemap.org), and [Google Earth Engine](https://earthengine.google.com). For more info, check out my streamlit [blog post](https://blog.streamlit.io/creating-satellite-timelapse-with-streamlit-and-earth-engine).
228
+ """
229
+ )
230
+
231
+ row1_col1, row1_col2 = st.columns([2, 1])
232
+
233
+ if st.session_state.get("zoom_level") is None:
234
+ st.session_state["zoom_level"] = 4
235
+
236
+ st.session_state["ee_asset_id"] = None
237
+ st.session_state["bands"] = None
238
+ st.session_state["palette"] = None
239
+ st.session_state["vis_params"] = None
240
+
241
+ with row1_col1:
242
+ ee_authenticate(token_name="EARTHENGINE_TOKEN")
243
+ m = geemap.Map(
244
+ basemap="HYBRID",
245
+ plugin_Draw=True,
246
+ Draw_export=True,
247
+ locate_control=True,
248
+ plugin_LatLngPopup=False,
249
+ )
250
+ m.add_basemap("ROADMAP")
251
+
252
+ with row1_col2:
253
+
254
+ keyword = st.text_input("Search for a location:", "")
255
+ if keyword:
256
+ locations = geemap.geocode(keyword)
257
+ if locations is not None and len(locations) > 0:
258
+ str_locations = [str(g)[1:-1] for g in locations]
259
+ location = st.selectbox("Select a location:", str_locations)
260
+ loc_index = str_locations.index(location)
261
+ selected_loc = locations[loc_index]
262
+ lat, lng = selected_loc.lat, selected_loc.lng
263
+ folium.Marker(location=[lat, lng], popup=location).add_to(m)
264
+ m.set_center(lng, lat, 12)
265
+ st.session_state["zoom_level"] = 12
266
+
267
+ collection = st.selectbox(
268
+ "Select a satellite image collection: ",
269
+ [
270
+ "Any Earth Engine ImageCollection",
271
+ "Landsat TM-ETM-OLI Surface Reflectance",
272
+ "Sentinel-2 MSI Surface Reflectance",
273
+ "Geostationary Operational Environmental Satellites (GOES)",
274
+ "MODIS Vegetation Indices (NDVI/EVI) 16-Day Global 1km",
275
+ "MODIS Gap filled Land Surface Temperature Daily",
276
+ "MODIS Ocean Color SMI",
277
+ "USDA National Agriculture Imagery Program (NAIP)",
278
+ ],
279
+ index=1,
280
+ )
281
+
282
+ if collection in [
283
+ "Landsat TM-ETM-OLI Surface Reflectance",
284
+ "Sentinel-2 MSI Surface Reflectance",
285
+ ]:
286
+ roi_options = ["Uploaded GeoJSON"] + list(landsat_rois.keys())
287
+
288
+ elif collection == "Geostationary Operational Environmental Satellites (GOES)":
289
+ roi_options = ["Uploaded GeoJSON"] + list(goes_rois.keys())
290
+
291
+ elif collection in [
292
+ "MODIS Vegetation Indices (NDVI/EVI) 16-Day Global 1km",
293
+ "MODIS Gap filled Land Surface Temperature Daily",
294
+ ]:
295
+ roi_options = ["Uploaded GeoJSON"] + list(modis_rois.keys())
296
+ elif collection == "MODIS Ocean Color SMI":
297
+ roi_options = ["Uploaded GeoJSON"] + list(ocean_rois.keys())
298
+ else:
299
+ roi_options = ["Uploaded GeoJSON"]
300
+
301
+ if collection == "Any Earth Engine ImageCollection":
302
+ keyword = st.text_input("Enter a keyword to search (e.g., MODIS):", "")
303
+ if keyword:
304
+
305
+ assets = geemap.search_ee_data(keyword)
306
+ ee_assets = []
307
+ for asset in assets:
308
+ if asset["ee_id_snippet"].startswith("ee.ImageCollection"):
309
+ ee_assets.append(asset)
310
+
311
+ asset_titles = [x["title"] for x in ee_assets]
312
+ dataset = st.selectbox("Select a dataset:", asset_titles)
313
+ if len(ee_assets) > 0:
314
+ st.session_state["ee_assets"] = ee_assets
315
+ st.session_state["asset_titles"] = asset_titles
316
+ index = asset_titles.index(dataset)
317
+ ee_id = ee_assets[index]["id"]
318
+ else:
319
+ ee_id = ""
320
+
321
+ if dataset is not None:
322
+ with st.expander("Show dataset details", False):
323
+ index = asset_titles.index(dataset)
324
+ html = geemap.ee_data_html(st.session_state["ee_assets"][index])
325
+ st.markdown(html, True)
326
+ # elif collection == "MODIS Gap filled Land Surface Temperature Daily":
327
+ # ee_id = ""
328
+ else:
329
+ ee_id = ""
330
+
331
+ asset_id = st.text_input("Enter an ee.ImageCollection asset ID:", ee_id)
332
+
333
+ if asset_id:
334
+ with st.expander("Customize band combination and color palette", True):
335
+ try:
336
+ col = ee.ImageCollection.load(asset_id)
337
+ st.session_state["ee_asset_id"] = asset_id
338
+ except:
339
+ st.error("Invalid Earth Engine asset ID.")
340
+ st.session_state["ee_asset_id"] = None
341
+ return
342
+
343
+ img_bands = col.first().bandNames().getInfo()
344
+ if len(img_bands) >= 3:
345
+ default_bands = img_bands[:3][::-1]
346
+ else:
347
+ default_bands = img_bands[:]
348
+ bands = st.multiselect(
349
+ "Select one or three bands (RGB):", img_bands, default_bands
350
+ )
351
+ st.session_state["bands"] = bands
352
+
353
+ if len(bands) == 1:
354
+ palette_options = st.selectbox(
355
+ "Color palette",
356
+ cm.list_colormaps(),
357
+ index=2,
358
+ )
359
+ palette_values = cm.get_palette(palette_options, 15)
360
+ palette = st.text_area(
361
+ "Enter a custom palette:",
362
+ palette_values,
363
+ )
364
+ st.write(
365
+ cm.plot_colormap(cmap=palette_options, return_fig=True)
366
+ )
367
+ st.session_state["palette"] = eval(palette)
368
+
369
+ if bands:
370
+ vis_params = st.text_area(
371
+ "Enter visualization parameters",
372
+ "{'bands': ["
373
+ + ", ".join([f"'{band}'" for band in bands])
374
+ + "]}",
375
+ )
376
+ else:
377
+ vis_params = st.text_area(
378
+ "Enter visualization parameters",
379
+ "{}",
380
+ )
381
+ try:
382
+ st.session_state["vis_params"] = eval(vis_params)
383
+ st.session_state["vis_params"]["palette"] = st.session_state[
384
+ "palette"
385
+ ]
386
+ except Exception as e:
387
+ st.session_state["vis_params"] = None
388
+ st.error(
389
+ f"Invalid visualization parameters. It must be a dictionary."
390
+ )
391
+
392
+ elif collection == "MODIS Gap filled Land Surface Temperature Daily":
393
+ with st.expander("Show dataset details", False):
394
+ st.markdown(
395
+ """
396
+ See the [Awesome GEE Community Datasets](https://samapriya.github.io/awesome-gee-community-datasets/projects/daily_lst/).
397
+ """
398
+ )
399
+
400
+ MODIS_options = ["Daytime (1:30 pm)", "Nighttime (1:30 am)"]
401
+ MODIS_option = st.selectbox("Select a MODIS dataset:", MODIS_options)
402
+ if MODIS_option == "Daytime (1:30 pm)":
403
+ st.session_state[
404
+ "ee_asset_id"
405
+ ] = "projects/sat-io/open-datasets/gap-filled-lst/gf_day_1km"
406
+ else:
407
+ st.session_state[
408
+ "ee_asset_id"
409
+ ] = "projects/sat-io/open-datasets/gap-filled-lst/gf_night_1km"
410
+
411
+ palette_options = st.selectbox(
412
+ "Color palette",
413
+ cm.list_colormaps(),
414
+ index=90,
415
+ )
416
+ palette_values = cm.get_palette(palette_options, 15)
417
+ palette = st.text_area(
418
+ "Enter a custom palette:",
419
+ palette_values,
420
+ )
421
+ st.write(cm.plot_colormap(cmap=palette_options, return_fig=True))
422
+ st.session_state["palette"] = eval(palette)
423
+ elif collection == "MODIS Ocean Color SMI":
424
+ with st.expander("Show dataset details", False):
425
+ st.markdown(
426
+ """
427
+ See the [Earth Engine Data Catalog](https://developers.google.com/earth-engine/datasets/catalog/NASA_OCEANDATA_MODIS-Aqua_L3SMI).
428
+ """
429
+ )
430
+
431
+ MODIS_options = ["Aqua", "Terra"]
432
+ MODIS_option = st.selectbox("Select a satellite:", MODIS_options)
433
+ st.session_state["ee_asset_id"] = MODIS_option
434
+ # if MODIS_option == "Daytime (1:30 pm)":
435
+ # st.session_state[
436
+ # "ee_asset_id"
437
+ # ] = "projects/sat-io/open-datasets/gap-filled-lst/gf_day_1km"
438
+ # else:
439
+ # st.session_state[
440
+ # "ee_asset_id"
441
+ # ] = "projects/sat-io/open-datasets/gap-filled-lst/gf_night_1km"
442
+
443
+ band_dict = {
444
+ "Chlorophyll a concentration": "chlor_a",
445
+ "Normalized fluorescence line height": "nflh",
446
+ "Particulate organic carbon": "poc",
447
+ "Sea surface temperature": "sst",
448
+ "Remote sensing reflectance at band 412nm": "Rrs_412",
449
+ "Remote sensing reflectance at band 443nm": "Rrs_443",
450
+ "Remote sensing reflectance at band 469nm": "Rrs_469",
451
+ "Remote sensing reflectance at band 488nm": "Rrs_488",
452
+ "Remote sensing reflectance at band 531nm": "Rrs_531",
453
+ "Remote sensing reflectance at band 547nm": "Rrs_547",
454
+ "Remote sensing reflectance at band 555nm": "Rrs_555",
455
+ "Remote sensing reflectance at band 645nm": "Rrs_645",
456
+ "Remote sensing reflectance at band 667nm": "Rrs_667",
457
+ "Remote sensing reflectance at band 678nm": "Rrs_678",
458
+ }
459
+
460
+ band_options = list(band_dict.keys())
461
+ band = st.selectbox(
462
+ "Select a band",
463
+ band_options,
464
+ band_options.index("Sea surface temperature"),
465
+ )
466
+ st.session_state["band"] = band_dict[band]
467
+
468
+ colors = cm.list_colormaps()
469
+ palette_options = st.selectbox(
470
+ "Color palette",
471
+ colors,
472
+ index=colors.index("coolwarm"),
473
+ )
474
+ palette_values = cm.get_palette(palette_options, 15)
475
+ palette = st.text_area(
476
+ "Enter a custom palette:",
477
+ palette_values,
478
+ )
479
+ st.write(cm.plot_colormap(cmap=palette_options, return_fig=True))
480
+ st.session_state["palette"] = eval(palette)
481
+
482
+ sample_roi = st.selectbox(
483
+ "Select a sample ROI or upload a GeoJSON file:",
484
+ roi_options,
485
+ index=0,
486
+ )
487
+
488
+ add_outline = st.checkbox(
489
+ "Overlay an administrative boundary on timelapse", False
490
+ )
491
+
492
+ if add_outline:
493
+
494
+ with st.expander("Customize administrative boundary", True):
495
+
496
+ overlay_options = {
497
+ "User-defined": None,
498
+ "Continents": "continents",
499
+ "Countries": "countries",
500
+ "US States": "us_states",
501
+ "China": "china",
502
+ }
503
+
504
+ overlay = st.selectbox(
505
+ "Select an administrative boundary:",
506
+ list(overlay_options.keys()),
507
+ index=2,
508
+ )
509
+
510
+ overlay_data = overlay_options[overlay]
511
+
512
+ if overlay_data is None:
513
+ overlay_data = st.text_input(
514
+ "Enter an HTTP URL to a GeoJSON file or an ee.FeatureCollection asset id:",
515
+ "https://raw.githubusercontent.com/giswqs/geemap/master/examples/data/countries.geojson",
516
+ )
517
+
518
+ overlay_color = st.color_picker(
519
+ "Select a color for the administrative boundary:", "#000000"
520
+ )
521
+ overlay_width = st.slider(
522
+ "Select a line width for the administrative boundary:", 1, 20, 1
523
+ )
524
+ overlay_opacity = st.slider(
525
+ "Select an opacity for the administrative boundary:",
526
+ 0.0,
527
+ 1.0,
528
+ 1.0,
529
+ 0.05,
530
+ )
531
+ else:
532
+ overlay_data = None
533
+ overlay_color = "black"
534
+ overlay_width = 1
535
+ overlay_opacity = 1
536
+
537
+ with row1_col1:
538
+
539
+ with st.expander(
540
+ "Steps: Draw a rectangle on the map -> Export it as a GeoJSON -> Upload it back to the app -> Click the Submit button. Expand this tab to see a demo πŸ‘‰"
541
+ ):
542
+ video_empty = st.empty()
543
+
544
+ data = st.file_uploader(
545
+ "Upload a GeoJSON file to use as an ROI. Customize timelapse parameters and then click the Submit button πŸ˜‡πŸ‘‡",
546
+ type=["geojson", "kml", "zip"],
547
+ )
548
+
549
+ crs = "epsg:4326"
550
+ if sample_roi == "Uploaded GeoJSON":
551
+ if data is None:
552
+ # st.info(
553
+ # "Steps to create a timelapse: Draw a rectangle on the map -> Export it as a GeoJSON -> Upload it back to the app -> Click Submit button"
554
+ # )
555
+ if collection in [
556
+ "Geostationary Operational Environmental Satellites (GOES)",
557
+ "USDA National Agriculture Imagery Program (NAIP)",
558
+ ] and (not keyword):
559
+ m.set_center(-100, 40, 3)
560
+ # else:
561
+ # m.set_center(4.20, 18.63, zoom=2)
562
+ else:
563
+ if collection in [
564
+ "Landsat TM-ETM-OLI Surface Reflectance",
565
+ "Sentinel-2 MSI Surface Reflectance",
566
+ ]:
567
+ gdf = gpd.GeoDataFrame(
568
+ index=[0], crs=crs, geometry=[landsat_rois[sample_roi]]
569
+ )
570
+ elif (
571
+ collection
572
+ == "Geostationary Operational Environmental Satellites (GOES)"
573
+ ):
574
+ gdf = gpd.GeoDataFrame(
575
+ index=[0], crs=crs, geometry=[goes_rois[sample_roi]["region"]]
576
+ )
577
+ elif collection == "MODIS Vegetation Indices (NDVI/EVI) 16-Day Global 1km":
578
+ gdf = gpd.GeoDataFrame(
579
+ index=[0], crs=crs, geometry=[modis_rois[sample_roi]]
580
+ )
581
+
582
+ if sample_roi != "Uploaded GeoJSON":
583
+
584
+ if collection in [
585
+ "Landsat TM-ETM-OLI Surface Reflectance",
586
+ "Sentinel-2 MSI Surface Reflectance",
587
+ ]:
588
+ gdf = gpd.GeoDataFrame(
589
+ index=[0], crs=crs, geometry=[landsat_rois[sample_roi]]
590
+ )
591
+ elif (
592
+ collection
593
+ == "Geostationary Operational Environmental Satellites (GOES)"
594
+ ):
595
+ gdf = gpd.GeoDataFrame(
596
+ index=[0], crs=crs, geometry=[goes_rois[sample_roi]["region"]]
597
+ )
598
+ elif collection in [
599
+ "MODIS Vegetation Indices (NDVI/EVI) 16-Day Global 1km",
600
+ "MODIS Gap filled Land Surface Temperature Daily",
601
+ ]:
602
+ gdf = gpd.GeoDataFrame(
603
+ index=[0], crs=crs, geometry=[modis_rois[sample_roi]]
604
+ )
605
+ elif collection == "MODIS Ocean Color SMI":
606
+ gdf = gpd.GeoDataFrame(
607
+ index=[0], crs=crs, geometry=[ocean_rois[sample_roi]]
608
+ )
609
+ try:
610
+ st.session_state["roi"] = geemap.gdf_to_ee(gdf, geodesic=False)
611
+ except Exception as e:
612
+ st.error(e)
613
+ st.error("Please draw another ROI and try again.")
614
+ return
615
+ m.add_gdf(gdf, "ROI")
616
+
617
+ elif data:
618
+ gdf = uploaded_file_to_gdf(data)
619
+ try:
620
+ st.session_state["roi"] = geemap.gdf_to_ee(gdf, geodesic=False)
621
+ m.add_gdf(gdf, "ROI")
622
+ except Exception as e:
623
+ st.error(e)
624
+ st.error("Please draw another ROI and try again.")
625
+ return
626
+
627
+ m.to_streamlit(height=600)
628
+
629
+ with row1_col2:
630
+
631
+ if collection in [
632
+ "Landsat TM-ETM-OLI Surface Reflectance",
633
+ "Sentinel-2 MSI Surface Reflectance",
634
+ ]:
635
+
636
+ if collection == "Landsat TM-ETM-OLI Surface Reflectance":
637
+ sensor_start_year = 1984
638
+ timelapse_title = "Landsat Timelapse"
639
+ timelapse_speed = 5
640
+ elif collection == "Sentinel-2 MSI Surface Reflectance":
641
+ sensor_start_year = 2015
642
+ timelapse_title = "Sentinel-2 Timelapse"
643
+ timelapse_speed = 5
644
+ video_empty.video("https://youtu.be/VVRK_-dEjR4")
645
+
646
+ with st.form("submit_landsat_form"):
647
+
648
+ roi = None
649
+ if st.session_state.get("roi") is not None:
650
+ roi = st.session_state.get("roi")
651
+ out_gif = geemap.temp_file_path(".gif")
652
+
653
+ title = st.text_input(
654
+ "Enter a title to show on the timelapse: ", timelapse_title
655
+ )
656
+ RGB = st.selectbox(
657
+ "Select an RGB band combination:",
658
+ [
659
+ "Red/Green/Blue",
660
+ "NIR/Red/Green",
661
+ "SWIR2/SWIR1/NIR",
662
+ "NIR/SWIR1/Red",
663
+ "SWIR2/NIR/Red",
664
+ "SWIR2/SWIR1/Red",
665
+ "SWIR1/NIR/Blue",
666
+ "NIR/SWIR1/Blue",
667
+ "SWIR2/NIR/Green",
668
+ "SWIR1/NIR/Red",
669
+ "SWIR2/NIR/SWIR1",
670
+ "SWIR1/NIR/SWIR2",
671
+ ],
672
+ index=9,
673
+ )
674
+
675
+ frequency = st.selectbox(
676
+ "Select a temporal frequency:",
677
+ ["year", "quarter", "month"],
678
+ index=0,
679
+ )
680
+
681
+ with st.expander("Customize timelapse"):
682
+
683
+ speed = st.slider("Frames per second:", 1, 30, timelapse_speed)
684
+ dimensions = st.slider(
685
+ "Maximum dimensions (Width*Height) in pixels", 768, 2000, 768
686
+ )
687
+ progress_bar_color = st.color_picker(
688
+ "Progress bar color:", "#0000ff"
689
+ )
690
+ years = st.slider(
691
+ "Start and end year:",
692
+ sensor_start_year,
693
+ today.year,
694
+ (sensor_start_year, today.year),
695
+ )
696
+ months = st.slider("Start and end month:", 1, 12, (1, 12))
697
+ font_size = st.slider("Font size:", 10, 50, 30)
698
+ font_color = st.color_picker("Font color:", "#ffffff")
699
+ apply_fmask = st.checkbox(
700
+ "Apply fmask (remove clouds, shadows, snow)", True
701
+ )
702
+ font_type = st.selectbox(
703
+ "Select the font type for the title:",
704
+ ["arial.ttf", "alibaba.otf"],
705
+ index=0,
706
+ )
707
+ fading = st.slider(
708
+ "Fading duration (seconds) for each frame:", 0.0, 3.0, 0.0
709
+ )
710
+ mp4 = st.checkbox("Save timelapse as MP4", True)
711
+
712
+ empty_text = st.empty()
713
+ empty_image = st.empty()
714
+ empty_fire_image = st.empty()
715
+ empty_video = st.container()
716
+ submitted = st.form_submit_button("Submit")
717
+ if submitted:
718
+
719
+ if sample_roi == "Uploaded GeoJSON" and data is None:
720
+ empty_text.warning(
721
+ "Steps to create a timelapse: Draw a rectangle on the map -> Export it as a GeoJSON -> Upload it back to the app -> Click the Submit button. Alternatively, you can select a sample ROI from the dropdown list."
722
+ )
723
+ else:
724
+
725
+ empty_text.text("Computing... Please wait...")
726
+
727
+ start_year = years[0]
728
+ end_year = years[1]
729
+ start_date = str(months[0]).zfill(2) + "-01"
730
+ end_date = str(months[1]).zfill(2) + "-30"
731
+ bands = RGB.split("/")
732
+
733
+ try:
734
+ if collection == "Landsat TM-ETM-OLI Surface Reflectance":
735
+ out_gif = geemap.landsat_timelapse(
736
+ roi=roi,
737
+ out_gif=out_gif,
738
+ start_year=start_year,
739
+ end_year=end_year,
740
+ start_date=start_date,
741
+ end_date=end_date,
742
+ bands=bands,
743
+ apply_fmask=apply_fmask,
744
+ frames_per_second=speed,
745
+ # dimensions=dimensions,
746
+ dimensions=768,
747
+ overlay_data=overlay_data,
748
+ overlay_color=overlay_color,
749
+ overlay_width=overlay_width,
750
+ overlay_opacity=overlay_opacity,
751
+ frequency=frequency,
752
+ date_format=None,
753
+ title=title,
754
+ title_xy=("2%", "90%"),
755
+ add_text=True,
756
+ text_xy=("2%", "2%"),
757
+ text_sequence=None,
758
+ font_type=font_type,
759
+ font_size=font_size,
760
+ font_color=font_color,
761
+ add_progress_bar=True,
762
+ progress_bar_color=progress_bar_color,
763
+ progress_bar_height=5,
764
+ loop=0,
765
+ mp4=mp4,
766
+ fading=fading,
767
+ )
768
+ elif collection == "Sentinel-2 MSI Surface Reflectance":
769
+ out_gif = geemap.sentinel2_timelapse(
770
+ roi=roi,
771
+ out_gif=out_gif,
772
+ start_year=start_year,
773
+ end_year=end_year,
774
+ start_date=start_date,
775
+ end_date=end_date,
776
+ bands=bands,
777
+ apply_fmask=apply_fmask,
778
+ frames_per_second=speed,
779
+ dimensions=768,
780
+ # dimensions=dimensions,
781
+ overlay_data=overlay_data,
782
+ overlay_color=overlay_color,
783
+ overlay_width=overlay_width,
784
+ overlay_opacity=overlay_opacity,
785
+ frequency=frequency,
786
+ date_format=None,
787
+ title=title,
788
+ title_xy=("2%", "90%"),
789
+ add_text=True,
790
+ text_xy=("2%", "2%"),
791
+ text_sequence=None,
792
+ font_type=font_type,
793
+ font_size=font_size,
794
+ font_color=font_color,
795
+ add_progress_bar=True,
796
+ progress_bar_color=progress_bar_color,
797
+ progress_bar_height=5,
798
+ loop=0,
799
+ mp4=mp4,
800
+ fading=fading,
801
+ )
802
+ except:
803
+ empty_text.error(
804
+ "An error occurred while computing the timelapse. Your probably requested too much data. Try reducing the ROI or timespan."
805
+ )
806
+ st.stop()
807
+
808
+ if out_gif is not None and os.path.exists(out_gif):
809
+
810
+ empty_text.text(
811
+ "Right click the GIF to save it to your computerπŸ‘‡"
812
+ )
813
+ empty_image.image(out_gif)
814
+
815
+ out_mp4 = out_gif.replace(".gif", ".mp4")
816
+ if mp4 and os.path.exists(out_mp4):
817
+ with empty_video:
818
+ st.text(
819
+ "Right click the MP4 to save it to your computerπŸ‘‡"
820
+ )
821
+ st.video(out_gif.replace(".gif", ".mp4"))
822
+
823
+ else:
824
+ empty_text.error(
825
+ "Something went wrong. You probably requested too much data. Try reducing the ROI or timespan."
826
+ )
827
+
828
+ elif collection == "Geostationary Operational Environmental Satellites (GOES)":
829
+
830
+ video_empty.video("https://youtu.be/16fA2QORG4A")
831
+
832
+ with st.form("submit_goes_form"):
833
+
834
+ roi = None
835
+ if st.session_state.get("roi") is not None:
836
+ roi = st.session_state.get("roi")
837
+ out_gif = geemap.temp_file_path(".gif")
838
+
839
+ satellite = st.selectbox("Select a satellite:", ["GOES-17", "GOES-16"])
840
+ earliest_date = datetime.date(2017, 7, 10)
841
+ latest_date = datetime.date.today()
842
+
843
+ if sample_roi == "Uploaded GeoJSON":
844
+ roi_start_date = today - datetime.timedelta(days=2)
845
+ roi_end_date = today - datetime.timedelta(days=1)
846
+ roi_start_time = datetime.time(14, 00)
847
+ roi_end_time = datetime.time(1, 00)
848
+ else:
849
+ roi_start = goes_rois[sample_roi]["start_time"]
850
+ roi_end = goes_rois[sample_roi]["end_time"]
851
+ roi_start_date = datetime.datetime.strptime(
852
+ roi_start[:10], "%Y-%m-%d"
853
+ )
854
+ roi_end_date = datetime.datetime.strptime(roi_end[:10], "%Y-%m-%d")
855
+ roi_start_time = datetime.time(
856
+ int(roi_start[11:13]), int(roi_start[14:16])
857
+ )
858
+ roi_end_time = datetime.time(
859
+ int(roi_end[11:13]), int(roi_end[14:16])
860
+ )
861
+
862
+ start_date = st.date_input("Select the start date:", roi_start_date)
863
+ end_date = st.date_input("Select the end date:", roi_end_date)
864
+
865
+ with st.expander("Customize timelapse"):
866
+
867
+ add_fire = st.checkbox("Add Fire/Hotspot Characterization", False)
868
+
869
+ scan_type = st.selectbox(
870
+ "Select a scan type:", ["Full Disk", "CONUS", "Mesoscale"]
871
+ )
872
+
873
+ start_time = st.time_input(
874
+ "Select the start time of the start date:", roi_start_time
875
+ )
876
+
877
+ end_time = st.time_input(
878
+ "Select the end time of the end date:", roi_end_time
879
+ )
880
+
881
+ start = (
882
+ start_date.strftime("%Y-%m-%d")
883
+ + "T"
884
+ + start_time.strftime("%H:%M:%S")
885
+ )
886
+ end = (
887
+ end_date.strftime("%Y-%m-%d")
888
+ + "T"
889
+ + end_time.strftime("%H:%M:%S")
890
+ )
891
+
892
+ speed = st.slider("Frames per second:", 1, 30, 5)
893
+ add_progress_bar = st.checkbox("Add a progress bar", True)
894
+ progress_bar_color = st.color_picker(
895
+ "Progress bar color:", "#0000ff"
896
+ )
897
+ font_size = st.slider("Font size:", 10, 50, 20)
898
+ font_color = st.color_picker("Font color:", "#ffffff")
899
+ fading = st.slider(
900
+ "Fading duration (seconds) for each frame:", 0.0, 3.0, 0.0
901
+ )
902
+ mp4 = st.checkbox("Save timelapse as MP4", True)
903
+
904
+ empty_text = st.empty()
905
+ empty_image = st.empty()
906
+ empty_video = st.container()
907
+ empty_fire_text = st.empty()
908
+ empty_fire_image = st.empty()
909
+
910
+ submitted = st.form_submit_button("Submit")
911
+ if submitted:
912
+ if sample_roi == "Uploaded GeoJSON" and data is None:
913
+ empty_text.warning(
914
+ "Steps to create a timelapse: Draw a rectangle on the map -> Export it as a GeoJSON -> Upload it back to the app -> Click the Submit button. Alternatively, you can select a sample ROI from the dropdown list."
915
+ )
916
+ else:
917
+ empty_text.text("Computing... Please wait...")
918
+
919
+ geemap.goes_timelapse(
920
+ roi,
921
+ out_gif,
922
+ start_date=start,
923
+ end_date=end,
924
+ data=satellite,
925
+ scan=scan_type.replace(" ", "_").lower(),
926
+ dimensions=768,
927
+ framesPerSecond=speed,
928
+ date_format="YYYY-MM-dd HH:mm",
929
+ xy=("3%", "3%"),
930
+ text_sequence=None,
931
+ font_type="arial.ttf",
932
+ font_size=font_size,
933
+ font_color=font_color,
934
+ add_progress_bar=add_progress_bar,
935
+ progress_bar_color=progress_bar_color,
936
+ progress_bar_height=5,
937
+ loop=0,
938
+ overlay_data=overlay_data,
939
+ overlay_color=overlay_color,
940
+ overlay_width=overlay_width,
941
+ overlay_opacity=overlay_opacity,
942
+ mp4=mp4,
943
+ fading=fading,
944
+ )
945
+
946
+ if out_gif is not None and os.path.exists(out_gif):
947
+ empty_text.text(
948
+ "Right click the GIF to save it to your computerπŸ‘‡"
949
+ )
950
+ empty_image.image(out_gif)
951
+
952
+ out_mp4 = out_gif.replace(".gif", ".mp4")
953
+ if mp4 and os.path.exists(out_mp4):
954
+ with empty_video:
955
+ st.text(
956
+ "Right click the MP4 to save it to your computerπŸ‘‡"
957
+ )
958
+ st.video(out_gif.replace(".gif", ".mp4"))
959
+
960
+ if add_fire:
961
+ out_fire_gif = geemap.temp_file_path(".gif")
962
+ empty_fire_text.text(
963
+ "Delineating Fire Hotspot... Please wait..."
964
+ )
965
+ geemap.goes_fire_timelapse(
966
+ out_fire_gif,
967
+ start_date=start,
968
+ end_date=end,
969
+ data=satellite,
970
+ scan=scan_type.replace(" ", "_").lower(),
971
+ region=roi,
972
+ dimensions=768,
973
+ framesPerSecond=speed,
974
+ date_format="YYYY-MM-dd HH:mm",
975
+ xy=("3%", "3%"),
976
+ text_sequence=None,
977
+ font_type="arial.ttf",
978
+ font_size=font_size,
979
+ font_color=font_color,
980
+ add_progress_bar=add_progress_bar,
981
+ progress_bar_color=progress_bar_color,
982
+ progress_bar_height=5,
983
+ loop=0,
984
+ )
985
+ if os.path.exists(out_fire_gif):
986
+ empty_fire_image.image(out_fire_gif)
987
+ else:
988
+ empty_text.text(
989
+ "Something went wrong, either the ROI is too big or there are no data available for the specified date range. Please try a smaller ROI or different date range."
990
+ )
991
+
992
+ elif collection == "MODIS Vegetation Indices (NDVI/EVI) 16-Day Global 1km":
993
+
994
+ video_empty.video("https://youtu.be/16fA2QORG4A")
995
+
996
+ satellite = st.selectbox("Select a satellite:", ["Terra", "Aqua"])
997
+ band = st.selectbox("Select a band:", ["NDVI", "EVI"])
998
+
999
+ with st.form("submit_modis_form"):
1000
+
1001
+ roi = None
1002
+ if st.session_state.get("roi") is not None:
1003
+ roi = st.session_state.get("roi")
1004
+ out_gif = geemap.temp_file_path(".gif")
1005
+
1006
+ with st.expander("Customize timelapse"):
1007
+
1008
+ start = st.date_input(
1009
+ "Select a start date:", datetime.date(2000, 2, 8)
1010
+ )
1011
+ end = st.date_input("Select an end date:", datetime.date.today())
1012
+
1013
+ start_date = start.strftime("%Y-%m-%d")
1014
+ end_date = end.strftime("%Y-%m-%d")
1015
+
1016
+ speed = st.slider("Frames per second:", 1, 30, 5)
1017
+ add_progress_bar = st.checkbox("Add a progress bar", True)
1018
+ progress_bar_color = st.color_picker(
1019
+ "Progress bar color:", "#0000ff"
1020
+ )
1021
+ font_size = st.slider("Font size:", 10, 50, 20)
1022
+ font_color = st.color_picker("Font color:", "#ffffff")
1023
+
1024
+ font_type = st.selectbox(
1025
+ "Select the font type for the title:",
1026
+ ["arial.ttf", "alibaba.otf"],
1027
+ index=0,
1028
+ )
1029
+ fading = st.slider(
1030
+ "Fading duration (seconds) for each frame:", 0.0, 3.0, 0.0
1031
+ )
1032
+ mp4 = st.checkbox("Save timelapse as MP4", True)
1033
+
1034
+ empty_text = st.empty()
1035
+ empty_image = st.empty()
1036
+ empty_video = st.container()
1037
+
1038
+ submitted = st.form_submit_button("Submit")
1039
+ if submitted:
1040
+ if sample_roi == "Uploaded GeoJSON" and data is None:
1041
+ empty_text.warning(
1042
+ "Steps to create a timelapse: Draw a rectangle on the map -> Export it as a GeoJSON -> Upload it back to the app -> Click the Submit button. Alternatively, you can select a sample ROI from the dropdown list."
1043
+ )
1044
+ else:
1045
+
1046
+ empty_text.text("Computing... Please wait...")
1047
+
1048
+ geemap.modis_ndvi_timelapse(
1049
+ roi,
1050
+ out_gif,
1051
+ satellite,
1052
+ band,
1053
+ start_date,
1054
+ end_date,
1055
+ 768,
1056
+ speed,
1057
+ overlay_data=overlay_data,
1058
+ overlay_color=overlay_color,
1059
+ overlay_width=overlay_width,
1060
+ overlay_opacity=overlay_opacity,
1061
+ mp4=mp4,
1062
+ fading=fading,
1063
+ )
1064
+
1065
+ geemap.reduce_gif_size(out_gif)
1066
+
1067
+ empty_text.text(
1068
+ "Right click the GIF to save it to your computerπŸ‘‡"
1069
+ )
1070
+ empty_image.image(out_gif)
1071
+
1072
+ out_mp4 = out_gif.replace(".gif", ".mp4")
1073
+ if mp4 and os.path.exists(out_mp4):
1074
+ with empty_video:
1075
+ st.text(
1076
+ "Right click the MP4 to save it to your computerπŸ‘‡"
1077
+ )
1078
+ st.video(out_gif.replace(".gif", ".mp4"))
1079
+
1080
+ elif collection == "Any Earth Engine ImageCollection":
1081
+
1082
+ with st.form("submit_ts_form"):
1083
+ with st.expander("Customize timelapse"):
1084
+
1085
+ title = st.text_input(
1086
+ "Enter a title to show on the timelapse: ", "Timelapse"
1087
+ )
1088
+ start_date = st.date_input(
1089
+ "Select the start date:", datetime.date(2020, 1, 1)
1090
+ )
1091
+ end_date = st.date_input(
1092
+ "Select the end date:", datetime.date.today()
1093
+ )
1094
+ frequency = st.selectbox(
1095
+ "Select a temporal frequency:",
1096
+ ["year", "quarter", "month", "day", "hour", "minute", "second"],
1097
+ index=0,
1098
+ )
1099
+ reducer = st.selectbox(
1100
+ "Select a reducer for aggregating data:",
1101
+ ["median", "mean", "min", "max", "sum", "variance", "stdDev"],
1102
+ index=0,
1103
+ )
1104
+ data_format = st.selectbox(
1105
+ "Select a date format to show on the timelapse:",
1106
+ [
1107
+ "YYYY-MM-dd",
1108
+ "YYYY",
1109
+ "YYMM-MM",
1110
+ "YYYY-MM-dd HH:mm",
1111
+ "YYYY-MM-dd HH:mm:ss",
1112
+ "HH:mm",
1113
+ "HH:mm:ss",
1114
+ "w",
1115
+ "M",
1116
+ "d",
1117
+ "D",
1118
+ ],
1119
+ index=0,
1120
+ )
1121
+
1122
+ speed = st.slider("Frames per second:", 1, 30, 5)
1123
+ add_progress_bar = st.checkbox("Add a progress bar", True)
1124
+ progress_bar_color = st.color_picker(
1125
+ "Progress bar color:", "#0000ff"
1126
+ )
1127
+ font_size = st.slider("Font size:", 10, 50, 30)
1128
+ font_color = st.color_picker("Font color:", "#ffffff")
1129
+ font_type = st.selectbox(
1130
+ "Select the font type for the title:",
1131
+ ["arial.ttf", "alibaba.otf"],
1132
+ index=0,
1133
+ )
1134
+ fading = st.slider(
1135
+ "Fading duration (seconds) for each frame:", 0.0, 3.0, 0.0
1136
+ )
1137
+ mp4 = st.checkbox("Save timelapse as MP4", True)
1138
+
1139
+ empty_text = st.empty()
1140
+ empty_image = st.empty()
1141
+ empty_video = st.container()
1142
+ empty_fire_image = st.empty()
1143
+
1144
+ roi = None
1145
+ if st.session_state.get("roi") is not None:
1146
+ roi = st.session_state.get("roi")
1147
+ out_gif = geemap.temp_file_path(".gif")
1148
+
1149
+ submitted = st.form_submit_button("Submit")
1150
+ if submitted:
1151
+
1152
+ if sample_roi == "Uploaded GeoJSON" and data is None:
1153
+ empty_text.warning(
1154
+ "Steps to create a timelapse: Draw a rectangle on the map -> Export it as a GeoJSON -> Upload it back to the app -> Click the Submit button. Alternatively, you can select a sample ROI from the dropdown list."
1155
+ )
1156
+ else:
1157
+
1158
+ empty_text.text("Computing... Please wait...")
1159
+ try:
1160
+ geemap.create_timelapse(
1161
+ st.session_state.get("ee_asset_id"),
1162
+ start_date=start_date.strftime("%Y-%m-%d"),
1163
+ end_date=end_date.strftime("%Y-%m-%d"),
1164
+ region=roi,
1165
+ frequency=frequency,
1166
+ reducer=reducer,
1167
+ date_format=data_format,
1168
+ out_gif=out_gif,
1169
+ bands=st.session_state.get("bands"),
1170
+ palette=st.session_state.get("palette"),
1171
+ vis_params=st.session_state.get("vis_params"),
1172
+ dimensions=768,
1173
+ frames_per_second=speed,
1174
+ crs="EPSG:3857",
1175
+ overlay_data=overlay_data,
1176
+ overlay_color=overlay_color,
1177
+ overlay_width=overlay_width,
1178
+ overlay_opacity=overlay_opacity,
1179
+ title=title,
1180
+ title_xy=("2%", "90%"),
1181
+ add_text=True,
1182
+ text_xy=("2%", "2%"),
1183
+ text_sequence=None,
1184
+ font_type=font_type,
1185
+ font_size=font_size,
1186
+ font_color=font_color,
1187
+ add_progress_bar=add_progress_bar,
1188
+ progress_bar_color=progress_bar_color,
1189
+ progress_bar_height=5,
1190
+ loop=0,
1191
+ mp4=mp4,
1192
+ fading=fading,
1193
+ )
1194
+ except:
1195
+ empty_text.error(
1196
+ "An error occurred while computing the timelapse. You probably requested too much data. Try reducing the ROI or timespan."
1197
+ )
1198
+
1199
+ empty_text.text(
1200
+ "Right click the GIF to save it to your computerπŸ‘‡"
1201
+ )
1202
+ empty_image.image(out_gif)
1203
+
1204
+ out_mp4 = out_gif.replace(".gif", ".mp4")
1205
+ if mp4 and os.path.exists(out_mp4):
1206
+ with empty_video:
1207
+ st.text(
1208
+ "Right click the MP4 to save it to your computerπŸ‘‡"
1209
+ )
1210
+ st.video(out_gif.replace(".gif", ".mp4"))
1211
+
1212
+ elif collection in [
1213
+ "MODIS Gap filled Land Surface Temperature Daily",
1214
+ "MODIS Ocean Color SMI",
1215
+ ]:
1216
+
1217
+ with st.form("submit_ts_form"):
1218
+ with st.expander("Customize timelapse"):
1219
+
1220
+ title = st.text_input(
1221
+ "Enter a title to show on the timelapse: ",
1222
+ "Surface Temperature",
1223
+ )
1224
+ start_date = st.date_input(
1225
+ "Select the start date:", datetime.date(2018, 1, 1)
1226
+ )
1227
+ end_date = st.date_input(
1228
+ "Select the end date:", datetime.date(2020, 12, 31)
1229
+ )
1230
+ frequency = st.selectbox(
1231
+ "Select a temporal frequency:",
1232
+ ["year", "quarter", "month", "week", "day"],
1233
+ index=2,
1234
+ )
1235
+ reducer = st.selectbox(
1236
+ "Select a reducer for aggregating data:",
1237
+ ["median", "mean", "min", "max", "sum", "variance", "stdDev"],
1238
+ index=0,
1239
+ )
1240
+
1241
+ vis_params = st.text_area(
1242
+ "Enter visualization parameters",
1243
+ "",
1244
+ help="Enter a string in the format of a dictionary, such as '{'min': 23, 'max': 32}'",
1245
+ )
1246
+
1247
+ speed = st.slider("Frames per second:", 1, 30, 5)
1248
+ add_progress_bar = st.checkbox("Add a progress bar", True)
1249
+ progress_bar_color = st.color_picker(
1250
+ "Progress bar color:", "#0000ff"
1251
+ )
1252
+ font_size = st.slider("Font size:", 10, 50, 30)
1253
+ font_color = st.color_picker("Font color:", "#ffffff")
1254
+ font_type = st.selectbox(
1255
+ "Select the font type for the title:",
1256
+ ["arial.ttf", "alibaba.otf"],
1257
+ index=0,
1258
+ )
1259
+ add_colorbar = st.checkbox("Add a colorbar", True)
1260
+ colorbar_label = st.text_input(
1261
+ "Enter the colorbar label:", "Surface Temperature (Β°C)"
1262
+ )
1263
+ fading = st.slider(
1264
+ "Fading duration (seconds) for each frame:", 0.0, 3.0, 0.0
1265
+ )
1266
+ mp4 = st.checkbox("Save timelapse as MP4", True)
1267
+
1268
+ empty_text = st.empty()
1269
+ empty_image = st.empty()
1270
+ empty_video = st.container()
1271
+
1272
+ roi = None
1273
+ if st.session_state.get("roi") is not None:
1274
+ roi = st.session_state.get("roi")
1275
+ out_gif = geemap.temp_file_path(".gif")
1276
+
1277
+ submitted = st.form_submit_button("Submit")
1278
+ if submitted:
1279
+
1280
+ if sample_roi == "Uploaded GeoJSON" and data is None:
1281
+ empty_text.warning(
1282
+ "Steps to create a timelapse: Draw a rectangle on the map -> Export it as a GeoJSON -> Upload it back to the app -> Click the Submit button. Alternatively, you can select a sample ROI from the dropdown list."
1283
+ )
1284
+ else:
1285
+
1286
+ empty_text.text("Computing... Please wait...")
1287
+ try:
1288
+ if (
1289
+ collection
1290
+ == "MODIS Gap filled Land Surface Temperature Daily"
1291
+ ):
1292
+ out_gif = geemap.create_timelapse(
1293
+ st.session_state.get("ee_asset_id"),
1294
+ start_date=start_date.strftime("%Y-%m-%d"),
1295
+ end_date=end_date.strftime("%Y-%m-%d"),
1296
+ region=roi,
1297
+ bands=None,
1298
+ frequency=frequency,
1299
+ reducer=reducer,
1300
+ date_format=None,
1301
+ out_gif=out_gif,
1302
+ palette=st.session_state.get("palette"),
1303
+ vis_params=None,
1304
+ dimensions=768,
1305
+ frames_per_second=speed,
1306
+ crs="EPSG:3857",
1307
+ overlay_data=overlay_data,
1308
+ overlay_color=overlay_color,
1309
+ overlay_width=overlay_width,
1310
+ overlay_opacity=overlay_opacity,
1311
+ title=title,
1312
+ title_xy=("2%", "90%"),
1313
+ add_text=True,
1314
+ text_xy=("2%", "2%"),
1315
+ text_sequence=None,
1316
+ font_type=font_type,
1317
+ font_size=font_size,
1318
+ font_color=font_color,
1319
+ add_progress_bar=add_progress_bar,
1320
+ progress_bar_color=progress_bar_color,
1321
+ progress_bar_height=5,
1322
+ add_colorbar=add_colorbar,
1323
+ colorbar_label=colorbar_label,
1324
+ loop=0,
1325
+ mp4=mp4,
1326
+ fading=fading,
1327
+ )
1328
+ elif collection == "MODIS Ocean Color SMI":
1329
+ if vis_params.startswith("{") and vis_params.endswith(
1330
+ "}"
1331
+ ):
1332
+ vis_params = eval(vis_params)
1333
+ else:
1334
+ vis_params = None
1335
+ out_gif = geemap.modis_ocean_color_timelapse(
1336
+ st.session_state.get("ee_asset_id"),
1337
+ start_date=start_date.strftime("%Y-%m-%d"),
1338
+ end_date=end_date.strftime("%Y-%m-%d"),
1339
+ region=roi,
1340
+ bands=st.session_state["band"],
1341
+ frequency=frequency,
1342
+ reducer=reducer,
1343
+ date_format=None,
1344
+ out_gif=out_gif,
1345
+ palette=st.session_state.get("palette"),
1346
+ vis_params=vis_params,
1347
+ dimensions=768,
1348
+ frames_per_second=speed,
1349
+ crs="EPSG:3857",
1350
+ overlay_data=overlay_data,
1351
+ overlay_color=overlay_color,
1352
+ overlay_width=overlay_width,
1353
+ overlay_opacity=overlay_opacity,
1354
+ title=title,
1355
+ title_xy=("2%", "90%"),
1356
+ add_text=True,
1357
+ text_xy=("2%", "2%"),
1358
+ text_sequence=None,
1359
+ font_type=font_type,
1360
+ font_size=font_size,
1361
+ font_color=font_color,
1362
+ add_progress_bar=add_progress_bar,
1363
+ progress_bar_color=progress_bar_color,
1364
+ progress_bar_height=5,
1365
+ add_colorbar=add_colorbar,
1366
+ colorbar_label=colorbar_label,
1367
+ loop=0,
1368
+ mp4=mp4,
1369
+ fading=fading,
1370
+ )
1371
+ except:
1372
+ empty_text.error(
1373
+ "Something went wrong. You probably requested too much data. Try reducing the ROI or timespan."
1374
+ )
1375
+
1376
+ if out_gif is not None and os.path.exists(out_gif):
1377
+
1378
+ geemap.reduce_gif_size(out_gif)
1379
+
1380
+ empty_text.text(
1381
+ "Right click the GIF to save it to your computerπŸ‘‡"
1382
+ )
1383
+ empty_image.image(out_gif)
1384
+
1385
+ out_mp4 = out_gif.replace(".gif", ".mp4")
1386
+ if mp4 and os.path.exists(out_mp4):
1387
+ with empty_video:
1388
+ st.text(
1389
+ "Right click the MP4 to save it to your computerπŸ‘‡"
1390
+ )
1391
+ st.video(out_gif.replace(".gif", ".mp4"))
1392
+
1393
+ else:
1394
+ st.error(
1395
+ "Something went wrong. You probably requested too much data. Try reducing the ROI or timespan."
1396
+ )
1397
+
1398
+ elif collection == "USDA National Agriculture Imagery Program (NAIP)":
1399
+
1400
+ with st.form("submit_naip_form"):
1401
+ with st.expander("Customize timelapse"):
1402
+
1403
+ title = st.text_input(
1404
+ "Enter a title to show on the timelapse: ", "NAIP Timelapse"
1405
+ )
1406
+
1407
+ years = st.slider(
1408
+ "Start and end year:",
1409
+ 2003,
1410
+ today.year,
1411
+ (2003, today.year),
1412
+ )
1413
+
1414
+ bands = st.selectbox(
1415
+ "Select a band combination:", ["N/R/G", "R/G/B"], index=0
1416
+ )
1417
+
1418
+ speed = st.slider("Frames per second:", 1, 30, 3)
1419
+ add_progress_bar = st.checkbox("Add a progress bar", True)
1420
+ progress_bar_color = st.color_picker(
1421
+ "Progress bar color:", "#0000ff"
1422
+ )
1423
+ font_size = st.slider("Font size:", 10, 50, 30)
1424
+ font_color = st.color_picker("Font color:", "#ffffff")
1425
+ font_type = st.selectbox(
1426
+ "Select the font type for the title:",
1427
+ ["arial.ttf", "alibaba.otf"],
1428
+ index=0,
1429
+ )
1430
+ fading = st.slider(
1431
+ "Fading duration (seconds) for each frame:", 0.0, 3.0, 0.0
1432
+ )
1433
+ mp4 = st.checkbox("Save timelapse as MP4", True)
1434
+
1435
+ empty_text = st.empty()
1436
+ empty_image = st.empty()
1437
+ empty_video = st.container()
1438
+ empty_fire_image = st.empty()
1439
+
1440
+ roi = None
1441
+ if st.session_state.get("roi") is not None:
1442
+ roi = st.session_state.get("roi")
1443
+ out_gif = geemap.temp_file_path(".gif")
1444
+
1445
+ submitted = st.form_submit_button("Submit")
1446
+ if submitted:
1447
+
1448
+ if sample_roi == "Uploaded GeoJSON" and data is None:
1449
+ empty_text.warning(
1450
+ "Steps to create a timelapse: Draw a rectangle on the map -> Export it as a GeoJSON -> Upload it back to the app -> Click the Submit button. Alternatively, you can select a sample ROI from the dropdown list."
1451
+ )
1452
+ else:
1453
+
1454
+ empty_text.text("Computing... Please wait...")
1455
+ try:
1456
+ geemap.naip_timelapse(
1457
+ roi,
1458
+ years[0],
1459
+ years[1],
1460
+ out_gif,
1461
+ bands=bands.split("/"),
1462
+ palette=st.session_state.get("palette"),
1463
+ vis_params=None,
1464
+ dimensions=768,
1465
+ frames_per_second=speed,
1466
+ crs="EPSG:3857",
1467
+ overlay_data=overlay_data,
1468
+ overlay_color=overlay_color,
1469
+ overlay_width=overlay_width,
1470
+ overlay_opacity=overlay_opacity,
1471
+ title=title,
1472
+ title_xy=("2%", "90%"),
1473
+ add_text=True,
1474
+ text_xy=("2%", "2%"),
1475
+ text_sequence=None,
1476
+ font_type=font_type,
1477
+ font_size=font_size,
1478
+ font_color=font_color,
1479
+ add_progress_bar=add_progress_bar,
1480
+ progress_bar_color=progress_bar_color,
1481
+ progress_bar_height=5,
1482
+ loop=0,
1483
+ mp4=mp4,
1484
+ fading=fading,
1485
+ )
1486
+ except:
1487
+ empty_text.error(
1488
+ "Something went wrong. You either requested too much data or the ROI is outside the U.S."
1489
+ )
1490
+
1491
+ if out_gif is not None and os.path.exists(out_gif):
1492
+
1493
+ empty_text.text(
1494
+ "Right click the GIF to save it to your computerπŸ‘‡"
1495
+ )
1496
+ empty_image.image(out_gif)
1497
+
1498
+ out_mp4 = out_gif.replace(".gif", ".mp4")
1499
+ if mp4 and os.path.exists(out_mp4):
1500
+ with empty_video:
1501
+ st.text(
1502
+ "Right click the MP4 to save it to your computerπŸ‘‡"
1503
+ )
1504
+ st.video(out_gif.replace(".gif", ".mp4"))
1505
+
1506
+ else:
1507
+ st.error(
1508
+ "Something went wrong. You either requested too much data or the ROI is outside the U.S."
1509
+ )
1510
+
1511
+
1512
+ try:
1513
+ app()
1514
+ except Exception as e:
1515
+ pass
pages/2_🏠_U.S._Housing.py ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import os
3
+ import pathlib
4
+ import requests
5
+ import zipfile
6
+ import pandas as pd
7
+ import pydeck as pdk
8
+ import geopandas as gpd
9
+ import streamlit as st
10
+ import leafmap.colormaps as cm
11
+ from leafmap.common import hex_to_rgb
12
+
13
+ st.set_page_config(layout="wide")
14
+
15
+
16
+
17
+ STREAMLIT_STATIC_PATH = pathlib.Path(st.__path__[0]) / "static"
18
+ # We create a downloads directory within the streamlit static asset directory
19
+ # and we write output files to it
20
+ DOWNLOADS_PATH = STREAMLIT_STATIC_PATH / "downloads"
21
+ if not DOWNLOADS_PATH.is_dir():
22
+ DOWNLOADS_PATH.mkdir()
23
+
24
+ # Data source: https://www.realtor.com/research/data/
25
+ # link_prefix = "https://econdata.s3-us-west-2.amazonaws.com/Reports/"
26
+ link_prefix = "https://raw.githubusercontent.com/giswqs/data/main/housing/"
27
+
28
+ data_links = {
29
+ "weekly": {
30
+ "national": link_prefix + "Core/listing_weekly_core_aggregate_by_country.csv",
31
+ "metro": link_prefix + "Core/listing_weekly_core_aggregate_by_metro.csv",
32
+ },
33
+ "monthly_current": {
34
+ "national": link_prefix + "Core/RDC_Inventory_Core_Metrics_Country.csv",
35
+ "state": link_prefix + "Core/RDC_Inventory_Core_Metrics_State.csv",
36
+ "metro": link_prefix + "Core/RDC_Inventory_Core_Metrics_Metro.csv",
37
+ "county": link_prefix + "Core/RDC_Inventory_Core_Metrics_County.csv",
38
+ "zip": link_prefix + "Core/RDC_Inventory_Core_Metrics_Zip.csv",
39
+ },
40
+ "monthly_historical": {
41
+ "national": link_prefix + "Core/RDC_Inventory_Core_Metrics_Country_History.csv",
42
+ "state": link_prefix + "Core/RDC_Inventory_Core_Metrics_State_History.csv",
43
+ "metro": link_prefix + "Core/RDC_Inventory_Core_Metrics_Metro_History.csv",
44
+ "county": link_prefix + "Core/RDC_Inventory_Core_Metrics_County_History.csv",
45
+ "zip": link_prefix + "Core/RDC_Inventory_Core_Metrics_Zip_History.csv",
46
+ },
47
+ "hotness": {
48
+ "metro": link_prefix
49
+ + "Hotness/RDC_Inventory_Hotness_Metrics_Metro_History.csv",
50
+ "county": link_prefix
51
+ + "Hotness/RDC_Inventory_Hotness_Metrics_County_History.csv",
52
+ "zip": link_prefix + "Hotness/RDC_Inventory_Hotness_Metrics_Zip_History.csv",
53
+ },
54
+ }
55
+
56
+
57
+ def get_data_columns(df, category, frequency="monthly"):
58
+ if frequency == "monthly":
59
+ if category.lower() == "county":
60
+ del_cols = ["month_date_yyyymm", "county_fips", "county_name"]
61
+ elif category.lower() == "state":
62
+ del_cols = ["month_date_yyyymm", "state", "state_id"]
63
+ elif category.lower() == "national":
64
+ del_cols = ["month_date_yyyymm", "country"]
65
+ elif category.lower() == "metro":
66
+ del_cols = ["month_date_yyyymm", "cbsa_code", "cbsa_title", "HouseholdRank"]
67
+ elif category.lower() == "zip":
68
+ del_cols = ["month_date_yyyymm", "postal_code", "zip_name", "flag"]
69
+ elif frequency == "weekly":
70
+ if category.lower() == "national":
71
+ del_cols = ["week_end_date", "geo_country"]
72
+ elif category.lower() == "metro":
73
+ del_cols = ["week_end_date", "cbsa_code", "cbsa_title", "hh_rank"]
74
+
75
+ cols = df.columns.values.tolist()
76
+
77
+ for col in cols:
78
+ if col.strip() in del_cols:
79
+ cols.remove(col)
80
+ if category.lower() == "metro":
81
+ return cols[2:]
82
+ else:
83
+ return cols[1:]
84
+
85
+
86
+ @st.cache_data
87
+ def get_inventory_data(url):
88
+ df = pd.read_csv(url)
89
+ url = url.lower()
90
+ if "county" in url:
91
+ df["county_fips"] = df["county_fips"].map(str)
92
+ df["county_fips"] = df["county_fips"].str.zfill(5)
93
+ elif "state" in url:
94
+ df["STUSPS"] = df["state_id"].str.upper()
95
+ elif "metro" in url:
96
+ df["cbsa_code"] = df["cbsa_code"].map(str)
97
+ elif "zip" in url:
98
+ df["postal_code"] = df["postal_code"].map(str)
99
+ df["postal_code"] = df["postal_code"].str.zfill(5)
100
+
101
+ if "listing_weekly_core_aggregate_by_country" in url:
102
+ columns = get_data_columns(df, "national", "weekly")
103
+ for column in columns:
104
+ if column != "median_days_on_market_by_day_yy":
105
+ df[column] = df[column].str.rstrip("%").astype(float) / 100
106
+ if "listing_weekly_core_aggregate_by_metro" in url:
107
+ columns = get_data_columns(df, "metro", "weekly")
108
+ for column in columns:
109
+ if column != "median_days_on_market_by_day_yy":
110
+ df[column] = df[column].str.rstrip("%").astype(float) / 100
111
+ df["cbsa_code"] = df["cbsa_code"].str[:5]
112
+ return df
113
+
114
+
115
+ def filter_weekly_inventory(df, week):
116
+ df = df[df["week_end_date"] == week]
117
+ return df
118
+
119
+
120
+ def get_start_end_year(df):
121
+ start_year = int(str(df["month_date_yyyymm"].min())[:4])
122
+ end_year = int(str(df["month_date_yyyymm"].max())[:4])
123
+ return start_year, end_year
124
+
125
+
126
+ def get_periods(df):
127
+ return [str(d) for d in list(set(df["month_date_yyyymm"].tolist()))]
128
+
129
+
130
+ @st.cache_data
131
+ def get_geom_data(category):
132
+
133
+ prefix = (
134
+ "https://raw.githubusercontent.com/giswqs/streamlit-geospatial/master/data/"
135
+ )
136
+ links = {
137
+ "national": prefix + "us_nation.geojson",
138
+ "state": prefix + "us_states.geojson",
139
+ "county": prefix + "us_counties.geojson",
140
+ "metro": prefix + "us_metro_areas.geojson",
141
+ "zip": "https://www2.census.gov/geo/tiger/GENZ2018/shp/cb_2018_us_zcta510_500k.zip",
142
+ }
143
+
144
+ if category.lower() == "zip":
145
+ r = requests.get(links[category])
146
+ out_zip = os.path.join(DOWNLOADS_PATH, "cb_2018_us_zcta510_500k.zip")
147
+ with open(out_zip, "wb") as code:
148
+ code.write(r.content)
149
+ zip_ref = zipfile.ZipFile(out_zip, "r")
150
+ zip_ref.extractall(DOWNLOADS_PATH)
151
+ gdf = gpd.read_file(out_zip.replace("zip", "shp"))
152
+ else:
153
+ gdf = gpd.read_file(links[category])
154
+ return gdf
155
+
156
+
157
+ def join_attributes(gdf, df, category):
158
+
159
+ new_gdf = None
160
+ if category == "county":
161
+ new_gdf = gdf.merge(df, left_on="GEOID", right_on="county_fips", how="outer")
162
+ elif category == "state":
163
+ new_gdf = gdf.merge(df, left_on="STUSPS", right_on="STUSPS", how="outer")
164
+ elif category == "national":
165
+ if "geo_country" in df.columns.values.tolist():
166
+ df["country"] = None
167
+ df.loc[0, "country"] = "United States"
168
+ new_gdf = gdf.merge(df, left_on="NAME", right_on="country", how="outer")
169
+ elif category == "metro":
170
+ new_gdf = gdf.merge(df, left_on="CBSAFP", right_on="cbsa_code", how="outer")
171
+ elif category == "zip":
172
+ new_gdf = gdf.merge(df, left_on="GEOID10", right_on="postal_code", how="outer")
173
+ return new_gdf
174
+
175
+
176
+ def select_non_null(gdf, col_name):
177
+ new_gdf = gdf[~gdf[col_name].isna()]
178
+ return new_gdf
179
+
180
+
181
+ def select_null(gdf, col_name):
182
+ new_gdf = gdf[gdf[col_name].isna()]
183
+ return new_gdf
184
+
185
+
186
+ def get_data_dict(name):
187
+ in_csv = os.path.join(os.getcwd(), "data/realtor_data_dict.csv")
188
+ df = pd.read_csv(in_csv)
189
+ label = list(df[df["Name"] == name]["Label"])[0]
190
+ desc = list(df[df["Name"] == name]["Description"])[0]
191
+ return label, desc
192
+
193
+
194
+ def get_weeks(df):
195
+ seq = list(set(df[~df["week_end_date"].isnull()]["week_end_date"].tolist()))
196
+ weeks = [
197
+ datetime.date(int(d.split("/")[2]), int(d.split("/")[0]), int(d.split("/")[1]))
198
+ for d in seq
199
+ ]
200
+ weeks.sort()
201
+ return weeks
202
+
203
+
204
+ def get_saturday(in_date):
205
+ idx = (in_date.weekday() + 1) % 7
206
+ sat = in_date + datetime.timedelta(6 - idx)
207
+ return sat
208
+
209
+
210
+ def app():
211
+
212
+ st.title("U.S. Real Estate Data and Market Trends")
213
+ st.markdown(
214
+ """**Introduction:** This interactive dashboard is designed for visualizing U.S. real estate data and market trends at multiple levels (i.e., national,
215
+ state, county, and metro). The data sources include [Real Estate Data](https://www.realtor.com/research/data) from realtor.com and
216
+ [Cartographic Boundary Files](https://www.census.gov/geographies/mapping-files/time-series/geo/carto-boundary-file.html) from U.S. Census Bureau.
217
+ Several open-source packages are used to process the data and generate the visualizations, e.g., [streamlit](https://streamlit.io),
218
+ [geopandas](https://geopandas.org), [leafmap](https://leafmap.org), and [pydeck](https://deckgl.readthedocs.io).
219
+ """
220
+ )
221
+
222
+ with st.expander("See a demo"):
223
+ st.image("https://i.imgur.com/Z3dk6Tr.gif")
224
+
225
+ row1_col1, row1_col2, row1_col3, row1_col4, row1_col5 = st.columns(
226
+ [0.6, 0.8, 0.6, 1.4, 2]
227
+ )
228
+ with row1_col1:
229
+ frequency = st.selectbox("Monthly/weekly data", ["Monthly", "Weekly"])
230
+ with row1_col2:
231
+ types = ["Current month data", "Historical data"]
232
+ if frequency == "Weekly":
233
+ types.remove("Current month data")
234
+ cur_hist = st.selectbox(
235
+ "Current/historical data",
236
+ types,
237
+ )
238
+ with row1_col3:
239
+ if frequency == "Monthly":
240
+ scale = st.selectbox(
241
+ "Scale", ["National", "State", "Metro", "County"], index=3
242
+ )
243
+ else:
244
+ scale = st.selectbox("Scale", ["National", "Metro"], index=1)
245
+
246
+ gdf = get_geom_data(scale.lower())
247
+
248
+ if frequency == "Weekly":
249
+ inventory_df = get_inventory_data(data_links["weekly"][scale.lower()])
250
+ weeks = get_weeks(inventory_df)
251
+ with row1_col1:
252
+ selected_date = st.date_input("Select a date", value=weeks[-1])
253
+ saturday = get_saturday(selected_date)
254
+ selected_period = saturday.strftime("%-m/%-d/%Y")
255
+ if saturday not in weeks:
256
+ st.error(
257
+ "The selected date is not available in the data. Please select a date between {} and {}".format(
258
+ weeks[0], weeks[-1]
259
+ )
260
+ )
261
+ selected_period = weeks[-1].strftime("%-m/%-d/%Y")
262
+ inventory_df = get_inventory_data(data_links["weekly"][scale.lower()])
263
+ inventory_df = filter_weekly_inventory(inventory_df, selected_period)
264
+
265
+ if frequency == "Monthly":
266
+ if cur_hist == "Current month data":
267
+ inventory_df = get_inventory_data(
268
+ data_links["monthly_current"][scale.lower()]
269
+ )
270
+ selected_period = get_periods(inventory_df)[0]
271
+ else:
272
+ with row1_col2:
273
+ inventory_df = get_inventory_data(
274
+ data_links["monthly_historical"][scale.lower()]
275
+ )
276
+ start_year, end_year = get_start_end_year(inventory_df)
277
+ periods = get_periods(inventory_df)
278
+ with st.expander("Select year and month", True):
279
+ selected_year = st.slider(
280
+ "Year",
281
+ start_year,
282
+ end_year,
283
+ value=start_year,
284
+ step=1,
285
+ )
286
+ selected_month = st.slider(
287
+ "Month",
288
+ min_value=1,
289
+ max_value=12,
290
+ value=int(periods[0][-2:]),
291
+ step=1,
292
+ )
293
+ selected_period = str(selected_year) + str(selected_month).zfill(2)
294
+ if selected_period not in periods:
295
+ st.error("Data not available for selected year and month")
296
+ selected_period = periods[0]
297
+ inventory_df = inventory_df[
298
+ inventory_df["month_date_yyyymm"] == int(selected_period)
299
+ ]
300
+
301
+ data_cols = get_data_columns(inventory_df, scale.lower(), frequency.lower())
302
+
303
+ with row1_col4:
304
+ selected_col = st.selectbox("Attribute", data_cols)
305
+ with row1_col5:
306
+ show_desc = st.checkbox("Show attribute description")
307
+ if show_desc:
308
+ try:
309
+ label, desc = get_data_dict(selected_col.strip())
310
+ markdown = f"""
311
+ **{label}**: {desc}
312
+ """
313
+ st.markdown(markdown)
314
+ except:
315
+ st.warning("No description available for selected attribute")
316
+
317
+ row2_col1, row2_col2, row2_col3, row2_col4, row2_col5, row2_col6 = st.columns(
318
+ [0.6, 0.68, 0.7, 0.7, 1.5, 0.8]
319
+ )
320
+
321
+ palettes = cm.list_colormaps()
322
+ with row2_col1:
323
+ palette = st.selectbox("Color palette", palettes, index=palettes.index("Blues"))
324
+ with row2_col2:
325
+ n_colors = st.slider("Number of colors", min_value=2, max_value=20, value=8)
326
+ with row2_col3:
327
+ show_nodata = st.checkbox("Show nodata areas", value=True)
328
+ with row2_col4:
329
+ show_3d = st.checkbox("Show 3D view", value=False)
330
+ with row2_col5:
331
+ if show_3d:
332
+ elev_scale = st.slider(
333
+ "Elevation scale", min_value=1, max_value=1000000, value=1, step=10
334
+ )
335
+ with row2_col6:
336
+ st.info("Press Ctrl and move the left mouse button.")
337
+ else:
338
+ elev_scale = 1
339
+
340
+ gdf = join_attributes(gdf, inventory_df, scale.lower())
341
+ gdf_null = select_null(gdf, selected_col)
342
+ gdf = select_non_null(gdf, selected_col)
343
+ gdf = gdf.sort_values(by=selected_col, ascending=True)
344
+
345
+ colors = cm.get_palette(palette, n_colors)
346
+ colors = [hex_to_rgb(c) for c in colors]
347
+
348
+ for i, ind in enumerate(gdf.index):
349
+ index = int(i / (len(gdf) / len(colors)))
350
+ if index >= len(colors):
351
+ index = len(colors) - 1
352
+ gdf.loc[ind, "R"] = colors[index][0]
353
+ gdf.loc[ind, "G"] = colors[index][1]
354
+ gdf.loc[ind, "B"] = colors[index][2]
355
+
356
+ initial_view_state = pdk.ViewState(
357
+ latitude=40,
358
+ longitude=-100,
359
+ zoom=3,
360
+ max_zoom=16,
361
+ pitch=0,
362
+ bearing=0,
363
+ height=900,
364
+ width=None,
365
+ )
366
+
367
+ min_value = gdf[selected_col].min()
368
+ max_value = gdf[selected_col].max()
369
+ color = "color"
370
+ # color_exp = f"[({selected_col}-{min_value})/({max_value}-{min_value})*255, 0, 0]"
371
+ color_exp = f"[R, G, B]"
372
+
373
+ geojson = pdk.Layer(
374
+ "GeoJsonLayer",
375
+ gdf,
376
+ pickable=True,
377
+ opacity=0.5,
378
+ stroked=True,
379
+ filled=True,
380
+ extruded=show_3d,
381
+ wireframe=True,
382
+ get_elevation=f"{selected_col}",
383
+ elevation_scale=elev_scale,
384
+ # get_fill_color="color",
385
+ get_fill_color=color_exp,
386
+ get_line_color=[0, 0, 0],
387
+ get_line_width=2,
388
+ line_width_min_pixels=1,
389
+ )
390
+
391
+ geojson_null = pdk.Layer(
392
+ "GeoJsonLayer",
393
+ gdf_null,
394
+ pickable=True,
395
+ opacity=0.2,
396
+ stroked=True,
397
+ filled=True,
398
+ extruded=False,
399
+ wireframe=True,
400
+ # get_elevation="properties.ALAND/100000",
401
+ # get_fill_color="color",
402
+ get_fill_color=[200, 200, 200],
403
+ get_line_color=[0, 0, 0],
404
+ get_line_width=2,
405
+ line_width_min_pixels=1,
406
+ )
407
+
408
+ # tooltip = {"text": "Name: {NAME}"}
409
+
410
+ # tooltip_value = f"<b>Value:</b> {median_listing_price}""
411
+ tooltip = {
412
+ "html": "<b>Name:</b> {NAME}<br><b>Value:</b> {"
413
+ + selected_col
414
+ + "}<br><b>Date:</b> "
415
+ + selected_period
416
+ + "",
417
+ "style": {"backgroundColor": "steelblue", "color": "white"},
418
+ }
419
+
420
+ layers = [geojson]
421
+ if show_nodata:
422
+ layers.append(geojson_null)
423
+
424
+ r = pdk.Deck(
425
+ layers=layers,
426
+ initial_view_state=initial_view_state,
427
+ map_style="light",
428
+ tooltip=tooltip,
429
+ )
430
+
431
+ row3_col1, row3_col2 = st.columns([6, 1])
432
+
433
+ with row3_col1:
434
+ st.pydeck_chart(r)
435
+ with row3_col2:
436
+ st.write(
437
+ cm.create_colormap(
438
+ palette,
439
+ label=selected_col.replace("_", " ").title(),
440
+ width=0.2,
441
+ height=3,
442
+ orientation="vertical",
443
+ vmin=min_value,
444
+ vmax=max_value,
445
+ font_size=10,
446
+ )
447
+ )
448
+ row4_col1, row4_col2, row4_col3 = st.columns([1, 2, 3])
449
+ with row4_col1:
450
+ show_data = st.checkbox("Show raw data")
451
+ with row4_col2:
452
+ show_cols = st.multiselect("Select columns", data_cols)
453
+ with row4_col3:
454
+ show_colormaps = st.checkbox("Preview all color palettes")
455
+ if show_colormaps:
456
+ st.write(cm.plot_colormaps(return_fig=True))
457
+ if show_data:
458
+ if scale == "National":
459
+ st.dataframe(gdf[["NAME", "GEOID"] + show_cols])
460
+ elif scale == "State":
461
+ st.dataframe(gdf[["NAME", "STUSPS"] + show_cols])
462
+ elif scale == "County":
463
+ st.dataframe(gdf[["NAME", "STATEFP", "COUNTYFP"] + show_cols])
464
+ elif scale == "Metro":
465
+ st.dataframe(gdf[["NAME", "CBSAFP"] + show_cols])
466
+ elif scale == "Zip":
467
+ st.dataframe(gdf[["GEOID10"] + show_cols])
468
+
469
+
470
+ app()
pages/3_πŸͺŸ_Split_Map.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import leafmap.foliumap as leafmap
3
+
4
+ st.set_page_config(layout="wide")
5
+
6
+
7
+
8
+ st.title("Split-panel Map")
9
+
10
+ with st.expander("See source code"):
11
+ with st.echo():
12
+ m = leafmap.Map()
13
+ m.split_map(
14
+ left_layer='ESA WorldCover 2020 S2 FCC', right_layer='ESA WorldCover 2020'
15
+ )
16
+ m.add_legend(title='ESA Land Cover', builtin_legend='ESA_WorldCover')
17
+
18
+ m.to_streamlit(height=700)
pages/4_πŸ”₯_Heatmap.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import leafmap.foliumap as leafmap
3
+
4
+ st.set_page_config(layout="wide")
5
+
6
+
7
+
8
+ st.title("Heatmap")
9
+
10
+ with st.expander("See source code"):
11
+ with st.echo():
12
+ filepath = "https://raw.githubusercontent.com/giswqs/leafmap/master/examples/data/us_cities.csv"
13
+ m = leafmap.Map(center=[40, -100], zoom=4, tiles="stamentoner")
14
+ m.add_heatmap(
15
+ filepath,
16
+ latitude="latitude",
17
+ longitude="longitude",
18
+ value="pop_max",
19
+ name="Heat map",
20
+ radius=20,
21
+ )
22
+ m.to_streamlit(height=700)
pages/5_πŸ“_Marker_Cluster.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import leafmap.foliumap as leafmap
3
+
4
+ st.set_page_config(layout="wide")
5
+
6
+
7
+
8
+ st.title("Marker Cluster")
9
+
10
+ with st.expander("See source code"):
11
+ with st.echo():
12
+
13
+ m = leafmap.Map(center=[40, -100], zoom=4)
14
+ cities = 'https://raw.githubusercontent.com/giswqs/leafmap/master/examples/data/us_cities.csv'
15
+ regions = 'https://raw.githubusercontent.com/giswqs/leafmap/master/examples/data/us_regions.geojson'
16
+
17
+ m.add_geojson(regions, layer_name='US Regions')
18
+ m.add_points_from_xy(
19
+ cities,
20
+ x="longitude",
21
+ y="latitude",
22
+ color_column='region',
23
+ icon_names=['gear', 'map', 'leaf', 'globe'],
24
+ spin=True,
25
+ add_legend=True,
26
+ )
27
+
28
+ m.to_streamlit(height=700)
pages/6_πŸ—ΊοΈ_Basemaps.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import leafmap.foliumap as leafmap
3
+
4
+ st.set_page_config(layout="wide")
5
+
6
+ def app():
7
+ st.title("Search Basemaps")
8
+ st.markdown(
9
+ """
10
+ This app is a demonstration of searching and loading basemaps from [xyzservices](https://github.com/geopandas/xyzservices) and [Quick Map Services (QMS)](https://github.com/nextgis/quickmapservices). Selecting from 1000+ basemaps with a few clicks.
11
+ """
12
+ )
13
+
14
+ with st.expander("See demo"):
15
+ st.image("https://i.imgur.com/0SkUhZh.gif")
16
+
17
+ row1_col1, row1_col2 = st.columns([3, 1])
18
+ width = 800
19
+ height = 600
20
+ tiles = None
21
+
22
+ with row1_col2:
23
+
24
+ checkbox = st.checkbox("Search Quick Map Services (QMS)")
25
+ keyword = st.text_input("Enter a keyword to search and press Enter:")
26
+ empty = st.empty()
27
+
28
+ if keyword:
29
+ options = leafmap.search_xyz_services(keyword=keyword)
30
+ if checkbox:
31
+ qms = leafmap.search_qms(keyword=keyword)
32
+ if qms is not None:
33
+ options = options + qms
34
+
35
+ tiles = empty.multiselect(
36
+ "Select XYZ tiles to add to the map:", options)
37
+
38
+ with row1_col1:
39
+ m = leafmap.Map()
40
+
41
+ if tiles is not None:
42
+ for tile in tiles:
43
+ m.add_xyz_service(tile)
44
+
45
+ m.to_streamlit(height=height)
46
+
47
+
48
+ app()
pages/7_πŸ“¦_Web_Map_Service.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ast
2
+ import streamlit as st
3
+ import leafmap.foliumap as leafmap
4
+
5
+ st.set_page_config(layout="wide")
6
+
7
+
8
+
9
+
10
+ @st.cache_data
11
+ def get_layers(url):
12
+ options = leafmap.get_wms_layers(url)
13
+ return options
14
+
15
+
16
+ def app():
17
+ st.title("Web Map Service (WMS)")
18
+ st.markdown(
19
+ """
20
+ This app is a demonstration of loading Web Map Service (WMS) layers. Simply enter the URL of the WMS service
21
+ in the text box below and press Enter to retrieve the layers. Go to https://apps.nationalmap.gov/services to find
22
+ some WMS URLs if needed.
23
+ """
24
+ )
25
+
26
+ row1_col1, row1_col2 = st.columns([3, 1.3])
27
+ width = 800
28
+ height = 600
29
+ layers = None
30
+
31
+ with row1_col2:
32
+
33
+ esa_landcover = "https://services.terrascope.be/wms/v2"
34
+ url = st.text_input(
35
+ "Enter a WMS URL:", value="https://services.terrascope.be/wms/v2"
36
+ )
37
+ empty = st.empty()
38
+
39
+ if url:
40
+ options = get_layers(url)
41
+
42
+ default = None
43
+ if url == esa_landcover:
44
+ default = "WORLDCOVER_2020_MAP"
45
+ layers = empty.multiselect(
46
+ "Select WMS layers to add to the map:", options, default=default
47
+ )
48
+ add_legend = st.checkbox("Add a legend to the map", value=True)
49
+ if default == "WORLDCOVER_2020_MAP":
50
+ legend = str(leafmap.builtin_legends["ESA_WorldCover"])
51
+ else:
52
+ legend = ""
53
+ if add_legend:
54
+ legend_text = st.text_area(
55
+ "Enter a legend as a dictionary {label: color}",
56
+ value=legend,
57
+ height=200,
58
+ )
59
+
60
+ with row1_col1:
61
+ m = leafmap.Map(center=(36.3, 0), zoom=2)
62
+
63
+ if layers is not None:
64
+ for layer in layers:
65
+ m.add_wms_layer(
66
+ url, layers=layer, name=layer, attribution=" ", transparent=True
67
+ )
68
+ if add_legend and legend_text:
69
+ legend_dict = ast.literal_eval(legend_text)
70
+ m.add_legend(legend_dict=legend_dict)
71
+
72
+ m.to_streamlit(height=height)
73
+
74
+
75
+ app()
pages/8_🏜️_Raster_Data_Visualization.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import leafmap.foliumap as leafmap
3
+ import leafmap.colormaps as cm
4
+ import streamlit as st
5
+
6
+ st.set_page_config(layout="wide")
7
+
8
+
9
+
10
+
11
+ @st.cache_data
12
+ def load_cog_list():
13
+ print(os.getcwd())
14
+ in_txt = os.path.join(os.getcwd(), "data/cog_files.txt")
15
+ with open(in_txt) as f:
16
+ return [line.strip() for line in f.readlines()[1:]]
17
+
18
+
19
+ @st.cache_data
20
+ def get_palettes():
21
+ return list(cm.palettes.keys())
22
+ # palettes = dir(palettable.matplotlib)[:-16]
23
+ # return ["matplotlib." + p for p in palettes]
24
+
25
+
26
+ st.title("Visualize Raster Datasets")
27
+ st.markdown(
28
+ """
29
+ An interactive web app for visualizing local raster datasets and Cloud Optimized GeoTIFF ([COG](https://www.cogeo.org)). The app was built using [streamlit](https://streamlit.io), [leafmap](https://leafmap.org), and [Titiler](https://developmentseed.org/titiler/).
30
+
31
+
32
+ """
33
+ )
34
+
35
+ row1_col1, row1_col2 = st.columns([2, 1])
36
+
37
+ with row1_col1:
38
+ cog_list = load_cog_list()
39
+ cog = st.selectbox("Select a sample Cloud Opitmized GeoTIFF (COG)", cog_list)
40
+
41
+ with row1_col2:
42
+ empty = st.empty()
43
+
44
+ url = empty.text_input(
45
+ "Enter a HTTP URL to a Cloud Optimized GeoTIFF (COG)",
46
+ cog,
47
+ )
48
+
49
+ if url:
50
+ try:
51
+ options = leafmap.cog_bands(url)
52
+ except Exception as e:
53
+ st.error(e)
54
+ if len(options) > 3:
55
+ default = options[:3]
56
+ else:
57
+ default = options[0]
58
+ bands = st.multiselect("Select bands to display", options, default=options)
59
+
60
+ if len(bands) == 1 or len(bands) == 3:
61
+ pass
62
+ else:
63
+ st.error("Please select one or three bands")
64
+
65
+ add_params = st.checkbox("Add visualization parameters")
66
+ if add_params:
67
+ vis_params = st.text_area("Enter visualization parameters", "{}")
68
+ else:
69
+ vis_params = {}
70
+
71
+ if len(vis_params) > 0:
72
+ try:
73
+ vis_params = eval(vis_params)
74
+ except Exception as e:
75
+ st.error(
76
+ f"Invalid visualization parameters. It should be a dictionary. Error: {e}"
77
+ )
78
+ vis_params = {}
79
+
80
+ submit = st.button("Submit")
81
+
82
+ m = leafmap.Map(latlon_control=False)
83
+
84
+ if submit:
85
+ if url:
86
+ try:
87
+ m.add_cog_layer(url, bands=bands, **vis_params)
88
+ except Exception as e:
89
+ with row1_col2:
90
+ st.error(e)
91
+ st.error("Work in progress. Try it again later.")
92
+
93
+ with row1_col1:
94
+ m.to_streamlit()
pages/9_πŸ”²_Vector_Data_Visualization.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import fiona
3
+ import geopandas as gpd
4
+ import streamlit as st
5
+
6
+ st.set_page_config(layout="wide")
7
+
8
+
9
+
10
+
11
+ def save_uploaded_file(file_content, file_name):
12
+ """
13
+ Save the uploaded file to a temporary directory
14
+ """
15
+ import tempfile
16
+ import os
17
+ import uuid
18
+
19
+ _, file_extension = os.path.splitext(file_name)
20
+ file_id = str(uuid.uuid4())
21
+ file_path = os.path.join(tempfile.gettempdir(), f"{file_id}{file_extension}")
22
+
23
+ with open(file_path, "wb") as file:
24
+ file.write(file_content.getbuffer())
25
+
26
+ return file_path
27
+
28
+
29
+ def app():
30
+
31
+ st.title("Upload Vector Data")
32
+
33
+ row1_col1, row1_col2 = st.columns([2, 1])
34
+ width = 950
35
+ height = 600
36
+
37
+ with row1_col2:
38
+
39
+ backend = st.selectbox(
40
+ "Select a plotting backend", ["folium", "kepler.gl", "pydeck"], index=2
41
+ )
42
+
43
+ if backend == "folium":
44
+ import leafmap.foliumap as leafmap
45
+ elif backend == "kepler.gl":
46
+ import leafmap.kepler as leafmap
47
+ elif backend == "pydeck":
48
+ import leafmap.deck as leafmap
49
+
50
+ url = st.text_input(
51
+ "Enter a URL to a vector dataset",
52
+ "https://github.com/giswqs/streamlit-geospatial/raw/master/data/us_states.geojson",
53
+ )
54
+
55
+ data = st.file_uploader(
56
+ "Upload a vector dataset", type=["geojson", "kml", "zip", "tab"]
57
+ )
58
+
59
+ container = st.container()
60
+
61
+ if data or url:
62
+ if data:
63
+ file_path = save_uploaded_file(data, data.name)
64
+ layer_name = os.path.splitext(data.name)[0]
65
+ elif url:
66
+ file_path = url
67
+ layer_name = url.split("/")[-1].split(".")[0]
68
+
69
+ with row1_col1:
70
+ if file_path.lower().endswith(".kml"):
71
+ fiona.drvsupport.supported_drivers["KML"] = "rw"
72
+ gdf = gpd.read_file(file_path, driver="KML")
73
+ else:
74
+ gdf = gpd.read_file(file_path)
75
+ lon, lat = leafmap.gdf_centroid(gdf)
76
+ if backend == "pydeck":
77
+
78
+ column_names = gdf.columns.values.tolist()
79
+ random_column = None
80
+ with container:
81
+ random_color = st.checkbox("Apply random colors", True)
82
+ if random_color:
83
+ random_column = st.selectbox(
84
+ "Select a column to apply random colors", column_names
85
+ )
86
+
87
+ m = leafmap.Map(center=(lat, lon))
88
+ m.add_gdf(gdf, random_color_column=random_column)
89
+ st.pydeck_chart(m)
90
+
91
+ else:
92
+ m = leafmap.Map(center=(lat, lon), draw_export=True)
93
+ m.add_gdf(gdf, layer_name=layer_name)
94
+ # m.add_vector(file_path, layer_name=layer_name)
95
+ if backend == "folium":
96
+ m.zoom_to_gdf(gdf)
97
+ m.to_streamlit(width=width, height=height)
98
+
99
+ else:
100
+ with row1_col1:
101
+ m = leafmap.Map()
102
+ st.pydeck_chart(m)
103
+
104
+
105
+ app()
postBuild ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # enable nbserverproxy
2
+ jupyter serverextension enable --sys-prefix nbserverproxy
3
+ # streamlit launches at startup
4
+ mv streamlit_call.py ${NB_PYTHON_PREFIX}/lib/python*/site-packages/
5
+ # enable streamlit extension
6
+ jupyter serverextension enable --sys-prefix streamlit_call
requirements.txt ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ --find-links=https://girder.github.io/large_image_wheels GDAL
2
+ # cartopy
3
+ folium==0.13.0
4
+ geemap[extra]
5
+ geopandas
6
+ jupyter-server-proxy
7
+ keplergl
8
+ leafmap
9
+ localtileserver
10
+ matplotlib
11
+ nbserverproxy
12
+ owslib
13
+ palettable
14
+ plotly
15
+ pandas
16
+ seaborn
17
+ streamlit
18
+ streamlit-bokeh-events
19
+ streamlit-folium
20
+ streamlit-keplergl
21
+ tropycal
22
+
23
+
24
+ # git+https://github.com/giswqs/leafmap
25
+ # git+https://github.com/giswqs/geemap
26
+
setup.sh ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # sudo add-apt-repository ppa:ubuntugis/ppa && sudo apt-get update
2
+ # sudo apt-get update
3
+ # sudo apt-get install python3-dev
4
+ # sudo apt-get install gdal-bin
5
+ # sudo apt-get install libgdal-dev
6
+ # export CPLUS_INCLUDE_PATH=/usr/include/gdal
7
+ # export C_INCLUDE_PATH=/usr/include/gdal
8
+ # gdal-config --version
9
+ # pip install GDAL==$(gdal-config --version | awk -F'[.]' '{print $1"."$2}') localtileserver
10
+
11
+ mkdir -p ~/.streamlit/
12
+ echo "\
13
+ [server]\n\
14
+ headless = true\n\
15
+ port = $PORT\n\
16
+ enableCORS = false\n\
17
+ \n\
18
+ " > ~/.streamlit/config.toml
streamlit_app.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import leafmap.foliumap as leafmap
3
+
4
+ st.set_page_config(layout="wide")
5
+
6
+ # Customize page title
7
+ st.title("Streamlit for Geospatial Applications")
8
+
9
+ st.markdown(
10
+ """
11
+ This multipage app template demonstrates various interactive web apps created using [streamlit](https://streamlit.io) and [leafmap](https://leafmap.org). It is an open-source project and you are very welcome to contribute to the [GitHub repository](https://github.com/giswqs/streamlit-multipage-template).
12
+ """
13
+ )
14
+
15
+ st.header("Instructions")
16
+
17
+ markdown = """
18
+ 1. For the [GitHub repository](https://github.com/giswqs/streamlit-multipage-template) or [use it as a template](https://github.com/giswqs/streamlit-multipage-template/generate) for your own project.
19
+ 2. Customize the sidebar by changing the sidebar text and logo in each Python files.
20
+ 3. Find your favorite emoji from https://emojipedia.org.
21
+ 4. Add a new app to the `pages/` directory with an emoji in the file name, e.g., `1_πŸš€_Chart.py`.
22
+
23
+ """
24
+
25
+ st.markdown(markdown)
26
+
27
+ m = leafmap.Map(minimap_control=True)
28
+ m.add_basemap("OpenTopoMap")
29
+ m.to_streamlit(height=500)
streamlit_call.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from subprocess import Popen
2
+
3
+
4
+ def load_jupyter_server_extension(nbapp):
5
+ """serve the streamlit app"""
6
+ Popen(
7
+ [
8
+ "streamlit",
9
+ "run",
10
+ "Home.py",
11
+ "--browser.serverAddress=0.0.0.0",
12
+ "--server.enableCORS=False",
13
+ ]
14
+ )