Chapter 4. Geospatial Analytics in the Cloud: Google Earth Engine and Other Tools
How do you access geospatial data? Although data professionals with enterprise accounts may not think about the limitations of personal computing and relying on open source data, the rest of us often work within limits. Geospatial analysis in the cloud has narrowed the divide, since that means we no longer need to store large volumes of data locally. Never before has the general public had open source access to geospatial data on such a global scale. This chapter will show you where to find data for exploration and learning.
Space programs in the US and around the world have gathered data from satellites and sensors for decades, but only recently have we had the capacity to manipulate that data in real time for analysis. The USGS hosts EarthExplorer (Landsat), and the Copernicus Open Access Hub provides data from European Space Agency (ESA) Sentinel satellites. Landsat high-resolution satellite images enable us to evaluate and measure environmental change, understand the impact of climate science and agricultural practices, and respond to natural disasters across time and space, to name a few examples. The advent of free satellite images has enabled decision makers from economically challenged areas across the world to bring insights into view and focus on solutions.
Spatial analysis includes methods and tools applied to location data, in which the results vary based on the location or frame analyzing the object. It is essentially âlocation-specificâ analysis. This can be as simple as locating the nearest subway station or asking how many green spaces or parks are in a community, or as complex as revealing patterns in transportation accessibility or health outcomes. Spatial algorithms are a method of solving a problem by listing and executing sequential instructions integrated with geographic properties, used for analysis, modeling, and prediction.
GIS solve spatial problems that rely on location information like latitude, longitude, and projection. Spatial information answers âwhereâ questions: where on the Earthâs surface did something occur?
Imagine, for example, stepping out of your hotel on 41st and Madison Avenue in Manhattan. You search in your mapping app for where you might purchase a coat, since the weather is dramatically colder than you anticipated. Instantly, the locations of apparel stores populate your screen.
Or on the marketing side, say you work for an outdoor provision company, producing top-of-the-line outerwear for the discerning customer. You could use geospatial information to answer questions like: Where do your potential customers live, visit, or travel? Would a potential retail location nearby be a profitable marketing decision? How far would potential customers travel? What is the mean income within each of the locations you are considering? These where components exist in retail and commercial environments, the military, climate science, and health care, to name a few examples.
Attributes are another important component of spatially referenced data. Spatial attributes are bounded in space; these could include a community boundary or infrastructure, such as a roadway or metro station, usually represented by a polygon. Spatially referenced data can also have nonspatial attributes, such as the income of residents in a certain location, and can provide context for the location intelligence.
The I in GIS is increasingly being stored in the cloud. Today your laptop can access petabytes of information made available by geospatial analytics processing services in the cloud. This chapter will explore one of those services: Google Earth Engine (GEE).
In 2007, Jim Gray, a computer scientist at Microsoft until he was lost at sea later that year, was quite prescient when he said: âFor data analysis, one possibility is to move the data to you, but the other possibility is to move your query to the data. You can either move your questions or the data. Often it turns out to be more efficient to move the questions than to move the data.â Thatâs the basic principle behind doing geospatial analytics in the cloud.
In this chapter, youâll use GEE to perform a variety of tasks associated with geographic properties in spatial environments. Weâll also take a quick look at another tool that integrates with Python: Leafmap. By the end of the chapter, youâll have enough familiarity with these interfaces to follow along with later chapters and eventually launch your own independent project.
Google Earth Engine Setup
But first, youâll need to create your work environments. The Jupyter Notebooks for this chapter are available on GitHub. You can open them up and follow along or experiment with the code and explore separately when time permits. The instructions for installing the necessary packages and resources will be covered as well.
The GEE archive contains more than 60 petabytes of satellite imagery and remote sensing and geospatial dataâall freely available, preprocessed, and easy to access. Imagine trying to download all that to your laptop! GEEâs algorithms allow the public to create interactive applications or data products in the cloud. You just need to apply for a free Google Earth Engine account (which comes with 250 gigabytes of storage) and authenticate within either the terminal or notebook when you are granted access. Follow these steps:
To authorize access needed by Earth Engine, open the following URL in a web browser and follow the instructions: https://accounts.google.com/o/oauth2/auth?client_id=xxx The authorization workflow will generate a code, which you should paste in the box below Enter verification code: code will be here Successfully saved authorization token.
GEE will send you a unique link and verification code. Paste the code into the box and hit Enter.
Using the GEE Console and geemap
The GEE console is a quick resource for locating images and running the code. But Python isnât GEEâs native language: the GEE code editor is designed for writing and executing scripts in JavaScript. Its Javascript API has a robust IDE, extensive documentation, and interactive visualization functionality, and none of that is natively available for Python. To access the full spectrum of interactivity in a Python environment, you will need to use geemap, a Python package for interacting with GEE created by Dr. Qiusheng Wu.
Fortunately, you can use the extensive GEE catalog to locate and visualize data with a single click, with limited or no JavaScript expertise. You can find your way around the interface and generate maps simply by scrolling through the Scripts tab. Each code script allows you to run JavaScript code and generate maps. But if youâre seeking autonomy to build your own maps and engage interactively, youâll want to use geemap. The GEE catalog (pictured in Figure 4-1) contains useful information you will need when deciding how to interact with data in geemap.
Look through the Earth Engine Data Catalog, find a dataset collection, and scroll down the page. At the bottom, you will notice that the JavaScript code is provided. Simply copy and paste it into the console, as shown in Figure 4-2.
Figure 4-2 shows what is generated when you paste the code into the console and select Run from the list of options in the center panel. For example, the data from Figure 4-1 generates USGS Landsat 8 Level 2, Collection 2, Tier 1, identified as ee.ImageCollection("LANDSAT/LC08/C02/T1_L2")
.
Letâs learn how to generate GEE images using Python scripts in a Jupyter Notebook. geemap even has a tool that will convert JavaScript code to Python right in your Jupyter Notebook.
Jupyter Notebook is a separate entity from your Python environments. It was originally named for its ability to to interact with three different coding languages, Julia, Python, and R, but it has expanded well beyond its original capabilities. You have to tell the system which version of Python you want. The kernel is how the Notebook and Python communicate.
Installing geemap will create a console in a Notebook environment similar to what you see in the GEE console but with the Python API instead of JavaScript. Once you set up a Conda environment, you will be able to interact with GEE within a Jupyter Notebook. First, you will need to download the required packages and libraries.
Creating a Conda Environment
Anaconda is a popular platform-agnostic distribution manager for Python and other programming languages that installs and manages Conda packages. You could think of Anaconda as storage for all of your data science tools. Conda manages your packages and tools, allowing you to upload new tools as needed and to customize your work environment.
Conda packages are stored in the Anaconda repository or the cloud, so you donât need additional tools for installation. Conda allows you to make as many environments as you need with your preferred version of Python. You also have the option of downloading a leaner version of Anaconda called Miniconda, which I prefer, regardless of your operating system. Both are straightforward installations. I recommend the Miniconda installation instructions in this tutorial by Ted Petrou.
Opening the Jupyter Notebook
Jupyter Notebooks are open source, interactive, web-based tools. They run in your browser and donât require any additional downloads. You can access the Jupyter Notebook for this chapter on GitHub: the filename is 4 Geospatial Analytics in the Cloud. You can find and configure the installed nbextensions in the file menu of your Notebook. These are handy plug-ins that add more functionality to your Jupyter Notebook environment.
Installing geemap and Other Packages
Once youâve installed your Conda environment, you can open your terminal or command prompt to install geemap. Execute the following code line by line to activate your work environment. Here, Iâve named my geospatial environment gee
:
conda create -n gee python=3.9
conda activate gee
conda install geemap -c conda-forge
conda install cartopy -c conda-forge
conda install jupyter_contrib_nbextensions -c conda-forge
jupyter contrib nbextension install --user
Notice that I specified the version of Python to include in the environment. Iâve done this because there are still some dependencies that arenât ready for the latest version of Python. That is one important reason why environments are useful: you will receive a warning if there are compatibility conflicts, and you can create an environment using the version that will avoid those conflicts.Â
This install also includes Cartopy, a Python package for geospatial data processing; jupyter_contrib_nbextensions
, a package for expanded functionality; and contrib_nbextensions
, which will add styles to the Jupyter configuration.
Now that youâve installed the packages into your environment, whenever you open a new session, you will only need to run import geemap
in a code cell. The environment is now visible when you activate, shown here as (gee)
:
(gee) MacBook-Pro-8:~ bonnymcclain$ conda list
This environment will contain all of the associated packages as well as their dependencies. You can create different environments (abbreviated as env
) that include the dependencies and packages unique to each project.
The conda list
command will show you which packages are installed in the active environment. This following is a snippet of what loads for me when I execute the command:
# packages in environment at /Users/bonnymcclain/opt/miniconda3/envs/geo: # # Name Version Build Channel aiohttp 3.7.4 py38h96a0964_0 conda-forge anyio 3.1.0 py38h50d1736_0 conda-forge appnope 0.1.2 py38h50d1736_1 conda-forge argon2-cffi 20.1.0 py38h5406a74_2 conda-forge async-timeout 3.0.1 py_1000 conda-forge async_generator 1.10 py_0 conda-forge attrs 21.2.0 pyhd8ed1ab_0 conda-forge backcall 0.2.0 pyh9f0ad1d_0 conda-forge backports 1.0 py_2 conda-forge backports.functools_lru_cache 1.6.4 pyhd8ed1ab_0 conda-forge beautifulsoup4 4.9.3 pyhb0f4dca_0 conda-forge bleach 3.3.0 pyh44b312d_0 conda-forge bqplot 0.12.27 pyhd8ed1ab_0 conda-forge branca 0.4.2 pyhd8ed1ab_0 conda-forge brotlipy 0.7.0 py38h5406a74_1001 conda-forge bzip2 1.0.8 h0d85af4_4 conda-forge c-ares 1.17.1 h0d85af4_1 conda-forge ca-certificates 2020.12.5 h033912b_0 conda-forge cachetools 4.2.2 pyhd8ed1ab_0 conda-forge cartopy 0.19.0.post1 py38h4be4431_0 conda-forge certifi 2020.12.5 py38h50d1736_1 conda-forge cffi 1.14.5 py38ha97d567_0 conda-forge chardet 4.0.0 py38h50d1736_1 conda-forge click 8.0.1 py38h50d1736_0 conda-forge colour 0.1.5 py_0 conda-forge geemap 0.8.16 pyhd8ed1ab_0 conda-forge ...
This is helpful in case your code throws an error due to a missing dependency. Run conda list
; you should see the versions listed as well. Running conda env list
will display any environments you already have installed.
I install a kernel (a part of the operating system running in your environment) for each environment that I activate:
conda install ipykernel
Now you can add the kernel to your environmentâin this case, <your environment name>
is gee
:
python -m ipykernel install --user --name myenv --display-name
"<your environment name>"
Your local computer has to access files. The import
statement will add the package as a Python object (that is, a collection of data and methods) into the currently running instance of the program.
Open your Terminal and write jupyter notebook
into the console. A Notebook should open in your browser. You will need to import the required libraries into the Notebook. You can see them listed in the code shell. Recall that os
allows you to access the operating system where you are running Python, ee
is the Earth Engine library, and geemap
allows you to interface via Python. Youâll import these libraries using the import
function:
import os import ee import geemap #geemap.update_package()
The central component of a computer operating system is the kernel. The kernel is specific to each programming language, and the default kernel depends on what version of Python you are running in your Notebook.
You will need to restart the kernel for the update to take effect. Select Kernel from the menu and scroll to the option to rerun. You are now ready to begin working in the Notebook.
Note the hash symbol (#) in the last line of the previous code block. In Python code, the hash symbol denotes a comment, or a line of the code that wonât run. When you want to run that line of code, delete the hash symbol. To make sure you are using an updated geemap package, uncomment that last line (that is, remove the # in the last row) before running the code. Once you update geemap, you can once again insert the hash, since you wonât need to update the package every time you run the code. You may also add commented text to include any clarifying details. You will see this practice in many of the code blocks in this book.
Exploring the Landsat 9 Image Collection
We have been working with Landsat data, so letâs look at the Landsat 9 data, which was first released in early 2022 and still rolling out as of this writing. To see how much of the dataset is available, run the following code:
collection = ee.ImageCollection('LANDSAT/LC09/C02/T1_L2') print(collection.size().getInfo())
This outputs: 106919
.
The collection includes 106,919 imagesâand is still increasing!
For comparison, the Landsat/LC08/C02/T1_L2 collection contains 1,351,632 images. By the time this book is published, the number of Landsat 9 images will be vastly larger. You can calculate the median value of all matching bands to reduce the size of the image collection:
median = collection.median()
Working with Spectral Bands
As you learned in Chapter 1, spectral bands are like bins of different types of light. Reflected light is captured as bands of light energy in a range of different wavelengths or colors. Think of the electromagnetic spectrum. Each section of the spectrum is actually a band. The information about bands in this section is intended to highlight where to locate the data to enter into the code to access the correct information. The bands collected by Landsat 8 apply to Landsat 9. You will need this data to apply scaling factors, or comparisons of linear distances that adjust for distortion of areas and angles based on the map projection (also covered in Chapter 1). Remember, the Earth is shaped as an ellipsoid, not a perfect sphere! We derive scaling factors from the Scale and the Offset, as shown in Figure 4-6.
The USGS provides guidance on which spectral bands are best for different types of research. You can learn more about the science and explore common Landsat band combinations.
Youâll import ee.ImageCollection into your Jupyter Notebook and add it as a data layer to your map. Youâll then create a composite image from all of the images. This will yield the median value of the spectral bands.
In Python, we define functions by the keyword def
. In the following code, the function name is apply_scale_factors
, followed by the parameter (image)
:
def apply_scale_factors(image): opticalBands = image.select('SR_B.*').multiply(0.0000275).add(-0.2) thermalBands = image.select('ST_B.*').multiply(0.00341802).add(149.0) return image.addBands(opticalBands, None, True).addBands(thermalBands, None, True)
The asterisk (*) tells the function that you want to select multiple bands that meet the defined search requirements. Landsatâs sensors are the Operational Land Imager (OLI) and Thermal Infrared Sensor (TIRS). The OLI produces spectral bands 1 through 9, and TIRS consists of two thermal bands: SR_B and ST_B.
The colon (:) signals where the function body begins. Inside the function body, which is indented, the return
statement determines the value to be returned. After the function definition is complete, calling the function with an argument returns a value:
dataset = apply_scale_factors(median)
To understand why you would want to pick and choose certain bands, think of them as having a spectral signature. Natural color bands use SR_B4 for red, SR_B3 for green, and SR_B2 for blue. Green indicates healthy vegetation, brown is less healthy, whitish gray typically indicates urban features, and water will appear dark blue or black.
The near-infrared (NIR) composite uses NIR (SR_B5), red (SR_B4), and green (SR_B3). Areas in red have better vegetation health, dark areas are water, and urban areas are white. So include these as your visualization parameters:
vis_natural = { 'bands': ['SR_B4', 'SR_B3', 'SR_B2'], 'min': 0.0, 'max': 0.3, } vis_nir = { 'bands': ['SR_B5', 'SR_B4', 'SR_B3'], 'min': 0.0, 'max': 0.3, }
Now add them as data layers to your map:
Map.addLayer(dataset, vis_natural, 'True color (432)') Map.addLayer(dataset, vis_nir, 'Color infrared (543)') Map
In Figure 4-7, I toggled the infrared layer off so you can see the other bands more clearly. There appears to be cloud cover as well. Landsat 9 resamples every 16 days, so it will look different when you view it.
If you hover your cursor over the toolbar icon, you will see the Layers menu appear. You can change the opacity of any maps and deselect any layers you donât want to view in the Layers menu. You can also click the gear icon to explore attributes. You can also specify the minimum and maximum values to display. Stretching the data spreads the pixel values, and you can experiment with different values. Your data will show the range of the bands, and you can decide which values you want to display:
vis_params = [ {'bands': ['SR_B4', 'SR_B3', 'SR_B2'], 'min': 0, 'max': 0.3}, {'bands': ['SR_B5', 'SR_B4', 'SR_B3'], 'min': 0, 'max': 0.3}, {'bands': ['SR_B7', 'SR_B6', 'SR_B4'], 'min': 0, 'max': 0.3}, {'bands': ['SR_B6', 'SR_B5', 'SR_B2'], 'min': 0, 'max': 0.3}, ]
To add labels for these layers, create a list of labels:
labels = [ 'Natural Color (4, 3, 2)', 'Color Infrared (5, 4, 3)', 'Short-Wave Infrared (7, 6 4)', 'Agriculture (6, 5, 2)', ]
Then assign a label to each layer:
geemap.linked_maps( rows=2, cols=2, height="400px", center=[-3.4653, -62.2159], zoom=4, ee_objects=[dataset], vis_params=vis_params, labels=labels, label_position="topright", )
Examining two more parameters in Figure 4-8, you can also see shortwave infrared. Here, darker green indicates dense vegetation, urban areas are shown in blue, healthy vegetation is green, and bare earth is magenta.
Letâs apply your introduction to GEE and geemap to begin exploring.
The National Land Cover Database Basemap
The National Land Cover Database (NLCD) tracks land cover in the US. It is freely available in the Earth Engine Data Catalog and is updated every five years. Land cover data includes spatial reference and land surface characteristics, such as tree canopy cover (which we explored in the last chapter), impervious surfaces, and additional patterns of biodiversity and climate changes. Impervious land cover means nonnatural surfaces, such as asphalt, concrete, or other manmade layers, that limit the natural penetration of rainwater into soil. This information can help predict which areas may be more prone to flooding during heavy rains.
In this section, youâll use NLCD data to perform a Landsat-based examination of the imperviousness data layer (for urban classes) and of decision-tree classification (for the rest). We wonât be doing a full activity here, just a quick orientation, but I encourage you to explore more.
Accessing the Data
Navigate to the Earth Engine Data Catalog and scroll to NLCD_Releases/2019_REL/NLCD or the National Land Cover Database, as shown in Figure 4-9. Earlier we noted that you can simply add this data to the map, but there are a few more options here that I want to show you. Copy the JavaScript code and place it on your clipboard.
The NLCD catalog provides a wealth of information, including date ranges for collection, the data source, an image snippet, a data description, information about the multispectral bands, and image properties.
In geemap, generate a default map of the world:
map = geemap.Map() map
Next, select the convert JavaScript icon. The box shown in Figure 4-9 will pop up. Paste the JavaScript code from the catalog into the box. Follow the instructions in the code comments that populate in the pop-up shown in Figure 4-10:
// Import the NLCD collection. var dataset = ee.ImageCollection('USGS/NLCD_RELEASES/2019_REL/NLCD'); // The collection contains images for multiple years and regions in the USA. print('Products:', dataset.aggregate_array('system:index')); // Filter the collection to the 2016 product. var nlcd2016 = dataset.filter(ee.Filter.eq('system:index', '2016')).first(); // Each product has multiple bands for describing aspects of land cover. print('Bands:', nlcd2016.bandNames()); // Select the land cover band. var landcover = nlcd2016.select('landcover'); // Display land cover on the map. Map.setCenter(-95, 38, 5); Map.addLayer(landcover, null, 'Landcover');
Once you hit Convert, you will see the code update from JavaScript to Python, as shown in Figure 4-10.
If the code does not update into a new cell in your Jupyter Notebook, you can cut and paste it into a new cell and run the cell. The image will now appear as your map.
Now letâs include the default NLCD legend. Select the landcover layer. To discover which legends are available as defaults, run the builtin_legend
function:
legends = geemap.builtin_legends for legend in legends: print(legend)
The NLCDâs legend will be listed as an option. Select it to add it to your map.
Building a Custom Legend
While the NLCD offers a built-in legend option, many datasets do notâand even when they do, these legends donât always offer exactly what you need. Thus, itâs helpful to be able to create your own map legend. Letâs look at how to do that now.
The classes in a dataset usually correspond to the categories youâd want in a legend. Fortunately, you can convert a class table to a legend.
If your data is from the GEE data catalog, you can find a class table there. Then use the following code (or find this code cell in your Jupyter Notebook) and copy the text from the class table into it:
map = geemap.Map() legend_dict = { '11 Open Water': '466b9f', '12 Perennial Ice/Snow': 'd1def8', '21 Developed, Open Space': 'dec5c5', '22 Developed, Low Intensity': 'd99282', '23 Developed, Medium Intensity': 'eb0000', '24 Developed High Intensity': 'ab0000', '31 Barren Land (Rock/Sand/Clay)': 'b3ac9f', '41 Deciduous Forest': '68ab5f', '42 Evergreen Forest': '1c5f2c', '43 Mixed Forest': 'b5c58f', '51 Dwarf Scrub': 'af963c', '52 Shrub/Scrub': 'ccb879', '71 Grassland/Herbaceous': 'dfdfc2', '72 Sedge/Herbaceous': 'd1d182', '73 Lichens': 'a3cc51', '74 Moss': '82ba9e', '81 Pasture/Hay': 'dcd939', '82 Cultivated Crops': 'ab6c28', '90 Woody Wetlands': 'b8d9eb', '95 Emergent Herbaceous Wetlands': '6c9fb8' } landcover = ee.Image('USGS/NLCD/NLCD2019').select('landcover') Map.addLayer(landcover, {}, 'NLCD Land Cover') Map.add_legend(legend_title="NLCD Land Cover Classification", legend_dict=legend_dict) Map
You can find more info on building and customizing legends manually in the geemap documentation.
Now you can explore your map and dig deeper into your areas of interest. What questions do you want to ask of this map? Take some time to explore. There are many different ways to customize your maps with a broad selection of tools!
The GEE catalog is extensive. As you explore different databases and datasets using the skills youâve learned here, you will be able to work with raster and vector data as well as upload your own data sources. A list of handy additional functions in geemap is available on the geemap GitHub page. However, Iâd also like to introduce you to an alternative to GEE.
Leafmap: An Alternative to Google Earth Engine
Visualizing geospatial data outside of GEE does not have to be limiting! If you donât have access to a GEE account or arenât interested in working with GEE, consider using Leafmap. Leafmap is a Python package that lets you visualize interactive geospatial data in your Jupyter Notebook environment. It is based on the geemap package you have already experienced, but as you will see in this section, Leafmap provides access to geospatial data outside the GEE platform. Its GUI reduces the amount of coding you need to do. It has a variety of open source packages at its core.
Leafmap works with many different plotting backends, including ipyleaflet. (A backend, in this context, is internal code that runs on a server and receives client requests.) Users donât see backends, but they are always operating nonetheless.
You can access the Jupyter Notebook Leafmap with the GitHub link. Follow the Leafmap documentation for specific installation instructions depending on your version of Python. (If you arenât sure what version you have, enter python
in the terminal, and it will output the number of the version you have installed. This is important to remember in case you run into issues with your installation of packages.)
You can set up a new environment to work with Leafmap. I originally created the Conda environment shown in the following code with Python 3.8, but it is likely to work with later versions. I named this environment geo
because it is running in a different version of Python:
conda create -n geo python=3.8
conda activate geo
conda install geopandas
conda install leafmap -c conda-forge
conda install mamba -c conda-forge
mamba install leafmap xarray_leaflet -c conda-forge
conda install jupyter_contrib_nbextensions -c conda-forge
pip install keplergl
As before, to open the Notebook, type jupyter notebook
and hit Enter. Now enter the following code into the Notebook to reveal something similar to Figure 4-11:
from ipyleaflet import * m = Map(center=[48.8566, 2.3522], zoom=10, height=600, widescreen=False, basemaps=basemaps.Stamen.Terrain) m
Changing the basemap is as easy as placing your cursor inside the basemap parentheses and selecting Tab on the keyboard. Figure 4-12 shows the options that become available. Esri is the selected basemap here, but you can scroll up and down until you find a suitable one. Be sure to explore. Once you type Esri
, options will populate.
Another useful tool is the ability to preset your zoom levels. When you run the cell in your Notebook, you will have the option of sliding between different zoom levels:
m.interact(zoom=(5,10,1))
Figure 4-13 shows the output.
You can also provide a reference by inserting a minimap into your larger map, as shown in Figure 4-14. To do so, enter the following code:
minimap = Map( zoom_control=False, attribution_control=False, zoom=5, center=m.center, basemap=basemaps.Stamen.Terrain ) minimap.layout.width = '200px' minimap.layout.height = '200px' link((minimap, 'center'), (m, 'center')) minimap_control = WidgetControl(widget=minimap, position='bottomleft') m.add_control(minimap_control)
The minimap shown in Figure 4-14 will appear, helping users stay oriented in a larger context.
Summary
This chapter explored Google Earth Engine and some related tools, libraries, and packages that you can use to answer geospatial questions, and it introduced you to an alternative tool, Leafmap. This chapter and its associated Notebooks will be a handy reference for the projects youâll do in the next chapter. You have rendered visualizations and created maps on the canvas. Next, you will begin analyzing these relationships and exploring tools to do some advanced analysis of your geospatial data.
Get Python for Geospatial Data Analysis now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.