Wednesday, April 22, 2015

OpenLayers 3 for Beginners: Part 3

Welcome to the next part of OpenLayers 3 for Beginners!
If you have not worked through parts one or two yet, you can hop over to them here:
If you are a OpenLayers map creating and layer guru, you can download the source code here:
http://christopherjennison.com/tutorials/hello-map_2_final.zip
This is going to be the last foundation tutorial for this series. This doesn’t mean there wont be anymore tutorials, but everything else is an amendment of Layers, Maps, and Interactions. The next tutorial will be about Building Mapping Applications with OpenLayers 3 where we use the ideas presented in these tutorials as foundations for our very own mapping application.
With due course, let’s begin.


OpenLayers 3 for Beginners

Part 3: Interactions and Controls

Last time we discussed Layers, both Vector and Raster, and prior to that we created a map. These things are all great, but we are missing a core component of this. That component is, and you guessed it, Interactions. In Web Mapping, all Interactions have to do with changing a map in a way that a user can view data in a manner that suites them the best. The issue we will inevitably run into is that interactions are application specific. We don’t need to plot streams on a map about earthquakes, we also don’t need to add Dropout Rates of Students on a weather map.
We are going to keep this tutorial very general and leave the specificity to our actual application. We are going to implement the following things in a general manner so that, by doing this, we can use what we know of the foundations of implementing interactions to implement more specific and useful interactions. Be prepared to use ‘hack-like’ options to implement these solutions. While this library is still being released, some solutions are forced to be implemented by hacks. If you have a better way of doing these things, please inform me on Twitter or in the comments. My solutions work, but they are most likely not the best.
Interactions in OpenLayers are defined in two ways, build-in Interactions such as selections, highlighting, and drawing and hacked-in interactions that we may have to implement ourselves when it comes to things like creating bounding boxes, or calculating some extent of a polygon. This is a broad and lightly stepped tutorial on using controls and interactions. There are tens of thousands of words that I could spend writing or even write a book just about controls.
Here’s the agenda:
  • Adding Simple Controls
  • Mouse Move and Click Events
  • Drawing on the map
  • Creating Popups and Getting Information
  • Extents (Bounding Boxes)
If there are things I am not mentioning but you would like to know how to do them, comment your suggestion! I will reference a link you provide me as the person who asked the question.

1. OpenLayers 3 Controls: An Overview

Before we dive in, head first, we should analyze exactly what OpenLayers can do for us. OpenLayers has a Control class at http://ol3js.org/en/master/apidoc/ol.control.Control.html. These are our built-in options. Let’s take a quick look:
  • Attribution: This is the credit for any layer on the screen. If you use OpenStreetMap as your basemap you may see a copyright at the bottom right corner of your screen. I recommend you keep that there for the respect for the authors.
  • FullScreen: This is a control that allows you to place your map in full screen mode.
  • Logo: This is the OpenLayers logo. We all love OpenLayers, so we keep this.
  • MousePosition: This tracks the position of the mouse on our page, we will use this very much.
  • Rotate: For rotating the map. I found this more fun than useful.
  • ScaleLine: This is our, literally, scale line for viewing distances.
  • Zoom: This is how we zoom the map.
  • ZoomSlider: This will replace your default zoom button.
  • ZoomToExtent: This is used to zoom to a two (2) coordinate array of coordinates. This is useful, but we still need to calculate the extent.
These are the basic controls for your map. They should all seem pretty self-explanatory, but in case they don’t, let’s use them.
First, however, adding controls is easy! Add a control after your map has been instantiated. If you know what controls you need before hand, you can add controls as an array in the map object at instantiation.
map.addControl(myControlName);
Then, viola! The control has been added.
But be aware, we have made one fatal mistake. By default, the map shows a Logo, Zoom, and Attribution. Check your map, where are they?
House, OpenLayers 3 I Don't Know.
Well I don’t know where the OpenLayers controls are.
Quick Fix
Well we can see our Zoom control but not the other two I am implying are there. You may have experienced this before, but OpenLayers is 100% of our page size, height-wise, which at the moment is much larger than our actual window. All we have to do to fix such a thing is create two (2) CSS tags in your Index.html file.
html{height:100%}
body{height:100%}
and then change your map tag to:
#map{
  height:98%;
  width:100%
}
Refresh your page and you should see it all!

