|
70 | 70 | "from owslib.wms import WebMapService\n", |
71 | 71 | "\n", |
72 | 72 | "gv.extension(\"bokeh\")\n", |
| 73 | + "gv.renderer('bokeh').webgl = False\n", |
73 | 74 | "pn.extension(sizing_mode=\"stretch_width\")" |
74 | 75 | ] |
75 | 76 | }, |
|
129 | 130 | "\n", |
130 | 131 | "Now, you might be wondering, since there already exists an online explorer called [WorldView](https://worldview.earthdata.nasa.gov/), why bother reinventing the wheel? Well, here's the catch: by building your own explorer, you have the freedom to incorporate your own datasets into the mix!\n", |
131 | 132 | "\n", |
132 | | - "Not only does this provide a unique opportunity to personalize your exploration experience, but it's also a fantastic way to explore all the exciting options available while showcasing the incredible power of Python packages working in harmony!" |
| 133 | + "Not only does this provide a unique opportunity to personalize your exploration experience, but it's also a fantastic way to explore all the exciting options available while showcasing the incredible power of Python packages working in harmony!\n", |
| 134 | + "\n", |
| 135 | + "Try it out interactively [here](https://charming-ornate-flying-snake.anacondaapps.cloud/)!" |
133 | 136 | ] |
134 | 137 | }, |
135 | 138 | { |
136 | 139 | "cell_type": "code", |
137 | 140 | "execution_count": null, |
138 | 141 | "metadata": {}, |
139 | | - "outputs": [ |
140 | | - { |
141 | | - "name": "stderr", |
142 | | - "output_type": "stream", |
143 | | - "text": [ |
144 | | - "No such comm: 90be3a54996442ad9c86f9abc1f60f70\n" |
145 | | - ] |
146 | | - } |
147 | | - ], |
| 142 | + "outputs": [], |
148 | 143 | "source": [ |
149 | 144 | "BASE_URL = \"https://gibs.earthdata.nasa.gov/wms/epsg3857/best/wms.cgi?SERVICE=WMS\"\n", |
150 | 145 | "XMIN = -20037507.539400\n", |
|
153 | 148 | "YMAX = 7714669.39460\n", |
154 | 149 | "\n", |
155 | 150 | "\n", |
156 | | - "class NasaEarthDataWmsExplorer:\n", |
| 151 | + "class NasaEarthDataGibsWmsExplorer:\n", |
157 | 152 | " def __init__(self):\n", |
158 | 153 | " self.wms = WebMapService(BASE_URL)\n", |
159 | | - " layers = list(self.wms.contents)\n", |
| 154 | + " layers = sorted(self.wms.contents)\n", |
| 155 | + " self.products_layers = {\"Miscellaneous\": []}\n", |
| 156 | + " for layer in layers:\n", |
| 157 | + " if \"_\" in layer:\n", |
| 158 | + " product, product_layer = layer.split(\"_\", 1)\n", |
| 159 | + " if product not in self.products_layers:\n", |
| 160 | + " self.products_layers[product] = []\n", |
| 161 | + " self.products_layers[product].append(product_layer)\n", |
| 162 | + " else:\n", |
| 163 | + " self.products_layers[\"Miscellaneous\"].append(layer)\n", |
160 | 164 | "\n", |
161 | 165 | " # create widgets\n", |
162 | | - " self.layer_input = pn.widgets.Select(\n", |
| 166 | + " self.product_select = pn.widgets.Select(\n", |
| 167 | + " name=\"Product\",\n", |
| 168 | + " options=sorted(self.products_layers),\n", |
| 169 | + " )\n", |
| 170 | + " self.layer_select = pn.widgets.Select(\n", |
163 | 171 | " name=\"Layer\",\n", |
164 | | - " options=layers,\n", |
| 172 | + " options=sorted(self.products_layers[self.product_select.value]),\n", |
165 | 173 | " )\n", |
166 | 174 | " self.time_slider = pn.widgets.DiscreteSlider(name=\"Time\", margin=(5, 16))\n", |
167 | | - " self.coastline_feature = gv.feature.coastline().opts(\n", |
| 175 | + " self.refresh_button = pn.widgets.Button(name=\"Refresh\", button_type=\"light\")\n", |
| 176 | + " self.image_pane = pn.pane.Image() # for colorbar / legend\n", |
| 177 | + " self.holoviews_pane = pn.pane.HoloViews(min_height=500, sizing_mode=\"stretch_both\")\n", |
| 178 | + " pn.state.onload(self._onload)\n", |
| 179 | + " \n", |
| 180 | + " def _onload(self):\n", |
| 181 | + " # add interactivity\n", |
| 182 | + " self.product_select.param.watch(self.update_layers, \"value\")\n", |
| 183 | + " self.layer_select.param.watch(self.update_time, \"value\")\n", |
| 184 | + " self.refresh_button.on_click(self.update_web_map)\n", |
| 185 | + "\n", |
| 186 | + " # create imagery\n", |
| 187 | + " coastline_feature = gv.feature.coastline().opts(\n", |
168 | 188 | " global_extent=True, responsive=True\n", |
169 | 189 | " )\n", |
170 | | - " self.static_text = pn.widgets.StaticText()\n", |
171 | | - " self.image_pane = pn.pane.Image()\n", |
172 | | - " self.holoviews_pane = pn.pane.HoloViews(\n", |
173 | | - " object=self.coastline_feature, min_height=500, sizing_mode=\"stretch_both\"\n", |
| 190 | + " border_feature = gv.feature.borders()\n", |
| 191 | + " self.dynamic_map = gv.DynamicMap(\n", |
| 192 | + " self.update_web_map, streams=[self.time_slider.param.value_throttled]\n", |
174 | 193 | " )\n", |
| 194 | + " self.holoviews_pane.object = coastline_feature * border_feature * self.dynamic_map\n", |
175 | 195 | "\n", |
176 | | - " # add interactivity\n", |
177 | | - " self.layer_input.param.watch(self.update_time, \"value\")\n", |
178 | | - " self.time_slider.param.watch(self.update_web_map, \"value_throttled\")\n", |
| 196 | + " def get_layer(self, product=None, product_layer=None):\n", |
| 197 | + " product = product or self.product_select.value\n", |
| 198 | + " if product == \"Miscellaneous\":\n", |
| 199 | + " layer = product_layer or self.layer_select.value\n", |
| 200 | + " else:\n", |
| 201 | + " layer = f\"{product}_{product_layer or self.layer_select.value}\"\n", |
| 202 | + " return layer\n", |
| 203 | + "\n", |
| 204 | + " def update_layers(self, event):\n", |
| 205 | + " product = event.new\n", |
| 206 | + " product_layers = self.products_layers[product]\n", |
| 207 | + " self.layer_select.options = sorted(product_layers)\n", |
179 | 208 | "\n", |
180 | 209 | " def update_time(self, event):\n", |
181 | | - " layer = event.new\n", |
| 210 | + " layer = self.get_layer()\n", |
182 | 211 | " time_positions = self.wms.contents[layer].timepositions\n", |
183 | | - " if not time_positions:\n", |
184 | | - " # use N/A instead of None to circumvent Panel from crashing\n", |
185 | | - " # when going from time-dependent layer to time-independent layer\n", |
186 | | - " options = [\"N/A\"]\n", |
187 | | - " value = \"N/A\"\n", |
188 | | - " else:\n", |
| 212 | + " if time_positions:\n", |
189 | 213 | " ini, end, step = time_positions[0].split(\"/\")\n", |
| 214 | + " try:\n", |
| 215 | + " freq = pd.Timedelta(step)\n", |
| 216 | + " except ValueError:\n", |
| 217 | + " freq = step.lstrip(\"P\")\n", |
190 | 218 | " options = (\n", |
191 | | - " pd.date_range(ini, end, freq=pd.Timedelta(step))\n", |
| 219 | + " pd.date_range(ini, end, freq=freq)\n", |
192 | 220 | " .strftime(\"%Y-%m-%dT%H:%M:%SZ\")\n", |
193 | 221 | " .tolist()\n", |
194 | 222 | " )\n", |
195 | | - " value = options[0]\n", |
196 | | - " self.time_slider.param.set_param(options=options, value=value)\n", |
197 | | - " self.update_web_map()\n", |
| 223 | + " if options:\n", |
| 224 | + " value = options[0]\n", |
| 225 | + " self.time_slider.param.set_param(options=options, value=value)\n", |
| 226 | + " return\n", |
| 227 | + " # use N/A instead of None to circumvent Panel from crashing\n", |
| 228 | + " # when going from time-dependent layer to time-independent layer\n", |
| 229 | + " options = [\"N/A\"]\n", |
| 230 | + " self.time_slider.options = options\n", |
| 231 | + " self.time_slider.param.trigger(\"value_throttled\")\n", |
| 232 | + "\n", |
| 233 | + " def get_url_template(self, layer, time=None):\n", |
| 234 | + " get_map_kwargs = dict(\n", |
| 235 | + " layers=[layer],\n", |
| 236 | + " srs=\"EPSG:3857\",\n", |
| 237 | + " bbox=(XMIN, YMIN, XMAX, YMAX),\n", |
| 238 | + " size=(256, 256),\n", |
| 239 | + " format=\"image/png\",\n", |
| 240 | + " transparent=True,\n", |
| 241 | + " time=time\n", |
| 242 | + " )\n", |
| 243 | + " try:\n", |
| 244 | + " url = self.wms.getmap(**get_map_kwargs).geturl()\n", |
| 245 | + " except Exception:\n", |
| 246 | + " get_map_kwargs.pop(\"time\")\n", |
| 247 | + " url = self.wms.getmap(**get_map_kwargs).geturl()\n", |
| 248 | + " url_template = (\n", |
| 249 | + " url.replace(str(XMIN), \"{XMIN}\")\n", |
| 250 | + " .replace(str(YMIN), \"{YMIN}\")\n", |
| 251 | + " .replace(str(XMAX), \"{XMAX}\")\n", |
| 252 | + " .replace(str(YMAX), \"{YMAX}\")\n", |
| 253 | + " )\n", |
| 254 | + " return url_template\n", |
198 | 255 | "\n", |
199 | | - " def update_web_map(self, event=None):\n", |
| 256 | + " def update_web_map(self, value_throttled=None):\n", |
200 | 257 | " try:\n", |
201 | 258 | " self.holoviews_pane.loading = True\n", |
202 | | - " layer = self.layer_input.value\n", |
| 259 | + " layer = self.get_layer()\n", |
203 | 260 | " time = self.time_slider.value\n", |
204 | 261 | " if time == \"N/A\":\n", |
205 | 262 | " time = None\n", |
206 | | - "\n", |
207 | | - " url = wms.getmap(\n", |
208 | | - " layers=[layer],\n", |
209 | | - " srs=\"EPSG:3857\",\n", |
210 | | - " bbox=(XMIN, YMIN, XMAX, YMAX),\n", |
211 | | - " size=(256, 256),\n", |
212 | | - " format=\"image/png\",\n", |
213 | | - " transparent=True,\n", |
214 | | - " time=time,\n", |
215 | | - " ).geturl()\n", |
216 | | - " url_template = (\n", |
217 | | - " url.replace(str(XMIN), \"{XMIN}\")\n", |
218 | | - " .replace(str(YMIN), \"{YMIN}\")\n", |
219 | | - " .replace(str(XMAX), \"{XMAX}\")\n", |
220 | | - " .replace(str(YMAX), \"{YMAX}\")\n", |
221 | | - " )\n", |
222 | | - "\n", |
| 263 | + " url_template = self.get_url_template(layer, time)\n", |
223 | 264 | " layer_meta = self.wms[layer]\n", |
224 | 265 | " self.image_pane.object = layer_meta.styles.get(\"default\", {}).get(\"legend\")\n", |
225 | | - " self.static_text.value = layer_meta.abstract\n", |
226 | 266 | " layer_imagery = gv.WMTS(url_template).opts(title=layer_meta.title)\n", |
227 | | - "\n", |
228 | | - " self.holoviews_pane.object = self.coastline_feature * layer_imagery\n", |
229 | 267 | " finally:\n", |
230 | 268 | " self.holoviews_pane.loading = False\n", |
| 269 | + " return layer_imagery\n", |
231 | 270 | "\n", |
232 | 271 | " def view(self):\n", |
233 | 272 | " widget_box = pn.WidgetBox(\n", |
234 | | - " self.layer_input,\n", |
| 273 | + " self.product_select,\n", |
| 274 | + " self.layer_select,\n", |
235 | 275 | " self.time_slider,\n", |
236 | 276 | " self.image_pane,\n", |
237 | | - " self.static_text,\n", |
| 277 | + " self.refresh_button,\n", |
238 | 278 | " pn.Spacer(sizing_mode=\"stretch_height\"),\n", |
239 | 279 | " sizing_mode=\"stretch_both\",\n", |
240 | 280 | " )\n", |
|
244 | 284 | " )\n", |
245 | 285 | "\n", |
246 | 286 | "\n", |
247 | | - "nasa_earth_data_wms_explorer = NasaEarthDataWmsExplorer()\n", |
248 | | - "nasa_earth_data_wms_explorer.view()" |
| 287 | + "explorer = NasaEarthDataGibsWmsExplorer()\n", |
| 288 | + "explorer.view().servable()" |
249 | 289 | ] |
250 | 290 | }, |
251 | 291 | { |
|
263 | 303 | "The imagery is displayed using the GeoViews library, combined with a coastline feature.\n" |
264 | 304 | ] |
265 | 305 | }, |
| 306 | + { |
| 307 | + "cell_type": "markdown", |
| 308 | + "metadata": {}, |
| 309 | + "source": [ |
| 310 | + "## Side-by-Side Comparisons\n", |
| 311 | + "\n", |
| 312 | + "After some exploration, I discovered that GPW (Gridded Population of the World) product had four snapshots of population density, in 2000, 2005, 2010, 2020.\n", |
| 313 | + "\n", |
| 314 | + "What if we wanted a closer picture of what changed between 2000 and 2020?\n", |
| 315 | + "\n", |
| 316 | + "First, we can define a helper function, using the methods from the `NasaEarthDataGibsWmsExplorer` class." |
| 317 | + ] |
| 318 | + }, |
| 319 | + { |
| 320 | + "cell_type": "code", |
| 321 | + "execution_count": null, |
| 322 | + "metadata": {}, |
| 323 | + "outputs": [], |
| 324 | + "source": [ |
| 325 | + "def get_web_map(product, product_layer):\n", |
| 326 | + " return (\n", |
| 327 | + " gv.WMTS(\n", |
| 328 | + " explorer.get_url_template(explorer.get_layer(product, product_layer))\n", |
| 329 | + " ).opts(responsive=True, height=500, title=product_layer, global_extent=True)\n", |
| 330 | + " )" |
| 331 | + ] |
| 332 | + }, |
| 333 | + { |
| 334 | + "cell_type": "markdown", |
| 335 | + "metadata": {}, |
| 336 | + "source": [ |
| 337 | + "Then, we can layout the Population Density snapshots, side by side.\n", |
| 338 | + "\n", |
| 339 | + "When we zoom in on one, not only does the tiles are updated to show the new resolution, but the others' zoom is also synced, so we can easily compare and contrast specific regions of interest." |
| 340 | + ] |
| 341 | + }, |
| 342 | + { |
| 343 | + "cell_type": "code", |
| 344 | + "execution_count": null, |
| 345 | + "metadata": {}, |
| 346 | + "outputs": [], |
| 347 | + "source": [ |
| 348 | + "pop_density_2000_map = get_web_map(\"GPW\", \"Population_Density_2000\")\n", |
| 349 | + "pop_density_2020_map = get_web_map(\"GPW\", \"Population_Density_2020\")\n", |
| 350 | + "\n", |
| 351 | + "pop_density_2000_map + pop_density_2020_map" |
| 352 | + ] |
| 353 | + }, |
| 354 | + { |
| 355 | + "cell_type": "markdown", |
| 356 | + "metadata": {}, |
| 357 | + "source": [ |
| 358 | + "Upon zooming into specific regions, I realized that it'd be helpful to add borders, coastlines, and labels, so let's update function." |
| 359 | + ] |
| 360 | + }, |
| 361 | + { |
| 362 | + "cell_type": "code", |
| 363 | + "execution_count": null, |
| 364 | + "metadata": {}, |
| 365 | + "outputs": [], |
| 366 | + "source": [ |
| 367 | + "def get_web_map(product, product_layer):\n", |
| 368 | + " return (\n", |
| 369 | + " gv.WMTS(\n", |
| 370 | + " explorer.get_url_template(explorer.get_layer(product, product_layer))\n", |
| 371 | + " ).opts(responsive=True, height=500, title=product_layer, global_extent=True) *\n", |
| 372 | + " gv.feature.coastline() * gv.feature.borders() * gv.tile_sources.StamenLabels()\n", |
| 373 | + " )\n", |
| 374 | + " \n", |
| 375 | + "pop_density_2000_map = get_web_map(\"GPW\", \"Population_Density_2000\")\n", |
| 376 | + "pop_density_2020_map = get_web_map(\"GPW\", \"Population_Density_2020\")\n", |
| 377 | + "\n", |
| 378 | + "pop_density_2000_map + pop_density_2020_map" |
| 379 | + ] |
| 380 | + }, |
| 381 | + { |
| 382 | + "cell_type": "markdown", |
| 383 | + "metadata": {}, |
| 384 | + "source": [ |
| 385 | + "One interesting thing I noticed was that in Egypt, there was a line of high population density. It'd would be interesting to see if it's because of a water source." |
| 386 | + ] |
| 387 | + }, |
| 388 | + { |
| 389 | + "cell_type": "code", |
| 390 | + "execution_count": null, |
| 391 | + "metadata": {}, |
| 392 | + "outputs": [], |
| 393 | + "source": [ |
| 394 | + "pop_density_2000_map = get_web_map(\"GPW\", \"Population_Density_2000\")\n", |
| 395 | + "water_bodies = get_web_map(\"Miscellaneous\", \"Water Bodies\")\n", |
| 396 | + "xlim = (2735065.540470079, 3886016.688009746)\n", |
| 397 | + "ylim = (2442736.280432458, 3639157.2571363684)\n", |
| 398 | + "\n", |
| 399 | + "pop_density_2000_map.opts(global_extent=False, xlim=xlim, ylim=ylim) + water_bodies" |
| 400 | + ] |
| 401 | + }, |
| 402 | + { |
| 403 | + "cell_type": "markdown", |
| 404 | + "metadata": {}, |
| 405 | + "source": [ |
| 406 | + "Despite the limited visibility of the water body, it appears that areas with high population density in Egypt are associated with the presence of a river." |
| 407 | + ] |
| 408 | + }, |
266 | 409 | { |
267 | 410 | "attachments": {}, |
268 | 411 | "cell_type": "markdown", |
269 | 412 | "metadata": {}, |
270 | 413 | "source": [ |
271 | 414 | "## Summary\n", |
272 | 415 | "\n", |
273 | | - "While the standalone nature of this custom-built explorer may not rival the capabilities of the existing WorldView explorer, its true potential lies in the ability to incorporate your own data. By integrating your unique datasets into this explorer, you can unlock a world of fascinating possibilities and create a truly captivating exploration experience.\n", |
| 416 | + "While the standalone capabilities of this custom-built explorer may not rival those of the current WorldView explorer, its true power lies in its ability to incorporate personal data, combine various layers for analysis, and effectively communicate a narrative.\n", |
| 417 | + "\n", |
| 418 | + "What sets this explorer apart and makes it truly captivating and compelling is the seamless integration of personal data.\n", |
| 419 | + "\n", |
| 420 | + "Here are a few ideas to try:\n", |
| 421 | + "- Implementing a search bar feature to easily navigate through the available layers.\n", |
| 422 | + "- Overlaying satellite fire detection layers with other data sets, such as air quality measurements, to gain deeper insights.\n", |
| 423 | + "- Examining the correlation between night lights and population density to uncover interesting patterns and trends.\n", |
| 424 | + "- Tracking changes in land types over the years to observe the evolving landscape.\n", |
| 425 | + "- By incorporating these ideas, the explorer can offer a more comprehensive and dynamic user experience.\n", |
| 426 | + "- Visualizing climate data: Integrate climate data layers such as temperature, precipitation, or wind patterns to understand the relationship between climate and various geographical features.\n", |
| 427 | + "- Analyzing vegetation indices: Incorporate vegetation indices like NDVI (Normalized Difference Vegetation Index) to assess vegetation health and identify areas with dense vegetation or potential vegetation changes.\n", |
| 428 | + "- Mapping infrastructure and urban development: Overlay infrastructure data, such as roads, buildings, and urban areas, to analyze the impact of urbanization on the surrounding environment and land use patterns.\n", |
| 429 | + "- Exploring natural disasters: Incorporate real-time or historical data on natural disasters such as hurricanes, earthquakes, or floods, to study their impact on the affected regions and aid in disaster management and response efforts.\n", |
| 430 | + "- Monitoring water resources: Utilize data on water bodies, water availability, and water quality to assess water resources, identify areas of concern, and track changes over time.\n", |
| 431 | + "- Investigating demographic patterns: Overlay demographic data, such as population density, age groups, or socioeconomic indicators, to study demographic patterns and their spatial relationships with other layers.\n", |
| 432 | + "- Tracking wildlife habitats: Integrate data on wildlife habitats, migration patterns, or conservation areas to gain insights into ecological dynamics and support biodiversity conservation efforts.\n", |
| 433 | + "\n", |
| 434 | + "Furthermore, it's important to note that the functionality of this explorer is not restricted to geographic maps alone. It has the flexibility to incorporate a combination of charts and maps, offering a more diverse and comprehensive data visualization experience.\n", |
274 | 435 | "\n", |
275 | | - "It is this integration of personal data that sets this explorer apart and makes it incredibly compelling and engaging!" |
| 436 | + "We'd love to see your work showcased on [HoloViz Discourse](https://discourse.holoviz.org/c/showcase/)!" |
276 | 437 | ] |
277 | 438 | } |
278 | 439 | ], |
|
0 commit comments