Draw a Box in Google Earth
In this tutorial, we'll deep dive into some interesting uses of the new drawing tools API.
- We'll learn how to dynamically generate a grid and save it as a customer-side geometry.
- We'll also build a simple classification tool that classifies based on user-fatigued points.
Note: if you are re-create and pasting the code snippets beneath into the Lawmaking Editor, they may non work without cartoon a geometry commencement.
Let'due south go started!
Background
Earlier the Drawing Tools API, geometries had to be rendered using Map.addLayer(geometry)
, which means they had to round trip to the server whenever the user panned or zoomed on the map. With the new drawing tools API, nosotros tin now render those geometries directly on the map as if we had drawn them ourselves!
Documentation on the drawing tools API is found here: https://developers.google.com/earth-engine/ui_widgets#ui.map.drawingtools.
The drawing tools API adds two new components to the ui.Map
API:
-
ui.Map.DrawingTools
, and -
ui.Map.GeometryLayer
.
Together, these manage the geometry editing operations as well as any of the geometry layers on the drawing tools. The cartoon tools provide a rich fix of functionality that permit you to listen for when the user draws, edits, or deletes a geometry, as well as adding, removing, or selecting geometry layers.
To retrieve the drawing tools for a map, simply call the drawingTools()
method on the map:
var drawingTools = Map.drawingTools();
Unlike the default Map
, newly created ui.Maps
don't get drawing tools past default, but we tin add drawing tools to newly created maps by simply calling the drawingTools()
method to recollect them:
var map = ui.Map(); // Prints truthful since drawingTools() adds drawing tools to the map. print(map.drawingTools().getShown()); // Supercede the default Map with the newly created map. ui.root.widgets().reset([map]);
Note: you can just take one drawing tools on a map at a time.
Use the layers()
method on the drawing tools to retrieve an ActiveList
of the geometry layers. A geometry layer contains a list of geometries that y'all tin obtain by calling the geometries()
method on the layer. In that location are two means to add new geometry layers to the drawing tools:
var drawingTools = Map.drawingTools(); // Use the addLayer method on the drawing tools directly. var geometries = [ee.Geometry.Point([0, 0]), ee.Geometry.Rectangle([[0, 0], [one, 1]])]; drawingTools.addLayer(geometries, 'my_geometry1', 'carmine'); // Create a new GeometryLayer and add it to the layers list. var layer = ui.Map.GeometryLayer(geometries, 'my_geometry2', 'blue'); drawingTools.layers().add(layer); // Print the list of geometries. print(layer.geometries()); // Add a geometry to the layer. layer.geometries().add(ee.Geometry.Point(1, 1));
There are some nice configuration options for the drawing tools:
-
setShown(
boolean
)
changes the visibility of the drawing tools. -
setDrawModes(['betoken', 'line', 'polygon', 'rectangle'])
changes the available draw modes to the user. -
setShape('bespeak')
sets the current draw shape and starts describe mode. -
setLinked(
boolean
)
configures whether geometries are linked to the imports.
Let's have a deeper look at setLinked
, since it's surprisingly useful!
- When linked manner is turned on, the geometries in the drawing tools are linked to the imports console.
- When linked mode is turned off, the geometries in the imports panel will be removed from the drawing tools (they won't be deleted), and newly created geometry layers will not exist added to the imports.
The default Map
has linked mode turned on past default whereas a newly created ui.Map
has linked mode turned off by default.
Caution: If yous delete a geometry layer while linked mode is on (like calling layers().reset()
), they will be deleted from the imports!
Example: Linked Maps
Suppose you wanted to make a split map where geometries are reflected on both maps. All y'all have to do is turn on linked mode for both maps' drawing tools and whatsoever geometries that are fatigued or edited on one map will be reflected on the other!
Here is a demo script:
var map1 = ui.Map(); map1.drawingTools().setLinked(true); var map2 = ui.Map(); map2.drawingTools().setLinked(true); ui.Map.Linker([map1, map2]); ui.root.widgets().reset([ui.SplitPanel({firstPanel: map1, secondPanel: map2})]); // Now endeavor cartoon a geometry on both maps!
Figure 1. A split map with geometries reflected on both sides.
Neat, correct? Let'south do something even more advanced!
Client Side Geometries
Next up, we'll learn how to dynamically generate a grid in Earth Engine and add together information technology equally a client-side geometry so that it won't be rerendered every fourth dimension the map viewport changes.
The cartoon tools let you take a server-side geometry and render information technology on the customer. This requires us to use evaluate
on the server-side geometries to fetch them from the server and make them bachelor to be drawn straight on the map.
Also new, your imported geometries now show up in Apps so you simply take to run your expensive geometry rendering code once.
For example, suppose that y'all rendered a complex geometry in Earth Engine, you can add it to the map as a client side geometry (with linked manner on). When you publish the app, the prerendered and imported geometry will already be there; no further coding required! Hither'south an example of that in action:
Instance: Build a Client-Side Grid
Here'southward a link to the full script. Press run to see the generated grid!
Annotation: this example doesn't piece of work with all projections and doesn't work well with large geometries, so go on that in heed.
Beginning, nosotros'll demand to write a role that generates a grid based on a geometry. Draw a geometry around a region of involvement and so run the post-obit script:
// pixelLonLat returns an image with each pixel labeled with longitude and // breadth values. var lonLat = ee.Prototype.pixelLonLat(); // Select the longitude and latitude bands, multiply by a large number then // truncate them to integers. var lonGrid = lonLat .select('longitude') .multiply(10000000) .toInt(); var latGrid = lonLat .select('latitude') .multiply(10000000) .toInt(); // To produce the filigree, multiply the latitude and longitude images and and then use // reduce to vectors at the 10km resolution to group the grid into vectors. var grid = lonGrid .multiply(latGrid) .reduceToVectors({ geometry: geometry, // This is undefined until you describe a geometry. scale: 10000, geometryType: 'polygon', }); Map.addLayer(filigree);
Then nosotros enumerate over the generated Features
in the FeatureCollection
and build a listing of LinearRings
that we'll use to build our GeometryLayer
.
// Fetch the features in the grid and produce a list of linear rings. // The filigree variable is divers in the stride in a higher place. var geometries = grid.toList(1000).map(function(feature) { var featureGeometry = ee.Feature(characteristic).geometry(); var coordinates = featureGeometry.coordinates().get(0); return ee.Geometry.LinearRing(coordinates); });
Finally, nosotros add together the geometries to the map using the cartoon tools. Observe that we employ evaluate
to fetch the geometries customer-side in social club to add them to the drawing tools.
var drawingTools = Map.drawingTools(); // Fetch the geometries so they can be added to the drawing tools. geometries.evaluate(office(geometriesList) { var layer = ui.Map.GeometryLayer({ geometries: geometriesList, proper noun: 'grid', color: 'blackness', shown: true, // Show the layer (already defaults to true). locked: true, // Lock the layer. }); drawingTools.layers().set(1, layer); });
In that location! Now we take a grid that is rendered client-side that won't go away when the map viewport changes. Discover that the geometry is uneditable; that's because we constructed the layer with the locked
property ready to true
.
Figure 2. A map with a grid over the San Francisco Bay Area.
We'll refactor the code to use functions to make it more reusable:
function makeGrid(geometry, calibration) { // pixelLonLat returns an image with each pixel labeled with longitude and // breadth values. var lonLat = ee.Epitome.pixelLonLat(); // Select the longitude and latitude bands, multiply past a large number and so // truncate them to integers. var lonGrid = lonLat .select('longitude') .multiply(10000000) .toInt(); var latGrid = lonLat .select('latitude') .multiply(10000000) .toInt(); // To produce the grid, multiply the latitude and longitude images and then use // reduce to vectors at the 10km resolution to group the grid into vectors. return lonGrid .multiply(latGrid) .reduceToVectors({ geometry: geometry, // This is undefined until you draw a geometry. scale: scale, geometryType: 'polygon', }); } function buildGeometryList(grid, limit) { return grid.toList(limit).map(role(characteristic) { var featureGeometry = ee.Feature(feature).geometry(); var coordinates = featureGeometry.coordinates().get(0); return ee.Geometry.LinearRing(coordinates); }); } var grid = makeGrid(geometry, 10000); // 10 km calibration var geometries = buildGeometryList(grid, 1000); var drawingTools = Map.drawingTools(); geometries.evaluate(function(geometriesList) { var layer = ui.Map.GeometryLayer({ geometries: geometriesList, name: 'filigree', color: 'black', shown: truthful, // Show the layer (already defaults to true). locked: truthful, // Lock the layer. }); drawingTools.layers().set(1, layer); });
Since the filigree has already been added to the imports, the grid
variable is now available for united states to utilize, so nosotros can comment out the fleck that generated the grid in the starting time identify to save the states some computation time.
// var filigree = makeGrid(geometry); // var geometries = buildGeometryList(filigree); // var drawingTools = Map.drawingTools(); // geometries.evaluate(function(geometriesList) { // var layer = ui.Map.GeometryLayer({ // geometries: geometriesList, // name: 'grid', // color: 'black', // shown: true, // Show the layer (already defaults to true). // locked: true, // Lock the layer. // }); // drawingTools.layers().prepare(ane, layer); // });
If you publish this as an app, since the imports carry over to Apps, the grid will be at that place, ready to become!
Event treatment
Finally, the drawing tools API allows united states of america to mind for when users interact with:
- private geometries,
- layers,
- or the cartoon tools themselves,
and lets us run callback functions when they happen. Here are some useful events you tin can listen for:
Drawing Tools Events:
-
onShapeChange(
callback
)
- A describe mode is changed, for examplepoint
,line
,polygon
, orrectangle
.
Geometry Events (callback chosen with geometry, layer, and drawing tools):
-
onDraw(
callback
)
- a geometry is added to a layer. -
onEdit(
callback
)
- a geometry is edited. -
onErase(
callback
)
- a geometry is removed. -
onSelect(
callback
)
- a geometry is selected.
Layer Events (callback called with layer and drawing tools):
-
onLayerAdd(
callback
)
- a layer is added. -
onLayerRemove(
callback
)
- a layer is removed. -
onLayerSelect(
callback
)
- a layer is selected. -
onLayerConfig(
callback
)
- a layer attribute (e.thousand. proper name, color) is changed.
Nosotros tin utilise these to listen for user interactions with the cartoon tools for a responsive and personalized, Globe Engine experience!
Example: Nomenclature With User-Drawn Geometries
The last instance is adapting the Classifications instance to be more than interactive. This example runs a machine learning classification using points divers in iii FeatureCollections
, one for urban, vegetation, and water. Currently, if we wanted to rerun the classification on new points, we would accept to run the script later we edit the points. What if we wanted to rerun the classification automatically when we edit the points?
We can refactor the classification to rely on the points in the drawing tools, which are always kept up to date. To make this easier, nosotros can employ the getEeObject()
method on the GeometryLayer
to go the World Engine object associated with a GeometryLayer
(e.g. the underlying FeatureCollection
).
First, import the feature collections at the top of the file so nosotros accept urban
, vegetation
, and water
in your imports (hover over the text at the top of the script and press convert
on the pick that says ""urban", "vegetation" and "h2o" can be converted to import records.
").
Figure three. Classification tools instance with imported feature collections.
Next, become the geometry layers associated with each of the characteristic collections, since any edits to the geometries will exist reflected on the layer (add to the top of the file):
// Starter script: https://lawmaking.earthengine.google.com/?scriptPath=Examples:Demos/Nomenclature var drawingTools = Map.drawingTools(); // Only allow drawing points. drawingTools.setDrawModes(['betoken']); // Get the layers list. var layers = drawingTools.layers(); // Assuming the order is urban, vegetation, then water. var urbanLayer = layers.go(0); var vegetationLayer = layers.get(1); var waterLayer = layers.go(2);
At present that we have the layers, we can become the FeatureCollection
associated with each layer by calling the getEeObject()
method.
Change the post-obit line from:
// Merge the three geometry layers into a single FeatureCollection. var newfc = urban.merge(vegetation).merge(h2o);
to:
// Merge the three geometry layers into a single FeatureCollection. var urbanfc = urbanLayer.getEeObject(); var vegetationfc = vegetationLayer.getEeObject(); var waterfc = waterLayer.getEeObject(); var newfc = urbanfc.merge(vegetationfc).merge(waterfc);
and run the script to see that everything stayed the same. Next, nosotros will attach effect listeners to the drawing tools to rerun the classification whenever a geometry is inverse.
First, we will bundle all of the classification code into a unmarried function called classify
and make some modifications to the Map.addLayer
calls to accommodate for the function running multiple times:
function classify() { // Merge the three geometry layers into a unmarried FeatureCollection. var urbanfc = urbanLayer.getEeObject(); var vegetationfc = vegetationLayer.getEeObject(); var waterfc = waterLayer.getEeObject(); var newfc = urbanfc.merge(vegetationfc).merge(waterfc); // Employ these bands for classification. var bands = ['B2', 'B3', 'B4', 'B5', 'B6', 'B7']; // The name of the property on the points storing the class label. var classProperty = 'landcover'; // Sample the composite to generate training data. Notation that the // form label is stored in the 'landcover' property. var grooming = composite.select(bands).sampleRegions({ collection: newfc, properties: [classProperty], scale: xxx }); // Railroad train a CART classifier. var classifier = ee.Classifier.smileCart().railroad train({ features: training, classProperty: classProperty, }); // Impress some info about the classifier (specific to CART). impress('CART, explained', classifier.explain()); // Classify the blended. var classified = composite.classify(classifier); var palette = {min: 0, max: 2, palette: ['red', 'green', 'blue']}; var layer = ui.Map.Layer(classified, palette, 'classified'); Map.layers().set up(0, layer); // Optionally, practice some accuracy assessment. Fist, add together a column of // random uniforms to the training dataset. var withRandom = training.randomColumn('random'); // We desire to reserve some of the data for testing, // to avert overfitting the model. var dissever = 0.seven; // Roughly lxx% training, 30% testing. var trainingPartition = withRandom.filter(ee.Filter.lt('random', split)); var testingPartition = withRandom.filter(ee.Filter.gte('random', split)); // Trained with 70% of our data. var trainedClassifier = ee.Classifier.smileRandomForest(5).train({ features: trainingPartition, classProperty: classProperty, inputProperties: bands }); // Classify the test FeatureCollection. var test = testingPartition.classify(trainedClassifier); // Print the defoliation matrix. var confusionMatrix = test.errorMatrix(classProperty, 'classification'); impress('Defoliation Matrix', confusionMatrix); } classify();
Now, when we call allocate
, the script will utilise the upward-to-date version of the imports to run the classifier, which means we don't need to rerun the script to capture new points!
Nosotros add consequence listeners to detect when the user has made whatever edits to the geometries and run the allocate
function. We besides utilize ui.util.debounce
to merely run it once every 100 milliseconds to forbid it from firing as well fast, like when the user drags a indicate effectually.
// Use debounce to call the role at most every 100 milliseconds. drawingTools.onEdit(ui.util.debounce(allocate, 100)); drawingTools.onDraw(ui.util.debounce(classify, 100)); drawingTools.onErase(ui.util.debounce(classify, 100)); Map.centerObject(urban);
That'south all there is to it! Here's a link to the full script.
Determination
The cartoon tools API provides a rich gear up of features that give you full control of the client-side geometries in your World Engine scripts and apps. Nosotros're excited to see what y'all build with them!
Bonus
Since y'all made it this far down, here's an Earth Engine script that recreates the drawing tools from scratch :)
Source: https://developers.google.com/earth-engine/tutorials/community/drawing-tools