OpenLayers Map with Controls
All of our controls on our OpenLayers Map. Feels good to give someone credit.
Here are all of the controls in code. Do not use these in our project, yet.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//Attribution
var myAttributionControl = new ol.control.Attribution({
  className:'ol-attribution', //default parameter
  target:null, //default parameter. Places attribution in a div
});
map.addControl(myAttributionControl);
//This is a default control. If you felt like destroying someone else's credit, you could use the css reference to stop displaying the attribution.
//Logo
var myLogoControl = new ol.control.Logo({
  className:'ol-logo', //default parameter
});
map.addControl(myLogoControl);
//This is a default control. Show you are proud of your OpenLayers 3 library!
//Mouse Position
var mousePositionControl = new ol.control.MousePosition({
  className:'ol-full-screen', //default parameter
  coordinateFormat:ol.coordinate.createStringXY(4), //This is the format we want the coordinate in. 
  //The number arguement in createStringXY is the number of decimal places.
  projection:"EPSG:4326", //This is the actual projection of the coordinates. 
  //Luckily, if our map is not native to the projection here, the coordinates will be transformed to the appropriate projection.
  className:"custom-mouse-position",
  target:undefined, //define a target if you have a div you want to insert into already,
  undefinedHTML: ' ' //what openlayers will use if the map returns undefined for a map coordinate.
});
map.addControl(mousePositionControl);
//Full Screen
var myFullScreenControl = new ol.control.FullScreen();
map.addControl(myFullScreenControl);
//Rotate
var myRotateControl = new ol.control.Rotate()
map.addControl(myRotateControl);
//ScaleLine
var myScaleLine = new ol.control.ScaleLine()
map.addControl(myScaleLine);
//I often use the scale line. The default implementation looks nice.
//Zoom
var myZoom = new ol.control.Zoom();
map.addControl(myZoom);
//Zoom is a default control, but there are some parameters you could change if you wanted:
//Check them out here: http://ol3js.org/en/master/apidoc/ol.control.Zoom.html
//ZoomSlider
var myZoomSlider = new ol.control.ZoomSlider();
map.addControl(myZoomSlider);
//The zoom slider is a nice addition to your map. It is wise to have it accompany your zoom buttons.
//ZoomToExtent
var myExtentButton = new ol.control.ZoomToExtent({
    extent:undefined
});
map.addControl(myExtentButton);
//This is a complicated button. We will implement this in a special way. The key for this
//is to create an extent and pass it to the button. If undefined, the extent is the entire map.

Feel free to throw a couple of the controls into your project, but for our cases, you should only add in the ZoomToExtent control. Everything else is up to you.
These are the controls. They can really improve an application in GIS. I strongly encourage you use some of these appropriately.
OL3 Controls
All of OpenLayers Controls
Before we start talking about events, I have edited my file to clean things up. We don’t need to use the buttons we created before.
Download my source code here, which includes the controls. http://christopherjennison.com/tutorials/hello-map_3_controls.zip

2. Mouse Events

Now that we have implemented some of OpenLayer’s built-in controls, we can start to look into some click events.
Mouse Events are going to define the meat of our application’s abilities. Now we are going to turn our application from a map viewer to something that really makes things happen.
Much like JavaScript and JQuery’s Event Listeners, OpenLayers works very similarly. Add this to the end of your init() function.
1
2
3
map.on('singleclick', function(evt){
    console.log(evt);
})
Reload your application on the local server, and click the map while viewing the JavaScript console. You should see an object appear on your click! This object will have some important variables within it we should pay attention to. You can peer around the other variables if you find interesting.
coordinate: This is a two length array of a coordinate point containing the coordinate point of where you clicked. This is in the projection format of the map.
point: This is the point on the DOM plane where you made the click.
Let’s start off working with that coordinate point.
Imagine you are developing an application that has a very complex model on a server and the modeler needs you to pass to him a coordinate point, however, because he is a scientist, he can’t accept the projection format you are using in your map. Recall we need to use this projection format for user ease of use. We don’t wan’t to make the scientist alter a projection he wasn’t prepared for, so that duty is on us.
We need to transform a projection.
We can accomplish this by using OpenLayer’s Projection class. This class has a nice method calledtransform(). Transform takes in three (3) parameters for us.
([Coordinate as an Array], “EPSG:[From EPSG Code]”, “EPSG:[To EPSG Code]”)
Easy, so let’s do it!
1
2
3
4
5
map.on('singleclick', function(evt){
    var coord = evt.coordinate;
    var transformed_coordinate = ol.proj.transform(coord, "EPSG:900913", "EPSG:4326");
    console.log(transformed_coordinate);
})
  • 1: This is our click event.
  • 2: We then store the coordinate into a variable.
  • 3: Here is the transformation. Returned to us is a 2 length array of the transformed coordinates.
Reload your map and take a click! You should see an array pop up in your JavaScript console. This coordinate can now be used to pass to your scientist friend to do whatever he needs. This is a fairly easy subject, but this allows us to discuss a better topic.


3. Getting Information and Popups

Before we start, we need to set some things up. First, I enjoy JQuery, so we’re going to use some JQuery for this next part. Second, we have some CSS to define.
Without further adieu, add a <script> tag to your html file.
<script src='http://code.jquery.com/jquery-1.11.1.js'></script>
Now, before we change some CSS we should understand what it is we are doing.
Having a map is great on a broad spatial scale, it allows us to make general assumptions about large areas. But what about small scale? Our map is split into thousands of catchments, we only want one catchment. In fact, we only want the information within one catchment. Furthermore, we want the precise value of that catchment.
From the surface, this sounds pretty complicated. What do we know about layers thus far? Well, we do know about the WFS. We are going to need to get the exact properties of the catchment, so perhaps that would work. But where are we going to store this information?
You could, in theory, put this information anywhere. But our imaginary client is in love with popups. So we best deliver what they want.
Alright, Popups. There is a way to do popups ‘The Easy Way’ but this is unformatted and ugly. Head over here if you want to see it in action: http://ol3js.org/en/master/examples/popup.html?q=popup.
I want to do Popups the Chris Jennison way and I’d like to show you how. But first, the fundamentals.
First, we need to create an Overlay. An Overlay houses the popup we are going to create. By OpenLayers definition, the Overlay is a container that points to a given coordinate. We’re going to spawn our overlay from within our click function and create a popup at run time. Let’s do it!
Create a function spawnPopup(coord); and make a call to it with the coordinate we initially get as a result of the click function. It’ll look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
map.on('singleclick', function(evt){
    var coord = evt.coordinate;
    spawnPopup(coord);
});
function spawnPopup(coord){
    var popup = $("<div class='popup'></div>");
    
    var overlay = new ol.Overlay({
        element:popup
    });
    
    map.addOverlay(overlay);
    overlay.setPosition(coord);
}
As you can see, we’ll also create a popup object, then the overlay and add our popup as an argument to the overlay params object. We add the overlay to our map and then set the coordinates.
Before you refresh, jump over to the html file and add a CSS tag for our popup.
.popup{
  background:white;
  width:250px;
  height:200px;
  border-radius:5px;
}
Refresh your map and click somewhere.
Squares on a map.
Squares on our OpenLayers map!
Good, popups! Well, sort of. I’m going to go ahead and add a close button to our popup and a check to make sure no other popups are on the screen. You can do this yourself or download my code. This is a OpenLayers tutorial, not JavaScript 101.
http://christopherjennison.com/tutorials/hello-map_3_popupstart.zip
Make sure you download or make the necessary changes.
Otherwise, great, we have popups and they work. Now we need to get that data out of them.
I lied, we aren’t going to use WFS. We are going to use WMS, and it’s going to take a few changes.
We are using WMS so that we may utilize the getGetFeatureInfoUrl method from within the TileWMS class. If we are to extract information from a layer, we also have to pull the source out of one of our layers. Within our click method, we need to write a long string of code. Add this after the function call to spawnPopup(coord).
1
2
3
4
5
6
7
8
9
10
11
12
13
var viewProjection = map.getView().getProjection();
var viewResolution = map.getView().getResolution();
var url = vectorLayer.getSource().getGetFeatureInfoUrl(coord, viewResolution, viewProjection, {
    'INFO_FORMAT' : 'application/json'
});
if (url) {
    console.log(url)
    $.getJSON(url, function(d) {
        console.log(d);
    })
} else {
    console.log("Uh Oh, something went wrong.");
}
Let’s discuss this in detail:
  • 1: We need the projection. We can get this easily from the map’s View class.
  • 2: We need the resolution, same source.
  • 3: Here is where we build the url. We get the source from our vector layer and use it’s getGetFeatureInfoUrl method passing in our coordinates from our click, the projection and resolution.
  • 4: I also specified that I wanted the url to return to us a json format. There are other formats we could use, but JSON, in my opinion, is very good.
  • 6 – 13: If the URL works (if any of our information fails, OpenLayers will return null) then use JQuery’s getJSON method to get the information back from GeoServer.
If you try to do this again, you will unfortunately encounter a fatal error that I apologize for, deeply.
XMLHttpRequest cannot load http://felek.cns.umass.edu:8080/geoserver/wms?SERVICE=WMS&VERSION=1.3.0&REQ…754.171394622%2C3757032.814272985%2C-8766409.899970295%2C5009377.085697313. No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://localhost:8888‘ is therefore not allowed access. (index):1
Unfortunately for us, this is going to put a pause of our adventures into getting information out.
If you know a bit about why this occurs, this is because GeoServer, while using Tomcat, does not feel safe giving you information. This is something I will not fix, because this is my server. If this were a real application, the application would have to be built on the server hosting Tomcat or adding the local host to the list of approved origins.
In theory, the end product looks like this (With styling and a lot more code, of course:

NALCC OpenLayers GetFeatureInfo
OpenLayers GetFeatureInfo Example
The core concept to take away is that we have to use the source of a layer to get its information back from a server. With the source, the layer is already linked to where we got the picture from, we simple send a new request, but this time for the information.

4. Drawing

Let’s talk a bit about how to Draw things.
OpenLayers is well equipped to Draw things on your map and all of the applications of drawing are limitless. I use drawing specifically for creating bounding boxes for users using a linestring. We’ll learn about extents in the next section. Drawing is very standard to OpenLayers. I will explain it here, but if you find my writing and explanations annoying, head over to the Drawing Example and copy and paste that to your program. http://ol3js.org/en/master/examples/draw-features.html?q=Draw
Let’s create three buttons, Draw Points, Lines, and Polygons.
1
2
3
<button class='draw-point' onclick='startDraw("Point")' style='position: absolute; top:300px;'>Draw Points</button>
<button class='draw-line' onclick='startDraw("LineString")' style='position: absolute; top:350px;'>Draw Lines</button>
<button class='draw-polygon' onclick='startDraw("Polygon")' style='position: absolute; top:400px;'>Draw Polygon</button>
Then let’s create functions for our buttons.
1
2
3
4
5
6
7
8
9
10
11
var draw;
function startDraw(type){
    if(draw != null){
        cancelDraw();   
    }
    
}
function cancelDraw(){
    
}
Good, now this isn’t any of the actual OpenLayers code, so let’s fix that.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//global variable
var drawLayer;
//Inside init() function
drawLayer = new ol.layer.Vector({
    source : new ol.source.Vector(),
    style : new ol.style.Style({
        fill : new ol.style.Fill({
            color : 'rgba(255, 255, 255, 0.2)'
        }),
        stroke : new ol.style.Stroke({
            color : '#ffcc33',
            width : 2
        }),
        image : new ol.style.Circle({
            radius : 7,
            fill : new ol.style.Fill({
                color : '#ffcc33'
            })
        })
    })
});
map.addLayer(drawLayer);
There’s nothing new here, we’re just creating a layer specifically for drawing. We could, if we wanted, use one of our existing Vector layers to draw on, but I like to keep things separated.
Before we go and recklessly copy and paste more code, let’s discuss the thinking of this system. We have three buttons which denote different drawing types. We need to create a new OpenLayers Interaction for drawing when the button is clicked, but if any interactions are present we must destroy those two. Interactions, you say? Isn’t that what this tutorial is about. Why yes, voice in my head, it is.


Interactions are a huge part of OpenLayers, but many of them are already in your application. Check out the API for all of them. http://ol3js.org/en/master/apidoc/ol.interaction.html
Things like Drag Box (Shift and Click-Drag) and MouseWheelZoom, these are things already built into your application. We wont go over them because it would be unnecessary to re-implement all interactions into you application. The one we really don’t have that stands out is Draw, which is why we are building that now. Interactions are great for using existing parts of them to do very cool things. For example, you want to draw a bounding box. You can create an event listener for ‘boxstart‘ and ‘boxend‘ to check when a bounding box has been created with Shift Click-Drag. Then, call getGeometry() on the DragBox class to get the last box drawn, then getExtent() and viola, we have a bounding box geometry.
Altering Interactions are based on the needs of your application. If you want me to go over any interactions so that you may use them in your application, drop a request in the comments.


I digress, how about that drawing class?
First, add the following line to the beginning of your click function.
if(draw != null) return;
We do this just because we don’t want to be making popups when we’re drawing. You can make this better later on, but I like the easy route. Now let’s add the draw class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function startDraw(type) {
    if (draw != null) {
        cancelDraw();
    }
    
    draw = new ol.interaction.Draw({
        source:drawLayer.getSource(),
        type:type
    });
    map.addInteraction(draw);
}
function cancelDraw() {
    if(draw == null)return;
    
    map.removeInteraction(draw);
}
Painless and easy, just the way we like it. We update the draw variable with a draw class, we pass in our draw layer’s source and the type of draw interaction it is.

OpenLayers 3 Drawing
Drawing on OpenLayers 3

5. Extents

Remember that extent button we had? Let’s make that do something.
First, we should talk about what an extent is. An extent is the maximum distance between two points, converging as a rectangle, that fully contains all points of a given polygon or image within that rectangle. For example, if two points have the coordinates (20, 400) and (500, 10) then the extent of the rectangle that covers the coordinates is (500, 400) and (20, 10). That is, [Max X, Max Y] and [Min X, Min Y]. These are interchangeable, as long as they create the same rectangle.
One could calculate these themselves or they can use the source of the layer, like we did to make agetFeatureInfo() call. There is one catch, this can only be done on WFS layers. Specifically, for us, we have a WFS source on our GeoJSON layer from Part 2.
Let’s change our ZoomToExtent button from section 1 on this tutorial.
1
2
3
4
//ZoomToExtent
myExtentButton = new ol.control.ZoomToExtent({
    extent: geoLayer.getSource().getExtent()
});
This was easy and great, but I wouldn’t be teaching you right if I didn’t show you another way.
The ZoomToExtent button is awesome and I use it frequently, but ZoomToExtent actually goes to your View2D to actually fit the extent with view.fitExtent().
Write a new function called ZoomMapToExtent like this:
1
2
3
function fitMapToExtent(extent){
    map.getView().fitExtent(extent, map.getSize());
}
This should look pretty simple, all this will do is exactly what you extent button does. Except, we also pass in the size of the map with map.getSize().
Now you are a master of extents.
GeoJSON Extent OpenLayers
OpenLayers Extent of GeoJSON
Unfortunately, you may have noticed, it is not perfect. If you are looking for perfect, you will need to calculate the extent yourself.
If you followed the Drawing section, you can now draw bounding boxes, then, use the source to get the extent like above.
Ta-da! Instant extent drawing tool. (I use tools like these for getting catchments from a server using the extent coordinates. If you are just zooming, use Shift-Drag)

Congratulations!

You have a basic familiarity with OpenLayers 3! By completing all parts of this three part series, you have the ability to create basic mapping applications using OpenLayers 3. Be aware, however, this is only the beginning of your GIS adventure.
I do apologize for only touching the surface of some interactions and controls, but these could go in a million different directions. The biggest thing you can do for yourself is read the API and peer at the examples OpenLayers has provided for us. But with what you have done here in these tutorials will prepare you to be able to look at the code presented in the examples and understand how to migrate it into your own application.
While these are simply started tutorials, you will encounter difficult GIS based issues. If you want me to implement something or clarify anything in any of the tutorials, please let me know!
These were the first tutorials I have ever written and I am impressed by the sheer amount of interest people have. GIS is a fantastic area to learn and be familiar with and can benefit not only your and your organization but the public as well. GIS is your ability to play public relations and be the voice of your data. I hope you learned a lot and feel somewhat more understanding of OpenLayers 3.
Make sure to subscribe to my blog for more updates! Comment requests and let me know as soon as possible of any errors or suggestions you have!
Here are some resourced for going forward:
Download the final source code here: http://christopherjennison.com/tutorials/hello-map_3_final.zip

No comments:

Post a Comment

Please Comment Here!

How to backup and download Database using PHP

< ?php $mysqlUserName = 'databaseusername' ; $mysqlPassword = 'databasepassword' ; $mysqlHostNa...