Cleanup some JS files and webpack info
@@ -0,0 +1,5 @@
|
||||
---
|
||||
layout: redirected
|
||||
sitemap: false
|
||||
redirect_to: choropleth/example.html
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
layout: redirected
|
||||
sitemap: false
|
||||
redirect_to: choropleth/
|
||||
---
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Choropleth Tutorial
|
||||
---
|
||||
<script type="text/javascript" src="us-states.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
var map = L.map('map').setView([37.8, -96], 4);
|
||||
|
||||
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
|
||||
maxZoom: 18,
|
||||
attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
|
||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
|
||||
'Imagery © <a href="http://mapbox.com">Mapbox</a>',
|
||||
id: 'mapbox.light'
|
||||
}).addTo(map);
|
||||
|
||||
var geojson = L.geoJson(statesData).addTo(map);
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Choropleth Tutorial
|
||||
---
|
||||
|
||||
<script type="text/javascript" src="us-states.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
var map = L.map('map').setView([37.8, -96], 4);
|
||||
|
||||
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
|
||||
maxZoom: 18,
|
||||
attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
|
||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
|
||||
'Imagery © <a href="http://mapbox.com">Mapbox</a>',
|
||||
id: 'mapbox.light'
|
||||
}).addTo(map);
|
||||
|
||||
// get color depending on population density value
|
||||
function getColor(d) {
|
||||
return d > 1000 ? '#800026' :
|
||||
d > 500 ? '#BD0026' :
|
||||
d > 200 ? '#E31A1C' :
|
||||
d > 100 ? '#FC4E2A' :
|
||||
d > 50 ? '#FD8D3C' :
|
||||
d > 20 ? '#FEB24C' :
|
||||
d > 10 ? '#FED976' :
|
||||
'#FFEDA0';
|
||||
}
|
||||
|
||||
function style(feature) {
|
||||
return {
|
||||
weight: 2,
|
||||
opacity: 1,
|
||||
color: 'white',
|
||||
dashArray: '3',
|
||||
fillOpacity: 0.7,
|
||||
fillColor: getColor(feature.properties.density)
|
||||
};
|
||||
}
|
||||
|
||||
var geojson = L.geoJson(statesData, {
|
||||
style: style,
|
||||
}).addTo(map);
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,159 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Choropleth Tutorial
|
||||
css: "#map {
|
||||
width: 800px;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: 6px 8px;
|
||||
font: 14px/16px Arial, Helvetica, sans-serif;
|
||||
background: white;
|
||||
background: rgba(255,255,255,0.8);
|
||||
box-shadow: 0 0 15px rgba(0,0,0,0.2);
|
||||
border-radius: 5px;
|
||||
}
|
||||
.info h4 {
|
||||
margin: 0 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.legend {
|
||||
text-align: left;
|
||||
line-height: 18px;
|
||||
color: #555;
|
||||
}
|
||||
.legend i {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
float: left;
|
||||
margin-right: 8px;
|
||||
opacity: 0.7;
|
||||
}"
|
||||
---
|
||||
|
||||
<script type="text/javascript" src="us-states.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
var map = L.map('map').setView([37.8, -96], 4);
|
||||
|
||||
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
|
||||
maxZoom: 18,
|
||||
attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
|
||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
|
||||
'Imagery © <a href="http://mapbox.com">Mapbox</a>',
|
||||
id: 'mapbox.light'
|
||||
}).addTo(map);
|
||||
|
||||
|
||||
// control that shows state info on hover
|
||||
var info = L.control();
|
||||
|
||||
info.onAdd = function (map) {
|
||||
this._div = L.DomUtil.create('div', 'info');
|
||||
this.update();
|
||||
return this._div;
|
||||
};
|
||||
|
||||
info.update = function (props) {
|
||||
this._div.innerHTML = '<h4>US Population Density</h4>' + (props ?
|
||||
'<b>' + props.name + '</b><br />' + props.density + ' people / mi<sup>2</sup>'
|
||||
: 'Hover over a state');
|
||||
};
|
||||
|
||||
info.addTo(map);
|
||||
|
||||
|
||||
// get color depending on population density value
|
||||
function getColor(d) {
|
||||
return d > 1000 ? '#800026' :
|
||||
d > 500 ? '#BD0026' :
|
||||
d > 200 ? '#E31A1C' :
|
||||
d > 100 ? '#FC4E2A' :
|
||||
d > 50 ? '#FD8D3C' :
|
||||
d > 20 ? '#FEB24C' :
|
||||
d > 10 ? '#FED976' :
|
||||
'#FFEDA0';
|
||||
}
|
||||
|
||||
function style(feature) {
|
||||
return {
|
||||
weight: 2,
|
||||
opacity: 1,
|
||||
color: 'white',
|
||||
dashArray: '3',
|
||||
fillOpacity: 0.7,
|
||||
fillColor: getColor(feature.properties.density)
|
||||
};
|
||||
}
|
||||
|
||||
function highlightFeature(e) {
|
||||
var layer = e.target;
|
||||
|
||||
layer.setStyle({
|
||||
weight: 5,
|
||||
color: '#666',
|
||||
dashArray: '',
|
||||
fillOpacity: 0.7
|
||||
});
|
||||
|
||||
if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
|
||||
layer.bringToFront();
|
||||
}
|
||||
|
||||
info.update(layer.feature.properties);
|
||||
}
|
||||
|
||||
var geojson;
|
||||
|
||||
function resetHighlight(e) {
|
||||
geojson.resetStyle(e.target);
|
||||
info.update();
|
||||
}
|
||||
|
||||
function zoomToFeature(e) {
|
||||
map.fitBounds(e.target.getBounds());
|
||||
}
|
||||
|
||||
function onEachFeature(feature, layer) {
|
||||
layer.on({
|
||||
mouseover: highlightFeature,
|
||||
mouseout: resetHighlight,
|
||||
click: zoomToFeature
|
||||
});
|
||||
}
|
||||
|
||||
geojson = L.geoJson(statesData, {
|
||||
style: style,
|
||||
onEachFeature: onEachFeature
|
||||
}).addTo(map);
|
||||
|
||||
map.attributionControl.addAttribution('Population data © <a href="http://census.gov/">US Census Bureau</a>');
|
||||
|
||||
|
||||
var legend = L.control({position: 'bottomright'});
|
||||
|
||||
legend.onAdd = function (map) {
|
||||
|
||||
var div = L.DomUtil.create('div', 'info legend'),
|
||||
grades = [0, 10, 20, 50, 100, 200, 500, 1000],
|
||||
labels = [],
|
||||
from, to;
|
||||
|
||||
for (var i = 0; i < grades.length; i++) {
|
||||
from = grades[i];
|
||||
to = grades[i + 1];
|
||||
|
||||
labels.push(
|
||||
'<i style="background:' + getColor(from + 1) + '"></i> ' +
|
||||
from + (to ? '–' + to : '+'));
|
||||
}
|
||||
|
||||
div.innerHTML = labels.join('<br>');
|
||||
return div;
|
||||
};
|
||||
|
||||
legend.addTo(map);
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,232 @@
|
||||
---
|
||||
layout: tutorial_v2
|
||||
title: Interactive Choropleth Map
|
||||
---
|
||||
|
||||
## Interactive Choropleth Map
|
||||
|
||||
This is a case study of creating a colorful interactive [choropleth map](http://en.wikipedia.org/wiki/Choropleth_map) of US States Population Density with the help of [GeoJSON](../geojson/) and some [custom controls](../../reference.html#control) (that will hopefully convince all the remaining major news and government websites that do not use Leaflet yet to start doing so).
|
||||
|
||||
The tutorial was inspired by the [Texas Tribune US Senate Runoff Results map](http://www.texastribune.org/library/data/us-senate-runoff-results-map/) (also powered by Leaflet), created by [Ryan Murphy](http://www.texastribune.org/about/staff/ryan-murphy/).
|
||||
|
||||
{% include frame.html url="example.html" width=816 height=516 %}
|
||||
|
||||
### Data Source
|
||||
|
||||
We'll be creating a visualization of population density per US state. As the amount of data (state shapes and the density value for each state) is not very big, the most convenient and simple way to store and then display it is [GeoJSON](../geojson/).
|
||||
|
||||
Each feature of our GeoJSON data ([us-states.js](us-states.js)) will look like this:
|
||||
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"name": "Alabama",
|
||||
"density": 94.65
|
||||
},
|
||||
"geometry": ...
|
||||
...
|
||||
}
|
||||
|
||||
The GeoJSON with state shapes was kindly shared by [Mike Bostock](http://bost.ocks.org/mike) of [D3](http://d3js.org/) fame, extended with density values from [this Wikipedia article](http://en.wikipedia.org/wiki/List_of_U.S._states_by_population_density) based on July 1st 2011 data from [US Census Bureau](http://www.census.gov/) and assigned to `statesData` JS variable.
|
||||
|
||||
### Basic States Map
|
||||
|
||||
Let's display our states data on a map with a custom Mapbox style for nice grayscale tiles that look perfect as a background for visualizations:
|
||||
|
||||
var mapboxAccessToken = {your access token here};
|
||||
var map = L.map('map').setView([37.8, -96], 4);
|
||||
|
||||
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=' + mapboxAccessToken, {
|
||||
id: 'mapbox.light',
|
||||
attribution: ...
|
||||
}).addTo(map);
|
||||
|
||||
L.geoJson(statesData).addTo(map);
|
||||
|
||||
{% include frame.html url="example-basic.html" %}
|
||||
|
||||
|
||||
### Adding Some Color
|
||||
|
||||
Now we need to color the states according to their population density. Choosing nice colors for a map can be tricky, but there's a great tool that can help with it --- [ColorBrewer](http://colorbrewer2.org/). Using the values we got from it, we create a function that returns a color based on population density:
|
||||
|
||||
function getColor(d) {
|
||||
return d > 1000 ? '#800026' :
|
||||
d > 500 ? '#BD0026' :
|
||||
d > 200 ? '#E31A1C' :
|
||||
d > 100 ? '#FC4E2A' :
|
||||
d > 50 ? '#FD8D3C' :
|
||||
d > 20 ? '#FEB24C' :
|
||||
d > 10 ? '#FED976' :
|
||||
'#FFEDA0';
|
||||
}
|
||||
|
||||
Next we define a styling function for our GeoJSON layer so that its `fillColor` depends on `feature.properties.density` property, also adjusting the appearance a bit and adding a nice touch with dashed stroke.
|
||||
|
||||
function style(feature) {
|
||||
return {
|
||||
fillColor: getColor(feature.properties.density),
|
||||
weight: 2,
|
||||
opacity: 1,
|
||||
color: 'white',
|
||||
dashArray: '3',
|
||||
fillOpacity: 0.7
|
||||
};
|
||||
}
|
||||
|
||||
L.geoJson(statesData, {style: style}).addTo(map);
|
||||
|
||||
Looks much better now!
|
||||
|
||||
{% include frame.html url="example-color.html" %}
|
||||
|
||||
|
||||
### Adding Interaction
|
||||
|
||||
Now let's make the states highlighted visually in some way when they are hovered with a mouse. First we'll define an event listener for layer `mouseover` event:
|
||||
|
||||
function highlightFeature(e) {
|
||||
var layer = e.target;
|
||||
|
||||
layer.setStyle({
|
||||
weight: 5,
|
||||
color: '#666',
|
||||
dashArray: '',
|
||||
fillOpacity: 0.7
|
||||
});
|
||||
|
||||
if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
|
||||
layer.bringToFront();
|
||||
}
|
||||
}
|
||||
|
||||
Here we get access to the layer that was hovered through `e.target`, set a thick grey border on the layer as our highlight effect, also bringing it to the front so that the border doesn't clash with nearby states (but not for IE, Opera or Edge, since they have problems doing `bringToFront` on `mouseover`).
|
||||
|
||||
Next we'll define what happens on `mouseout`:
|
||||
|
||||
function resetHighlight(e) {
|
||||
geojson.resetStyle(e.target);
|
||||
}
|
||||
|
||||
The handy `geojson.resetStyle` method will reset the layer style to its default state (defined by our `style` function). For this to work, make sure our GeoJSON layer is accessible through the `geojson` variable by defining it before our listeners and assigning the layer to it later:
|
||||
|
||||
var geojson;
|
||||
// ... our listeners
|
||||
geojson = L.geoJson(...);
|
||||
|
||||
As an additional touch, let's define a `click` listener that zooms to the state:
|
||||
|
||||
function zoomToFeature(e) {
|
||||
map.fitBounds(e.target.getBounds());
|
||||
}
|
||||
|
||||
Now we'll use the `onEachFeature` option to add the listeners on our state layers:
|
||||
|
||||
function onEachFeature(feature, layer) {
|
||||
layer.on({
|
||||
mouseover: highlightFeature,
|
||||
mouseout: resetHighlight,
|
||||
click: zoomToFeature
|
||||
});
|
||||
}
|
||||
|
||||
geojson = L.geoJson(statesData, {
|
||||
style: style,
|
||||
onEachFeature: onEachFeature
|
||||
}).addTo(map);
|
||||
|
||||
This makes the states highlight nicely on hover and gives us the ability to add other interactions inside our listeners.
|
||||
|
||||
### Custom Info Control
|
||||
|
||||
We could use the usual popups on click to show information about different states, but we'll choose a different route --- showing it on state hover inside a [custom control](../../reference.html#icontrol).
|
||||
|
||||
Here's the code for our control:
|
||||
|
||||
var info = L.control();
|
||||
|
||||
info.onAdd = function (map) {
|
||||
this._div = L.DomUtil.create('div', 'info'); // create a div with a class "info"
|
||||
this.update();
|
||||
return this._div;
|
||||
};
|
||||
|
||||
// method that we will use to update the control based on feature properties passed
|
||||
info.update = function (props) {
|
||||
this._div.innerHTML = '<h4>US Population Density</h4>' + (props ?
|
||||
'<b>' + props.name + '</b><br />' + props.density + ' people / mi<sup>2</sup>'
|
||||
: 'Hover over a state');
|
||||
};
|
||||
|
||||
info.addTo(map);
|
||||
|
||||
We need to update the control when the user hovers over a state, so we'll also modify our listeners as follows:
|
||||
|
||||
function highlightFeature(e) {
|
||||
...
|
||||
info.update(layer.feature.properties);
|
||||
}
|
||||
|
||||
function resetHighlight(e) {
|
||||
...
|
||||
info.update();
|
||||
}
|
||||
|
||||
The control also needs some CSS styles to look nice:
|
||||
|
||||
{: .css}
|
||||
.info {
|
||||
padding: 6px 8px;
|
||||
font: 14px/16px Arial, Helvetica, sans-serif;
|
||||
background: white;
|
||||
background: rgba(255,255,255,0.8);
|
||||
box-shadow: 0 0 15px rgba(0,0,0,0.2);
|
||||
border-radius: 5px;
|
||||
}
|
||||
.info h4 {
|
||||
margin: 0 0 5px;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
### Custom Legend Control
|
||||
|
||||
Creating a control with a legend is easier, since it is static and doesn't change on state hover. JavaScript code:
|
||||
|
||||
var legend = L.control({position: 'bottomright'});
|
||||
|
||||
legend.onAdd = function (map) {
|
||||
|
||||
var div = L.DomUtil.create('div', 'info legend'),
|
||||
grades = [0, 10, 20, 50, 100, 200, 500, 1000],
|
||||
labels = [];
|
||||
|
||||
// loop through our density intervals and generate a label with a colored square for each interval
|
||||
for (var i = 0; i < grades.length; i++) {
|
||||
div.innerHTML +=
|
||||
'<i style="background:' + getColor(grades[i] + 1) + '"></i> ' +
|
||||
grades[i] + (grades[i + 1] ? '–' + grades[i + 1] + '<br>' : '+');
|
||||
}
|
||||
|
||||
return div;
|
||||
};
|
||||
|
||||
legend.addTo(map);
|
||||
|
||||
CSS styles for the control (we also reuse the `info` class defined earlier):
|
||||
|
||||
{: .css}
|
||||
.legend {
|
||||
line-height: 18px;
|
||||
color: #555;
|
||||
}
|
||||
.legend i {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
float: left;
|
||||
margin-right: 8px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
Enjoy the result on the top of this page, or on a [separate page](example.html).
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 182 KiB |
@@ -0,0 +1,16 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: CRS.Simple example
|
||||
---
|
||||
<script>
|
||||
|
||||
var map = L.map('map', {
|
||||
crs: L.CRS.Simple
|
||||
});
|
||||
|
||||
var bounds = [[0,0], [1000,1000]];
|
||||
var image = L.imageOverlay('uqm_map_full.png', bounds).addTo(map);
|
||||
|
||||
map.fitBounds(bounds);
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: CRS.Simple example
|
||||
---
|
||||
<script>
|
||||
|
||||
var map = L.map('map', {
|
||||
crs: L.CRS.Simple,
|
||||
minZoom: -3
|
||||
});
|
||||
|
||||
var bounds = [[-26.5,-25], [1021.5,1023]];
|
||||
var image = L.imageOverlay('uqm_map_full.png', bounds).addTo(map);
|
||||
|
||||
var sol = L.latLng([ 145, 175 ]);
|
||||
L.marker(sol).addTo(map);
|
||||
|
||||
map.setView( [70, 120], 1);
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: CRS.Simple example
|
||||
---
|
||||
<script>
|
||||
|
||||
var map = L.map('map', {
|
||||
crs: L.CRS.Simple,
|
||||
minZoom: -3
|
||||
});
|
||||
|
||||
var yx = L.latLng;
|
||||
|
||||
var xy = function(x, y) {
|
||||
if (L.Util.isArray(x)) { // When doing xy([x, y]);
|
||||
return yx(x[1], x[0]);
|
||||
}
|
||||
return yx(y, x); // When doing xy(x, y);
|
||||
};
|
||||
|
||||
var bounds = [xy(-25, -26.5), xy(1023, 1021.5)];
|
||||
var image = L.imageOverlay('uqm_map_full.png', bounds).addTo(map);
|
||||
|
||||
var sol = xy(175.2, 145.0);
|
||||
var mizar = xy( 41.6, 130.1);
|
||||
var kruegerZ = xy( 13.4, 56.5);
|
||||
var deneb = xy(218.7, 8.3);
|
||||
|
||||
L.marker( sol).addTo(map).bindPopup( 'Sol');
|
||||
L.marker( mizar).addTo(map).bindPopup( 'Mizar');
|
||||
L.marker(kruegerZ).addTo(map).bindPopup('Krueger-Z');
|
||||
L.marker( deneb).addTo(map).bindPopup( 'Deneb');
|
||||
|
||||
var travel = L.polyline([sol, deneb]).addTo(map);
|
||||
|
||||
map.setView(xy(120, 70), 1);
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
layout: tutorial_v2
|
||||
title: Non-geographical maps
|
||||
---
|
||||
|
||||
<style>
|
||||
iframe {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
## Not of this earth
|
||||
|
||||
Sometimes, maps do not represent things on the surface of the earth and, as such, do not have a concept of geographical latitude and geographical longitude. Most times this refers to big scanned images, such as game maps.
|
||||
|
||||
For this tutorial we've picked a starmap from Star Control II, a game that is now available as the [open-source project The Ur-Quan Masters](https://en.wikipedia.org/wiki/Star_Control_II#The_Ur-Quan_Masters). These maps were made with a [tool to read the open-source data files](http://www.highprogrammer.com/alan/games/video/uqm/index.html) of the game, and look like this:
|
||||
|
||||
<center>
|
||||
<img src="uqm_map_400px.png" style="border: 1px solid #ccc; border-radius: 5px" /><br/>
|
||||
</center>
|
||||
|
||||
The game has a built-in square coordinate system, as can be seen in the corners. This will allow us to establish a coordinate system.
|
||||
|
||||
<center>
|
||||
<img src="uqm_map_detail.png" style="border: 1px solid #ccc; border-radius: 5px" /><br/>
|
||||
</center>
|
||||
|
||||
|
||||
## CRS.Simple
|
||||
|
||||
**CRS** stands for [coordinate reference system](https://en.wikipedia.org/wiki/Spatial_reference_system), a term used by geographers to explain what the coordinates mean in a coordinate vector. For example, `[15, 60]` represents a point in the Indian Ocean if using latitude-longitude on the earth, or the solar system Krueger-Z in our starmap.
|
||||
|
||||
A Leaflet map has one CRS (and *one* CRS *only*), that can be changed when creating the map. For our game map we'll use `CRS.Simple`, which represents a square grid:
|
||||
|
||||
var map = L.map('map', {
|
||||
crs: L.CRS.Simple
|
||||
});
|
||||
|
||||
Then we can just add a `L.ImageOverlay` with the starmap image and its *approximate* bounds:
|
||||
|
||||
var bounds = [[0,0], [1000,1000]];
|
||||
var image = L.imageOverlay('uqm_map_full.png', bounds).addTo(map);
|
||||
|
||||
And show the whole map:
|
||||
|
||||
map.fitBounds(bounds);
|
||||
|
||||
{% include frame.html url="crs-simple-example1.html" %}
|
||||
|
||||
This example doesn't quite work, as we cannot see the whole map after doing a `fitBounds()`.
|
||||
|
||||
|
||||
## Common gotchas in CRS.Simple maps
|
||||
|
||||
In the default Leaflet CRS, `CRS.Earth`, 360 degrees of longitude are mapped to 256 horizontal pixels (at zoom level 0) and approximately 170 degrees of latitude are mapped to 256 vertical pixels (at zoom level 0).
|
||||
|
||||
In a `CRS.Simple`, one horizontal map unit is mapped to one horizontal pixel, and *idem* with vertical. This means that the whole map is about 1000x1000 pixels big and won't fit in our HTML container. Luckily, we can set `minZoom` to values lower than zero:
|
||||
|
||||
var map = L.map('map', {
|
||||
crs: L.CRS.Simple,
|
||||
minZoom: -5
|
||||
});
|
||||
|
||||
### Pixels vs. map units
|
||||
|
||||
One common mistake when using `CRS.Simple` is assuming that the map units equal image pixels. In this case, the map covers 1000x1000 units, but the image is 2315x2315 pixels big. Different cases will call for one pixel = one map unit, or 64 pixels = one map unit, or anything. **Think in map units** in a grid, and then add your layers (`L.ImageOverlay`s, `L.Marker`s and so on) accordingly.
|
||||
|
||||
In fact, the image we're using covers more than 1000 map units - there is a sizable margin. Measuring how many pixels there are between the 0 and 1000 coordinates, and extrapolating, we can have the right coordinate bounds for this image:
|
||||
|
||||
var bounds = [[-26.5,-25], [1021.5,1023]];
|
||||
var image = L.imageOverlay('uqm_map_full.png', bounds).addTo(map);
|
||||
|
||||
While we're at it, let's add some markers:
|
||||
|
||||
var sol = L.latLng([ 145, 175.2 ]);
|
||||
L.marker(sol).addTo(map);
|
||||
map.setView( [70, 120], 1);
|
||||
|
||||
{% include frame.html url="crs-simple-example2.html" %}
|
||||
|
||||
### This is not the `LatLng` you're looking for
|
||||
|
||||
You'll notice that Sol is at coordinates `[145,175]` instead of `[175,145]`, and the same happens with the map center. Coordinates in `CRS.Simple` take the form of `[y, x]` instead of `[x, y]`, in the same way Leaflet uses `[lat, lng]` instead of `[lng, lat]`.
|
||||
|
||||
<small>(In technical terms, Leaflet prefers to use [`[northing, easting]`](https://en.wikipedia.org/wiki/Easting_and_northing) over `[easting, northing]` - the first coordinate in a coordinate pair points "north" and the second points "east")</small>
|
||||
|
||||
The debate about whether `[lng, lat]` or `[lat, lng]` or `[y, x]` or `[x, y]` [is not new, and there is no clear consensus](http://www.macwright.org/lonlat/). This lack of consensus is why Leaflet has a class named `L.LatLng` instead of the more confusion-prone `L.Coordinate`.
|
||||
|
||||
If working with `[y, x]` coordinates with something named `L.LatLng` doesn't make much sense to you, you can easily create wrappers for them:
|
||||
|
||||
var yx = L.latLng;
|
||||
|
||||
var xy = function(x, y) {
|
||||
if (L.Util.isArray(x)) { // When doing xy([x, y]);
|
||||
return yx(x[1], x[0]);
|
||||
}
|
||||
return yx(y, x); // When doing xy(x, y);
|
||||
};
|
||||
|
||||
Now we can add a few stars and even a navigation line with `[x, y]` coordinates:
|
||||
|
||||
var sol = xy(175.2, 145.0);
|
||||
var mizar = xy( 41.6, 130.1);
|
||||
var kruegerZ = xy( 13.4, 56.5);
|
||||
var deneb = xy(218.7, 8.3);
|
||||
|
||||
L.marker( sol).addTo(map).bindPopup( 'Sol');
|
||||
L.marker( mizar).addTo(map).bindPopup( 'Mizar');
|
||||
L.marker(kruegerZ).addTo(map).bindPopup('Krueger-Z');
|
||||
L.marker( deneb).addTo(map).bindPopup( 'Deneb');
|
||||
|
||||
var travel = L.polyline([sol, deneb]).addTo(map);
|
||||
|
||||
The map looks pretty much the same, but the code is a bit more readable:
|
||||
|
||||
{% include frame.html url="crs-simple-example3.html" %}
|
||||
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 890 KiB |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
layout: redirected
|
||||
sitemap: false
|
||||
redirect_to: custom-icons/example.html
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
layout: redirected
|
||||
sitemap: false
|
||||
redirect_to: custom-icons/
|
||||
---
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Custom Icons Tutorial
|
||||
---
|
||||
<script>
|
||||
var map = L.map('map').setView([51.5, -0.09], 13);
|
||||
|
||||
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
var LeafIcon = L.Icon.extend({
|
||||
options: {
|
||||
shadowUrl: 'leaf-shadow.png',
|
||||
iconSize: [38, 95],
|
||||
shadowSize: [50, 64],
|
||||
iconAnchor: [22, 94],
|
||||
shadowAnchor: [4, 62],
|
||||
popupAnchor: [-3, -76]
|
||||
}
|
||||
});
|
||||
|
||||
var greenIcon = new LeafIcon({iconUrl: 'leaf-green.png'});
|
||||
|
||||
L.marker([51.5, -0.09], {icon: greenIcon}).addTo(map);
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Custom Icons Tutorial
|
||||
---
|
||||
<script>
|
||||
var map = L.map('map').setView([51.5, -0.09], 13);
|
||||
|
||||
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
var LeafIcon = L.Icon.extend({
|
||||
options: {
|
||||
shadowUrl: 'leaf-shadow.png',
|
||||
iconSize: [38, 95],
|
||||
shadowSize: [50, 64],
|
||||
iconAnchor: [22, 94],
|
||||
shadowAnchor: [4, 62],
|
||||
popupAnchor: [-3, -76]
|
||||
}
|
||||
});
|
||||
|
||||
var greenIcon = new LeafIcon({iconUrl: 'leaf-green.png'}),
|
||||
redIcon = new LeafIcon({iconUrl: 'leaf-red.png'}),
|
||||
orangeIcon = new LeafIcon({iconUrl: 'leaf-orange.png'});
|
||||
|
||||
L.marker([51.5, -0.09], {icon: greenIcon}).bindPopup("I am a green leaf.").addTo(map);
|
||||
L.marker([51.495, -0.083], {icon: redIcon}).bindPopup("I am a red leaf.").addTo(map);
|
||||
L.marker([51.49, -0.1], {icon: orangeIcon}).bindPopup("I am an orange leaf.").addTo(map);
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
layout: tutorial_v2
|
||||
title: Markers With Custom Icons
|
||||
---
|
||||
|
||||
## Markers With Custom Icons
|
||||
|
||||
In this tutorial, you'll learn how to easily define your own icons for use by the markers you put on the map.
|
||||
|
||||
{% include frame.html url="example.html" %}
|
||||
|
||||
### Preparing the images
|
||||
|
||||
To make a custom icon, we usually need two images --- the actual icon image and the image of its shadow. For this tutorial, we took the Leaflet logo and created four images out of it --- 3 leaf images of different colors and one shadow image for the three:
|
||||
|
||||
<p>
|
||||
<img style="border: 1px solid #ccc" src="leaf-green.png" />
|
||||
<img style="border: 1px solid #ccc" src="leaf-red.png" />
|
||||
<img style="border: 1px solid #ccc" src="leaf-orange.png" />
|
||||
<img style="border: 1px solid #ccc" src="leaf-shadow.png" />
|
||||
</p>
|
||||
|
||||
Note that the white area in the images is actually transparent.
|
||||
|
||||
### Creating an icon
|
||||
|
||||
Marker icons in Leaflet are defined by [L.Icon](../../reference.html#icon) objects, which are passed as an option when creating markers. Let's create a green leaf icon:
|
||||
|
||||
var greenIcon = L.icon({
|
||||
iconUrl: 'leaf-green.png',
|
||||
shadowUrl: 'leaf-shadow.png',
|
||||
|
||||
iconSize: [38, 95], // size of the icon
|
||||
shadowSize: [50, 64], // size of the shadow
|
||||
iconAnchor: [22, 94], // point of the icon which will correspond to marker's location
|
||||
shadowAnchor: [4, 62], // the same for the shadow
|
||||
popupAnchor: [-3, -76] // point from which the popup should open relative to the iconAnchor
|
||||
});
|
||||
|
||||
Now putting a marker with this icon on a map is easy:
|
||||
|
||||
L.marker([51.5, -0.09], {icon: greenIcon}).addTo(map);
|
||||
|
||||
{% include frame.html url="example-one-icon.html" %}
|
||||
|
||||
### Defining an icon class
|
||||
|
||||
What if we need to create several icons that have lots in common? Let's define our own icon class containing the shared options, inheriting from `L.Icon`! It's really easy in Leaflet:
|
||||
|
||||
var LeafIcon = L.Icon.extend({
|
||||
options: {
|
||||
shadowUrl: 'leaf-shadow.png',
|
||||
iconSize: [38, 95],
|
||||
shadowSize: [50, 64],
|
||||
iconAnchor: [22, 94],
|
||||
shadowAnchor: [4, 62],
|
||||
popupAnchor: [-3, -76]
|
||||
}
|
||||
});
|
||||
|
||||
Now we can create all three of our leaf icons from this class and use them:
|
||||
|
||||
var greenIcon = new LeafIcon({iconUrl: 'leaf-green.png'}),
|
||||
redIcon = new LeafIcon({iconUrl: 'leaf-red.png'}),
|
||||
orangeIcon = new LeafIcon({iconUrl: 'leaf-orange.png'});
|
||||
|
||||
You may have noticed that we used the `new` keyword for creating LeafIcon instances. So why do all Leaflet classes get created without it? The answer is simple: the real Leaflet classes are named with a capital letter (e.g. `L.Icon`), and they also need to be created with `new`, but there are also shortcuts with lowercase names (`L.icon`), created for convenience like this:
|
||||
|
||||
L.icon = function (options) {
|
||||
return new L.Icon(options);
|
||||
};
|
||||
|
||||
You can do the same with your classes too. OK, lets finally put some markers with these icons on the map:
|
||||
|
||||
L.marker([51.5, -0.09], {icon: greenIcon}).addTo(map).bindPopup("I am a green leaf.");
|
||||
L.marker([51.495, -0.083], {icon: redIcon}).addTo(map).bindPopup("I am a red leaf.");
|
||||
L.marker([51.49, -0.1], {icon: orangeIcon}).addTo(map).bindPopup("I am an orange leaf.");
|
||||
|
||||
That's it. Now take a look at the [full example](example.html), the [`L.Icon` docs](../../reference.html#icon), or browse [other examples](../../examples.html).
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 236 KiB |
@@ -0,0 +1,37 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: CanvasCircles
|
||||
---
|
||||
<script type='text/javascript'>
|
||||
|
||||
var map = L.map('map', {
|
||||
center: [0, 0],
|
||||
zoom: 0
|
||||
});
|
||||
|
||||
L.GridLayer.CanvasCircles = L.GridLayer.extend({
|
||||
createTile: function (coords) {
|
||||
var tile = document.createElement('canvas');
|
||||
|
||||
var tileSize = this.getTileSize();
|
||||
tile.setAttribute('width', tileSize.x);
|
||||
tile.setAttribute('height', tileSize.y);
|
||||
|
||||
var ctx = tile.getContext('2d');
|
||||
|
||||
// Draw whatever is needed in the canvas context
|
||||
// For example, circles which get bigger as we zoom in
|
||||
ctx.arc(tileSize.x/2, tileSize.x/2, 4 + coords.z*4, 0, 2*Math.PI, false);
|
||||
ctx.fill();
|
||||
|
||||
return tile;
|
||||
}
|
||||
});
|
||||
|
||||
L.gridLayer.canvasCircles = function(opts) {
|
||||
return new L.GridLayer.CanvasCircles(opts);
|
||||
};
|
||||
|
||||
map.addLayer( L.gridLayer.canvasCircles() );
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Leaflet class diagram
|
||||
---
|
||||
<script type='text/javascript'>
|
||||
|
||||
var bounds = [[0,0], [1570,1910]];
|
||||
|
||||
var map = L.map('map', {
|
||||
crs: L.CRS.Simple,
|
||||
maxZoom: 0,
|
||||
minZoom: -4,
|
||||
maxBounds: bounds
|
||||
});
|
||||
|
||||
map.getContainer().style.width = '100vw';
|
||||
map.getContainer().style.height= '100vh';
|
||||
document.body.style.margin = 0;
|
||||
|
||||
var image = L.imageOverlay('class-diagram.png', bounds).addTo(map);
|
||||
|
||||
map.fitBounds(bounds);
|
||||
|
||||
</script>
|
||||
|
After Width: | Height: | Size: 220 KiB |
@@ -0,0 +1,100 @@
|
||||
|
||||
// Class inheritances in a format that
|
||||
// http://www.yuml.me/diagram/class/draw
|
||||
// understands and draws things from
|
||||
|
||||
|
||||
[L.Util]
|
||||
[L.Browser]
|
||||
[L.LatLng|lat;lng]
|
||||
[L.LatLngBounds] has 2 -.-> [L.LatLng]
|
||||
[L.Point|x;y]
|
||||
[L.Bounds] has 2 -.-> [L.Point]
|
||||
[L.DomUtil]
|
||||
[L.DomEvent]
|
||||
|
||||
|
||||
|
||||
[L.Class|options|extend();include();initialize()]
|
||||
|
||||
[L.Class] ^- [L.Control|onAdd();onRemove()]
|
||||
[L.Control] ^- [L.Control.Attribution;L.Control.Layers;L.Control.Scale;L.Control.Zoom]
|
||||
|
||||
// L.Control] ^- L.Control.Attribution]
|
||||
// L.Control] ^- L.Control.Layers]
|
||||
// L.Control] ^- L.Control.Scale]
|
||||
// L.Control] ^- L.Control.Zoom]
|
||||
|
||||
|
||||
[L.Class] ^- [L.Evented]
|
||||
[L.Evented|on();off();fire()]
|
||||
|
||||
|
||||
[L.CRS] ^- [L.CRS.Earth]
|
||||
[L.CRS] ^- [L.CRS.Simple]
|
||||
[L.CRS.Earth] ^- [L.CRS.EPSG3395;L.CRS.EPSG3857;L.CRS.EPSG4326]
|
||||
|
||||
// L.CRS] ^- L.CRS.Earth]
|
||||
// L.CRS] ^- L.CRS.Simple]
|
||||
// L.CRS.Earth] ^- L.CRS.EPSG3395]
|
||||
// L.CRS.Earth] ^- L.CRS.EPSG3857]
|
||||
// L.CRS.EPSG3857] ^- L.CRS.EPSG900913]
|
||||
// L.CRS.Earth] ^- L.CRS.EPSG4326]
|
||||
|
||||
[L.Evented] ^- [L.Layer]
|
||||
|
||||
[L.Layer|onAdd();onRemove();getEvents();getAttribution();beforeAdd()]
|
||||
|
||||
[L.Evented] ^- [L.Map|addHandler();addControl();removeControl();addLayer();removeLayer()]
|
||||
[L.Map] contains -.-> [L.Layer]
|
||||
[L.Map] contains -.-> [L.Control]
|
||||
[L.Map] contains -.-> [L.Handler]
|
||||
[L.Map] has one -.-> [L.CRS]
|
||||
|
||||
[L.Class] ^- [L.Handler|addHooks();removeHooks()]
|
||||
[L.Handler] ^- [L.Map.BoxZoom;L.Map.DoubleClickZoom;L.Map.Drag;L.Map.Keyboard;L.Map.ScrollWheelZoom;L.Map.Tap;L.Map.TouchZoom]
|
||||
|
||||
// L.Handler] ^- L.Map.BoxZoom]
|
||||
// L.Handler] ^- L.Map.DoubleClickZoom]
|
||||
// L.Handler] ^- L.Map.Drag]
|
||||
// L.Handler] ^- L.Map.Keyboard]
|
||||
// L.Handler] ^- L.Map.ScrollWheelZoom]
|
||||
// L.Handler] ^- L.Map.Tap]
|
||||
// L.Handler] ^- L.Map.TouchZoom]
|
||||
|
||||
[L.Layer] ^- [L.Marker]
|
||||
[L.Icon] ^- [L.Icon.Default]
|
||||
[L.Icon] ^- [L.DivIcon]
|
||||
|
||||
[L.Marker] drawn as a -.-> [L.Icon]
|
||||
|
||||
|
||||
[L.Layer] ^- [L.GridLayer|createTile()]
|
||||
[L.GridLayer] ^- [L.TileLayer|getTileUrl()]
|
||||
[L.TileLayer] ^- [L.TileLayer.WMS]
|
||||
|
||||
|
||||
[L.Layer] ^- [L.Renderer]
|
||||
[L.Renderer] ^- [L.Canvas]
|
||||
[L.Renderer] ^- [L.SVG]
|
||||
|
||||
[L.Layer] ^- [L.Path]
|
||||
[L.CircleMarker] ^- [L.Circle]
|
||||
[L.Path] ^- [L.CircleMarker]
|
||||
[L.Path] ^- [L.Polyline]
|
||||
[L.Polyline] ^- [L.Polygon]
|
||||
[L.Polygon] ^- [L.Rectangle]
|
||||
[L.Path] drawn in a -.-> [L.Renderer]
|
||||
|
||||
|
||||
[L.Layer] ^- [L.LayerGroup]
|
||||
[L.LayerGroup] ^- [L.FeatureGroup]
|
||||
[L.FeatureGroup] ^- [L.GeoJSON]
|
||||
|
||||
[L.Layer] ^- [L.DivOverlay]
|
||||
[L.DivOverlay] ^- [L.Popup]
|
||||
[L.DivOverlay] ^- [L.Tooltip]
|
||||
|
||||
[L.Layer] ^- [L.ImageOverlay]
|
||||
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
---
|
||||
layout: tutorial_v2
|
||||
title: Extending Leaflet, Class Theory
|
||||
---
|
||||
|
||||
## Extending Leaflet
|
||||
|
||||
Leaflet has literally hundreds of plugins. These expand the capabilities of Leaflet: sometimes in a generic way, sometimes in a very use-case-specific way.
|
||||
|
||||
Part of the reason there are so many plugins is that Leaflet is easy to extend. This tutorial will cover the most commonly used ways of doing so.
|
||||
|
||||
Please note that this tutorial assumes you have a good grasp of:
|
||||
|
||||
* [JavaScript](https://developer.mozilla.org/en-US/Learn/JavaScript)
|
||||
* [DOM handling](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction)
|
||||
* [Object-oriented programming](https://en.wikipedia.org/wiki/Object-oriented_programming) (understanding concepts like classes, instances, inheritance, methods and properties)
|
||||
|
||||
|
||||
## Leaflet architecture
|
||||
|
||||
Let's have a look at a simplified UML Class diagram for Leaflet 1.0.0. There are more than 60 JavaScript classes, so the diagram is a bit big. Luckily we can make a zoomable image with a `L.ImageOverlay`:
|
||||
|
||||
{% include frame.html url="class-diagram.html" %}
|
||||
|
||||
|
||||
From a technical point of view, Leaflet can be extended in different ways:
|
||||
|
||||
* The most common: creating a new subclass of `L.Layer`, `L.Handler` or `L.Control`, with `L.Class.extend()`
|
||||
* Layers move when the map is moved/zoomed
|
||||
* Handlers are invisible and interpret browser events
|
||||
* Controls are fixed interface elements
|
||||
* Including more functionality in an existing class with `L.Class.include()`
|
||||
* Adding new methods and options
|
||||
* Changing some methods
|
||||
* Using `addInitHook` to run extra constructor code.
|
||||
* Changing parts of an existing class (replacing how a class method works) with `L.Class.include()`.
|
||||
|
||||
This tutorial covers some classes and methods available only in Leaflet 1.0.0. Use caution if you are developing a plugin for a previous version.
|
||||
|
||||
## `L.Class`
|
||||
|
||||
JavaScript is a bit of a weird language. It's not really an object-oriented language, but rather a [prototype-oriented language](https://en.wikipedia.org/wiki/Prototype-based_programming). This has made JavaScript historically difficult to use class inheritance in the classic OOP meaning of the term.
|
||||
|
||||
Leaflet works around this by having `L.Class`, which eases up class inheritance.
|
||||
|
||||
Even though modern JavaScript can use ES6 classes, Leaflet is not designed around them.
|
||||
|
||||
### `L.Class.extend()`
|
||||
|
||||
In order to create a subclass of anything in Leaflet, use the `.extend()` method. This accepts one parameter: a plain object with key-value pairs, each key being the name of a property or method, and each value being the initial value of a property, or the implementation of a method:
|
||||
|
||||
var MyDemoClass = L.Class.extend({
|
||||
|
||||
// A property with initial value = 42
|
||||
myDemoProperty: 42,
|
||||
|
||||
// A method
|
||||
myDemoMethod: function() { return this.myDemoProperty; }
|
||||
|
||||
});
|
||||
|
||||
var myDemoInstance = new MyDemoClass();
|
||||
|
||||
// This will output "42" to the development console
|
||||
console.log( myDemoInstance.myDemoMethod() );
|
||||
|
||||
When naming classes, methods and properties, adhere to the following conventions:
|
||||
|
||||
* Function, method, property and factory names should be in [`lowerCamelCase`](https://en.wikipedia.org/wiki/CamelCase).
|
||||
* Class names should be in [`UpperCamelCase`](https://en.wikipedia.org/wiki/CamelCase).
|
||||
* Private properties and methods start with an underscore (`_`). This doesn't make them private, just recommends developers not to use them directly.
|
||||
|
||||
### `L.Class.include()`
|
||||
|
||||
If a class is already defined, existing properties/methods can be redefined, or new ones can be added by using `.include()`:
|
||||
|
||||
MyDemoClass.include({
|
||||
|
||||
// Adding a new property to the class
|
||||
_myPrivateProperty: 78,
|
||||
|
||||
// Redefining a method
|
||||
myDemoMethod: function() { return this._myPrivateProperty; }
|
||||
|
||||
});
|
||||
|
||||
var mySecondDemoInstance = new MyDemoClass();
|
||||
|
||||
// This will output "78"
|
||||
console.log( mySecondDemoInstance.myDemoMethod() );
|
||||
|
||||
// However, properties and methods from before still exist
|
||||
// This will output "42"
|
||||
console.log( mySecondDemoInstance.myDemoProperty );
|
||||
|
||||
### `L.Class.initialize()`
|
||||
|
||||
In OOP, classes have a constructor method. In Leaflet's `L.Class`, the constructor method is always named `initialize`.
|
||||
|
||||
If your class has some specific `options`, it's a good idea to initialize them with `L.setOptions()` in the constructor. This utility function will merge the provided options with the default options of the class.
|
||||
|
||||
|
||||
var MyBoxClass = L.Class.extend({
|
||||
|
||||
options: {
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
|
||||
initialize: function(name, options) {
|
||||
this.name = name;
|
||||
L.setOptions(this, options);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var instance = new MyBoxClass('Red', {width: 10});
|
||||
|
||||
console.log(instance.name); // Outputs "Red"
|
||||
console.log(instance.options.width); // Outputs "10"
|
||||
console.log(instance.options.height); // Outputs "1", the default
|
||||
|
||||
Leaflet handles the `options` property in a special way: options available for a parent class will be inherited by a children class:.
|
||||
|
||||
var MyCubeClass = MyBoxClass.extend({
|
||||
options: {
|
||||
depth: 1
|
||||
}
|
||||
});
|
||||
|
||||
var instance = new MyCubeClass('Blue');
|
||||
|
||||
console.log(instance.options.width);
|
||||
console.log(instance.options.height);
|
||||
console.log(instance.options.depth);
|
||||
|
||||
|
||||
It's quite common for child classes to run the parent's constructor, and then their own constructor. In Leaflet this is achieved using `L.Class.addInitHook()`. This method can be used to "hook" initialization functions that run right after the class' `initialize()`, for example:
|
||||
|
||||
MyBoxClass.addInitHook(function(){
|
||||
this._area = this.options.width * this.options.length;
|
||||
});
|
||||
|
||||
That will run after `initialize()` is called (which calls `setOptions()`). This means that `this.options` exist and is valid when the init hook runs.
|
||||
|
||||
`addInitHook` has an alternate syntax, which uses method names and can fill method arguments in:
|
||||
|
||||
MyCubeClass.include({
|
||||
_calculateVolume: function(arg1, arg2) {
|
||||
this._volume = this.options.width * this.options.length * this.options.depth;
|
||||
}
|
||||
});
|
||||
|
||||
MyCubeClass.addInitHook('_calculateVolume', argValue1, argValue2);
|
||||
|
||||
|
||||
### Methods of the parent class
|
||||
|
||||
Calling a method of a parent class is achieved by reaching into the prototype of the parent class and using [`Function.call(…)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call). This can be seen, for example, in the code for `L.FeatureGroup`:
|
||||
|
||||
L.FeatureGroup = L.LayerGroup.extend({
|
||||
|
||||
addLayer: function (layer) {
|
||||
…
|
||||
L.LayerGroup.prototype.addLayer.call(this, layer);
|
||||
},
|
||||
|
||||
removeLayer: function (layer) {
|
||||
…
|
||||
L.LayerGroup.prototype.removeLayer.call(this, layer);
|
||||
},
|
||||
|
||||
…
|
||||
});
|
||||
|
||||
Calling the parent's constructor is done in a similar way, but using `ParentClass.prototype.initialize.call(this, …)` instead.
|
||||
|
||||
|
||||
### Factories
|
||||
|
||||
Most Leaflet classes have a corresponding [factory function](https://en.wikipedia.org/wiki/Factory_%28object-oriented_programming%29). A factory function has the same name as the class, but in `lowerCamelCase` instead of `UpperCamelCase`:
|
||||
|
||||
function myBoxClass(name, options) {
|
||||
return new MyBoxClass(name, options);
|
||||
}
|
||||
|
||||
|
||||
### Naming conventions
|
||||
|
||||
When naming classes for Leaflet plugins, please adhere to the following naming conventions:
|
||||
|
||||
* Never expose global variables in your plugin.
|
||||
* If you have a new class, put it directly in the `L` namespace (`L.MyPlugin`).
|
||||
* If you inherit one of the existing classes, make it a sub-property (`L.TileLayer.Banana`).
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
---
|
||||
layout: tutorial_v2
|
||||
title: Extending Leaflet, New Layers
|
||||
---
|
||||
|
||||
<br>
|
||||
|
||||
This tutorial assumes you've read the [theory of Leaflet class inheritance](./extending-1-classes.html).
|
||||
|
||||
In Leaflet, a "layer" is anything that moves around when the map is moved around. Before seeing how to create them from scratch, it's easier to explain how to do simple extensions.
|
||||
|
||||
## "Extension methods"
|
||||
|
||||
A few of the Leaflet classes have so-called "extension methods": entry points for writing code for sub-classes.
|
||||
|
||||
One of them is `L.TileLayer.getTileUrl()`. This method is called internally by `L.TileLayer` whenever a new tile needs to know which image to load. By making a subclass of `L.TileLayer` and rewriting its `getTileUrl()` function, we can create custom behaviour.
|
||||
|
||||
Let's illustrate with a custom `L.TileLayer` that will display random kitten images from [PlaceKitten]():
|
||||
|
||||
L.TileLayer.Kitten = L.TileLayer.extend({
|
||||
getTileUrl: function(coords) {
|
||||
var i = Math.ceil( Math.random() * 4 );
|
||||
return "http://placekitten.com/256/256?image=" + i;
|
||||
},
|
||||
getAttribution: function() {
|
||||
return "<a href='http://placekitten.com/attribution.html'>PlaceKitten</a>"
|
||||
}
|
||||
});
|
||||
|
||||
L.tileLayer.kitten = function() {
|
||||
return new L.TileLayer.Kitten();
|
||||
}
|
||||
|
||||
L.tileLayer.kitten().addTo(map);
|
||||
|
||||
{% include frame.html url="kittenlayer.html" %}
|
||||
|
||||
Normally, `getTileLayer()` receives the tile coordinates (as `coords.x`, `coords.y` and `coords.z`) and generates a tile URL from them. In our example, we ignore those and simply use a random number to get a different kitten every time.
|
||||
|
||||
### Splitting away the plugin code
|
||||
|
||||
In the previous example, `L.TileLayer.Kitten` is defined in the same place as it's used. For plugins, it's better to split the plugin code into its own file, and include that file when it's used.
|
||||
|
||||
For the KittenLayer, you should create a file like `L.KittenLayer.js` with:
|
||||
|
||||
L.TileLayer.Kitten = L.TileLayer.extend({
|
||||
getTileUrl: function(coords) {
|
||||
var i = Math.ceil( Math.random() * 4 );
|
||||
return "http://placekitten.com/256/256?image=" + i;
|
||||
},
|
||||
getAttribution: function() {
|
||||
return "<a href='http://placekitten.com/attribution.html'>PlaceKitten</a>"
|
||||
}
|
||||
});
|
||||
|
||||
And then, include that file when showing a map:
|
||||
|
||||
<html>
|
||||
…
|
||||
<script src='leaflet.js'>
|
||||
<script src='L.KittenLayer.js'>
|
||||
<script>
|
||||
var map = L.map('map-div-id');
|
||||
L.tileLayer.kitten().addTo(map);
|
||||
</script>
|
||||
…
|
||||
|
||||
|
||||
### `L.GridLayer` and DOM elements
|
||||
|
||||
Another extension method is `L.GridLayer.createTile()`. Where `L.TileLayer` assumes that there is a grid of images (as `<img>` elements), `L.GridLayer` doesn't assume that - it allows creating grids of any kind of [HTML Elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Element).
|
||||
|
||||
`L.GridLayer` allows creating grids of `<img>`s, but grids of [`<div>`s](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div), [`<canvas>`es](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas) or [`<picture>`s](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture) (or anything) are possible. `createTile()` just has to return an instance of [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) given the tile coordinates. Knowing how to manipulate elements in the [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction) is important here: Leaflet expects instances of `HTMLElement`, so elements created with libraries like jQuery will be problematic.
|
||||
|
||||
An example of a custom `GridLayer` is showing the tile coordinates in a `<div>`. This is particularly useful when debugging the internals of Leaflet, and for understanding how the tile coordinates work:
|
||||
|
||||
L.GridLayer.DebugCoords = L.GridLayer.extend({
|
||||
createTile: function (coords) {
|
||||
var tile = document.createElement('div');
|
||||
tile.innerHTML = [coords.x, coords.y, coords.z].join(', ');
|
||||
tile.style.outline = '1px solid red';
|
||||
return tile;
|
||||
}
|
||||
});
|
||||
|
||||
L.gridLayer.debugCoords = function(opts) {
|
||||
return new L.GridLayer.DebugCoords(opts);
|
||||
};
|
||||
|
||||
map.addLayer( L.gridLayer.debugCoords() );
|
||||
|
||||
|
||||
If the element has to do some asynchronous initialization, then use the second function parameter `done` and call it back when the tile is ready (for example, when an image has been fully loaded) or when there is an error. In here, we'll just delay the tiles artificially:
|
||||
|
||||
createTile: function (coords, done) {
|
||||
var tile = document.createElement('div');
|
||||
tile.innerHTML = [coords.x, coords.y, coords.z].join(', ');
|
||||
tile.style.outline = '1px solid red';
|
||||
|
||||
setTimeout(function () {
|
||||
done(null, tile); // Syntax is 'done(error, tile)'
|
||||
}, 500 + Math.random() * 1500);
|
||||
|
||||
return tile;
|
||||
}
|
||||
|
||||
{% include frame.html url="gridcoords.html" %}
|
||||
|
||||
With these custom `GridLayer`s, a plugin can have full control of the HTML elements that make up the grid. A few plugins already use `<canvas>`es in this way to do advanced rendering.
|
||||
|
||||
A very basic `<canvas>` `GridLayer` looks like:
|
||||
|
||||
L.GridLayer.CanvasCircles = L.GridLayer.extend({
|
||||
createTile: function (coords) {
|
||||
var tile = document.createElement('canvas');
|
||||
|
||||
var tileSize = this.getTileSize();
|
||||
tile.setAttribute('width', tileSize.x);
|
||||
tile.setAttribute('height', tileSize.y);
|
||||
|
||||
var ctx = tile.getContext('2d');
|
||||
|
||||
// Draw whatever is needed in the canvas context
|
||||
// For example, circles which get bigger as we zoom in
|
||||
ctx.beginPath();
|
||||
ctx.arc(tileSize.x/2, tileSize.x/2, 4 + coords.z*4, 0, 2*Math.PI, false);
|
||||
ctx.fill();
|
||||
|
||||
return tile;
|
||||
}
|
||||
});
|
||||
|
||||
{% include frame.html url="canvascircles.html" %}
|
||||
|
||||
|
||||
## The pixel origin
|
||||
|
||||
Creating custom `L.Layer`s is possible, but needs a deeper knowledge of how Leaflet positions HTML elements. The abridged version is:
|
||||
|
||||
* The `L.Map` container has "map panes", which are `<div>`s.
|
||||
* `L.Layer`s are HTML elements inside a map pane
|
||||
* The map transforms all `LatLng`s to coordinates in the map's CRS, and from that into absolute "pixel coordinates" (the origin of the CRS is the same as the origin of the pixel coordinates)
|
||||
* When the `L.Map` is ready (has a center `LatLng` and a zoom level), the absolute pixel coordinates of the top-left corner become the "pixel origin"
|
||||
* Each `L.Layer` is offset from its map pane according to the pixel origin and the absolute pixel coordinates of the layer's `LatLng`s
|
||||
* The pixel origin is reset after each `zoomend` or `viewreset` event on the `L.Map`, and every `L.Layer` has to recalculate its position (if needed)
|
||||
* The pixel origin is *not* reset when panning the map around; instead, the whole panes are repositioned.
|
||||
|
||||
This might be a bit overwhelming, so consider the following explanatory map:
|
||||
|
||||
{% include frame.html url="pixelorigin.html" %}
|
||||
|
||||
The CRS origin (green) stays in the same `LatLng`. The pixel origin (red) always starts at the top-left corner. The pixel origin moves around when the map is panned (map panes are repositioned relative to the map's container), and stays in the same place in the screen when zooming (map panes are *not* repositioned, but layers might redraw themselves). The absolute pixel coordinate to the pixel origin is updated when zooming, but is not updated when panning. Note how the absolute pixel coordinates (the distance to the green bracket) double every time the map is zoomed in.
|
||||
|
||||
To position anything (for example, a blue `L.Marker`), its `LatLng` is converted to an absolute pixel coordinate inside the map's `L.CRS`. Then the absolute pixel coordinate of the pixel origin is subtracted from its absolute pixel coordinate, giving an offset relative to the pixel origin (light blue). As the pixel origin is the top-left corner of all map panes, this offset can be applied to the HTML element of the marker's icon. The marker's `iconAnchor` (dark blue line) is achieved via negative CSS margins.
|
||||
|
||||
The `L.Map.project()` and `L.Map.unproject()` methods operate with these absolute pixel coordinates. Likewise, `L.Map.latLngToLayerPoint()` and `L.Map.layerPointToLatLng()` work with the offset relative to the pixel origin.
|
||||
|
||||
Different layers apply these calculations in different ways. `L.Marker`s simply reposition their icons; `L.GridLayer`s calculate the bounds of the map (in absolute pixel coordinates) and then calculate the list of tile coordinates to request; vector layers (polylines, polygons, circle markers, etc) transform each `LatLng` to pixels and draw the geometries using SVG or `<canvas>`.
|
||||
|
||||
|
||||
### `onAdd` and `onRemove`
|
||||
|
||||
At their core, all `L.Layer`s are HTML elements inside a map pane, their positions and contents defined by the layer's code. However, HTML elements cannot be created when a layer is instantiated; rather, this is done when the layer is added to the map - the layer doesn't know about the map (or even about the `document`) until then.
|
||||
|
||||
In other words: the map calls the `onAdd()` method of the layer, then the layer creates its HTML element(s) (commonly named 'container' element) and adds them to the map pane. Conversely, when the layer is removed from the map, its `onRemove()` method is called. The layer must update its contents when added to the map, and reposition them when the map view is updated. A layer skeleton looks like:
|
||||
|
||||
L.CustomLayer = L.Layer.extend({
|
||||
onAdd: function(map) {
|
||||
var pane = map.getPane(this.options.pane);
|
||||
this._container = L.DomUtil.create(…);
|
||||
|
||||
pane.appendChild(this._container);
|
||||
|
||||
// Calculate initial position of container with `L.Map.latLngToLayerPoint()`, `getPixelOrigin()` and/or `getPixelBounds()`
|
||||
|
||||
L.DomUtil.setPosition(this._container, point);
|
||||
|
||||
// Add and position children elements if needed
|
||||
|
||||
map.on('zoomend viewreset', this._update, this);
|
||||
},
|
||||
|
||||
onRemove: function(map) {
|
||||
L.DomUtil.remove(this._container);
|
||||
map.off('zoomend viewreset', this._update, this);
|
||||
},
|
||||
|
||||
_update: function() {
|
||||
// Recalculate position of container
|
||||
|
||||
L.DomUtil.setPosition(this._container, point);
|
||||
|
||||
// Add/remove/reposition children elements if needed
|
||||
}
|
||||
});
|
||||
|
||||
How to exactly position the HTML elements for a layer depends on the specifics of the layer, but this introduction should help you to read Leaflet's layer code, and create new layers.
|
||||
|
||||
### Using the parent's `onAdd`
|
||||
|
||||
Some use cases don't need the whole `onAdd` code to be recreated, but instead the code for the parent can be reused, then some specifics can be added before _or_ after that initialization (as needed).
|
||||
|
||||
To give an example, we can have a subclass of `L.Polyline` that will always be red (ignoring the options), like:
|
||||
|
||||
L.Polyline.Red = L.Polyline.extend({
|
||||
onAdd: function(map) {
|
||||
this.options.color = 'red';
|
||||
L.Polyline.prototype.onAdd.call(this, map);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
layout: tutorial_v2
|
||||
title: Extending Leaflet, New Handlers and Controls
|
||||
---
|
||||
|
||||
<br>
|
||||
|
||||
This tutorial assumes you've read the [theory of Leaflet class inheritance](./extending-1-classes.html).
|
||||
|
||||
In Leaflet, a "layer" is anything that moves with the map. In contraposition to that, a "control" is a HTML element that remains static relative to the map container, and a "handler" is a piece of invisible code that changes the map's behaviour.
|
||||
|
||||
## Handlers
|
||||
|
||||
Map handlers are a new concept in Leaflet 1.0, and their function is to process DOM events from the browser (like `click`, `dblclick` or `mousewheel`) and change the state of the map.
|
||||
|
||||
Handlers are relatively simple: they just need a `addHooks()` method (which runs when the handler is enabled in a map) and a `removeHooks()`, which runs when the handler is disabled. A skeleton for handlers is:
|
||||
|
||||
L.CustomHandler = L.Handler.extend({
|
||||
addHooks: function() {
|
||||
L.DomEvent.on(document, 'eventname', this._doSomething, this);
|
||||
},
|
||||
|
||||
removeHooks: function() {
|
||||
L.DomEvent.off(document, 'eventname', this._doSomething, this);
|
||||
},
|
||||
|
||||
_doSomething: function(event) { … }
|
||||
});
|
||||
|
||||
This can be illustrated with a simple handler to pan the map when a mobile device is tilted, through [`deviceorientation` events](https://developer.mozilla.org/en-US/docs/Web/API/Detecting_device_orientation):
|
||||
|
||||
L.TiltHandler = L.Handler.extend({
|
||||
addHooks: function() {
|
||||
L.DomEvent.on(window, 'deviceorientation', this._tilt, this);
|
||||
},
|
||||
|
||||
removeHooks: function() {
|
||||
L.DomEvent.off(window, 'deviceorientation', this._tilt, this);
|
||||
},
|
||||
|
||||
_tilt: function(ev) {
|
||||
// Treat Gamma angle as horizontal pan (1 degree = 1 pixel) and Beta angle as vertical pan
|
||||
this._map.panBy( L.point( ev.gamma, ev.beta ) );
|
||||
}
|
||||
});
|
||||
|
||||
The handler can be attached to the map using `map.addHandler('tilt', L.TiltHandler)` - this will store an instance of `L.TiltHandler` as `map.tilt`. However, it's more usual to attach handlers to all maps with the `addInitHook` syntax:
|
||||
|
||||
L.Map.addInitHook('addHandler', 'tilt', L.TiltHandler);
|
||||
|
||||
Our handler can now be enabled by running `map.tilt.enable()` and disabled by `map.tilt.disable()`
|
||||
|
||||
Moreover, if the map has a property named the same as the handler, then that handler will be enabled by default if that options is `true`, so this will enable our handler by default:
|
||||
|
||||
var map = L.map('mapDiv', { tilt: true });
|
||||
|
||||
To see this example, you'll need a mobile browser which [supports the `deviceorientation` event](http://caniuse.com/#search=deviceorientation) - and even so, this event is particularly flaky and ill-specified, so beware.
|
||||
|
||||
{% include frame.html url="tilt.html" %}
|
||||
|
||||
Depending on the type of event, a map handler can attach event listeners to the `document`, the `window`, or the container of the `L.Map` it's attached to.
|
||||
|
||||
## Controls
|
||||
|
||||
You already know controls - the zoom control in the top left corner, the scale at the bottom left, the layer switcher at the top right. At their core, an `L.Control` is an HTML Element that is at a static position in the map container.
|
||||
|
||||
To make a control, simply inherit from `L.Control` and implement `onAdd()` and `onRemove()`. These methods work in a similar way to their `L.Layer` counterparts (they run whenever the control is added to or removed from the map), except that `onAdd()` must return an instance of `HTMLElement` representing the control. Adding the element to the map is done automatically, and so is removing it.
|
||||
|
||||
The simplest example of a custom control would be a watermark, which is just an image:
|
||||
|
||||
L.Control.Watermark = L.Control.extend({
|
||||
onAdd: function(map) {
|
||||
var img = L.DomUtil.create('img');
|
||||
|
||||
img.src = '../../docs/images/logo.png';
|
||||
img.style.width = '200px';
|
||||
|
||||
return img;
|
||||
},
|
||||
|
||||
onRemove: function(map) {
|
||||
// Nothing to do here
|
||||
}
|
||||
});
|
||||
|
||||
L.control.watermark = function(opts) {
|
||||
return new L.Control.Watermark(opts);
|
||||
}
|
||||
|
||||
L.control.watermark({ position: 'bottomleft' }).addTo(map);
|
||||
|
||||
{% include frame.html url="watermark.html" %}
|
||||
|
||||
If your custom control has interactive elements such as clickable buttons, remember to use `L.DomEvent.on()` inside `onAdd()` and `L.DomEvent.off()` inside `onRemove()`.
|
||||
|
||||
If your custom control consists of more than one HTML element (like `L.Control.Zoom`, which has two buttons), you'll have to create the whole hierarchy of elements and return the topmost container.
|
||||
|
||||
## Publishing your plugin
|
||||
|
||||
If you have understood everything so far, you're ready to make som Leaflet plugins! But make sure to read the [`PLUGIN-GUIDE.md` file](https://github.com/Leaflet/Leaflet/blob/master/PLUGIN-GUIDE.md), as it contains some tips and good practices about naming and publishing your plugin.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Grid coordinates
|
||||
---
|
||||
<script type='text/javascript'>
|
||||
|
||||
var map = L.map('map', {
|
||||
center: [0, 0],
|
||||
zoom: 0
|
||||
});
|
||||
|
||||
L.GridLayer.DebugCoords = L.GridLayer.extend({
|
||||
createTile: function (coords, done) {
|
||||
var tile = document.createElement('div');
|
||||
tile.innerHTML = [coords.x, coords.y, coords.z].join(', ');
|
||||
tile.style.outline = '1px solid red';
|
||||
|
||||
setTimeout(function () {
|
||||
done(null, tile); // Syntax is 'done(error, tile)'
|
||||
}, 500 + Math.random() * 1500);
|
||||
|
||||
return tile;
|
||||
}
|
||||
});
|
||||
|
||||
L.gridLayer.debugCoords = function(opts) {
|
||||
return new L.GridLayer.DebugCoords(opts);
|
||||
};
|
||||
|
||||
map.addLayer( L.gridLayer.debugCoords() );
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: KittenLayer
|
||||
---
|
||||
<script type='text/javascript'>
|
||||
|
||||
var map = L.map('map', {
|
||||
crs: L.CRS.Simple,
|
||||
center: [0, 0],
|
||||
zoom: 5
|
||||
});
|
||||
|
||||
L.TileLayer.Kitten = L.TileLayer.extend({
|
||||
getTileUrl: function(coords) {
|
||||
var i = Math.ceil( Math.random() * 4 );
|
||||
return "http://placekitten.com/256/256?image=" + i;
|
||||
},
|
||||
getAttribution: function() {
|
||||
return "<a href='http://placekitten.com/attribution.html'>PlaceKitten</a>"
|
||||
}
|
||||
});
|
||||
|
||||
L.tileLayer.kitten = function() {
|
||||
return new L.TileLayer.Kitten();
|
||||
}
|
||||
|
||||
map.addLayer( L.tileLayer.kitten() );
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Grid coordinates
|
||||
---
|
||||
<style>
|
||||
|
||||
#info {
|
||||
position:absolute;
|
||||
top:0;
|
||||
right:0;
|
||||
width: 20em;
|
||||
height: 7.5em;
|
||||
background: rgba(255,255,255,.5);
|
||||
z-index:500;
|
||||
font: 12px Sans;
|
||||
}
|
||||
|
||||
.crsMarker {
|
||||
border-top: 2px green solid;
|
||||
border-left: 2px green solid;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id='info' style=''></div>
|
||||
|
||||
|
||||
<script type='text/javascript'>
|
||||
|
||||
var trd = [63.41, 10.41];
|
||||
|
||||
|
||||
var map = L.map('map', {
|
||||
center: [40, 0],
|
||||
zoom: 1
|
||||
});
|
||||
|
||||
var positron = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {
|
||||
attribution: "CartoDB"
|
||||
}).addTo(map);
|
||||
|
||||
var marker = L.marker(trd).addTo(map);
|
||||
|
||||
var pane = map.getPane('markerPane')
|
||||
|
||||
var paneCorner = document.createElement('div');
|
||||
paneCorner.style.width = '12px';
|
||||
paneCorner.style.height = '12px';
|
||||
paneCorner.style.borderTop = '2px red solid';
|
||||
paneCorner.style.borderLeft = '2px red solid';
|
||||
|
||||
pane.appendChild(paneCorner);
|
||||
|
||||
marker._icon.style.border = '1px solid blue';
|
||||
|
||||
var crsMarker = L.marker( map.unproject([0, 0]), {
|
||||
icon: L.divIcon({
|
||||
className: 'crsMarker',
|
||||
iconAnchor: [0, 0]
|
||||
})
|
||||
} ).addTo(map);
|
||||
|
||||
|
||||
var markerOffsetLine = L.polyline([[0, 0], [0, 0]], {color: 'skyblue'}).addTo(map);
|
||||
var iconOffsetLine = L.polyline([[0, 0], [0, 0]], {color: 'blue'}).addTo(map);
|
||||
|
||||
function info() {
|
||||
|
||||
var pixelOrigin = map.getPixelOrigin();
|
||||
var markerPixelCoords = map.project(trd, map.getZoom());
|
||||
var markerAnchor = marker.options.icon.options.iconAnchor;
|
||||
var markerOffset = marker._icon._leaflet_pos;
|
||||
|
||||
document.getElementById('info').innerHTML =
|
||||
'<div style="color: green">CRS origin: 0,0</div>' +
|
||||
'<div style="color: red">px origin: Δ' + pixelOrigin.x + ',' + pixelOrigin.y + '</div>' +
|
||||
'<div style="color: blue">marker px coords:' + markerPixelCoords.x.toFixed(2) + ',' + markerPixelCoords.y.toFixed(2) + '</div>' +
|
||||
'<div style="color: blue">marker anchor: Δ' + markerAnchor[0] + ',' + markerAnchor[1] + '</div>' +
|
||||
'<div style="color: skyblue">marker pane offset: Δ' + markerOffset.x + ',' + markerOffset.y + '</div>';
|
||||
|
||||
markerOffsetLine.setLatLngs([ map.unproject(pixelOrigin), map.unproject(pixelOrigin.add(markerOffset))]);
|
||||
iconOffsetLine.setLatLngs([ map.unproject(pixelOrigin.add(markerOffset)), map.unproject(pixelOrigin.add(markerOffset).subtract(markerAnchor))]);
|
||||
}
|
||||
|
||||
|
||||
map.on('load move moveend zoomend viewreset', info)
|
||||
|
||||
info();
|
||||
|
||||
|
||||
</script>
|
||||
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 431 KiB |
|
After Width: | Height: | Size: 96 KiB |
@@ -0,0 +1,59 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Tilt handler
|
||||
---
|
||||
<style>
|
||||
|
||||
#info {
|
||||
position:absolute;
|
||||
top:0;
|
||||
right:0;
|
||||
width: 20em;
|
||||
height: 7.5em;
|
||||
background: rgba(255,255,255,.5);
|
||||
z-index:500;
|
||||
font: 12px Sans;
|
||||
}
|
||||
|
||||
.crsMarker {
|
||||
border-top: 2px green solid;
|
||||
border-left: 2px green solid;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id='info' style=''></div>
|
||||
|
||||
|
||||
<script type='text/javascript'>
|
||||
|
||||
var trd = [63.41, 10.41];
|
||||
|
||||
L.TiltHandler = L.Handler.extend({
|
||||
addHooks: function() {
|
||||
L.DomEvent.on(window, 'deviceorientation', this._tilt, this);
|
||||
},
|
||||
|
||||
removeHooks: function() {
|
||||
L.DomEvent.off(window, 'deviceorientation', this._tilt, this);
|
||||
},
|
||||
|
||||
_tilt: function(ev) {
|
||||
// Treat Gamma angle as horizontal pan (1 degree = 1 pixel) and Beta angle as vertical pan
|
||||
this._map.panBy( L.point( ev.gamma, ev.beta ) );
|
||||
document.getElementById('info').innerHTML = ev.gamma + ',' + ev.beta;
|
||||
}
|
||||
});
|
||||
|
||||
L.Map.addInitHook('addHandler', 'tilt', L.TiltHandler);
|
||||
|
||||
var map = L.map('map', {
|
||||
center: [0, 0],
|
||||
zoom: 1,
|
||||
tilt: true
|
||||
});
|
||||
|
||||
var positron = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {
|
||||
attribution: "CartoDB"
|
||||
}).addTo(map);
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Watermark control
|
||||
---
|
||||
<script type='text/javascript'>
|
||||
var map = L.map('map', {
|
||||
center: [40, 0],
|
||||
zoom: 1
|
||||
});
|
||||
|
||||
var positron = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {
|
||||
attribution: "CartoDB"
|
||||
}).addTo(map);
|
||||
|
||||
L.Control.Watermark = L.Control.extend({
|
||||
onAdd: function(map) {
|
||||
var img = L.DomUtil.create('img');
|
||||
|
||||
img.src = '../../docs/images/logo.png';
|
||||
img.style.width = '200px';
|
||||
|
||||
return img;
|
||||
},
|
||||
|
||||
onRemove: function(map) {
|
||||
// Nothing to do here
|
||||
}
|
||||
});
|
||||
|
||||
L.control.watermark = function(opts) {
|
||||
return new L.Control.Watermark(opts);
|
||||
}
|
||||
|
||||
L.control.watermark({ position: 'bottomleft' }).addTo(map);
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
layout: redirected
|
||||
sitemap: false
|
||||
redirect_to: geojson/example.html
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
layout: redirected
|
||||
sitemap: false
|
||||
redirect_to: geojson/
|
||||
---
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,78 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: GeoJSON tutorial
|
||||
---
|
||||
<script src="sample-geojson.js" type="text/javascript"></script>
|
||||
|
||||
<script>
|
||||
var map = L.map('map').setView([39.74739, -105], 13);
|
||||
|
||||
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
|
||||
maxZoom: 18,
|
||||
attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
|
||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
|
||||
'Imagery © <a href="http://mapbox.com">Mapbox</a>',
|
||||
id: 'mapbox.light'
|
||||
}).addTo(map);
|
||||
|
||||
var baseballIcon = L.icon({
|
||||
iconUrl: 'baseball-marker.png',
|
||||
iconSize: [32, 37],
|
||||
iconAnchor: [16, 37],
|
||||
popupAnchor: [0, -28]
|
||||
});
|
||||
|
||||
function onEachFeature(feature, layer) {
|
||||
var popupContent = "<p>I started out as a GeoJSON " +
|
||||
feature.geometry.type + ", but now I'm a Leaflet vector!</p>";
|
||||
|
||||
if (feature.properties && feature.properties.popupContent) {
|
||||
popupContent += feature.properties.popupContent;
|
||||
}
|
||||
|
||||
layer.bindPopup(popupContent);
|
||||
}
|
||||
|
||||
L.geoJSON([bicycleRental, campus], {
|
||||
|
||||
style: function (feature) {
|
||||
return feature.properties && feature.properties.style;
|
||||
},
|
||||
|
||||
onEachFeature: onEachFeature,
|
||||
|
||||
pointToLayer: function (feature, latlng) {
|
||||
return L.circleMarker(latlng, {
|
||||
radius: 8,
|
||||
fillColor: "#ff7800",
|
||||
color: "#000",
|
||||
weight: 1,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.8
|
||||
});
|
||||
}
|
||||
}).addTo(map);
|
||||
|
||||
L.geoJSON(freeBus, {
|
||||
|
||||
filter: function (feature, layer) {
|
||||
if (feature.properties) {
|
||||
// If the property "underConstruction" exists and is true, return false (don't render features under construction)
|
||||
return feature.properties.underConstruction !== undefined ? !feature.properties.underConstruction : true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
onEachFeature: onEachFeature
|
||||
}).addTo(map);
|
||||
|
||||
var coorsLayer = L.geoJSON(coorsField, {
|
||||
|
||||
pointToLayer: function (feature, latlng) {
|
||||
return L.marker(latlng, {icon: baseballIcon});
|
||||
},
|
||||
|
||||
onEachFeature: onEachFeature
|
||||
}).addTo(map);
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Leaflet GeoJSON Example</title>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@{{ site.latest_leaflet_version}}/dist/leaflet.css" integrity="{{site.integrity_hash_css}}" crossorigin=""/>
|
||||
<script src="https://unpkg.com/leaflet@{{ site.latest_leaflet_version}}/dist/leaflet.js" integrity="{{site.integrity_hash_uglified}}" crossorigin=""></script></head>
|
||||
<body>
|
||||
<div id="map" style="width: 600px; height: 400px"></div>
|
||||
|
||||
<script src="sample-geojson.js"></script>
|
||||
|
||||
<script>
|
||||
var map = L.map('map').setView([39.74739, -105], 13);
|
||||
|
||||
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
|
||||
maxZoom: 18,
|
||||
attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
|
||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
|
||||
'Imagery © <a href="http://mapbox.com">Mapbox</a>',
|
||||
id: 'mapbox.light'
|
||||
}).addTo(map);
|
||||
|
||||
var baseballIcon = L.icon({
|
||||
iconUrl: 'baseball-marker.png',
|
||||
iconSize: [32, 37],
|
||||
iconAnchor: [16, 37],
|
||||
popupAnchor: [0, -28]
|
||||
});
|
||||
|
||||
function onEachFeature(feature, layer) {
|
||||
var popupContent = "<p>I started out as a GeoJSON " +
|
||||
feature.geometry.type + ", but now I'm a Leaflet vector!</p>";
|
||||
|
||||
if (feature.properties && feature.properties.popupContent) {
|
||||
popupContent += feature.properties.popupContent;
|
||||
}
|
||||
|
||||
layer.bindPopup(popupContent);
|
||||
}
|
||||
|
||||
L.geoJson([bicycleRental, campus], {
|
||||
|
||||
style: function (feature) {
|
||||
return feature.properties && feature.properties.style;
|
||||
},
|
||||
|
||||
onEachFeature: onEachFeature,
|
||||
|
||||
pointToLayer: function (feature, latlng) {
|
||||
return L.circleMarker(latlng, {
|
||||
radius: 8,
|
||||
fillColor: "#ff7800",
|
||||
color: "#000",
|
||||
weight: 1,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.8
|
||||
});
|
||||
}
|
||||
}).addTo(map);
|
||||
|
||||
L.geoJson(freeBus, {
|
||||
|
||||
filter: function (feature, layer) {
|
||||
if (feature.properties) {
|
||||
// If the property "underConstruction" exists and is true, return false (don't render features under construction)
|
||||
return feature.properties.underConstruction !== undefined ? !feature.properties.underConstruction : true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
onEachFeature: onEachFeature
|
||||
}).addTo(map);
|
||||
|
||||
var coorsLayer = L.geoJson(coorsField, {
|
||||
|
||||
pointToLayer: function (feature, latlng) {
|
||||
return L.marker(latlng, {icon: baseballIcon});
|
||||
},
|
||||
|
||||
onEachFeature: onEachFeature
|
||||
}).addTo(map);
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,283 @@
|
||||
---
|
||||
layout: tutorial
|
||||
title: Using GeoJSON with Leaflet
|
||||
---
|
||||
|
||||
<h3>Using GeoJSON with Leaflet</h3>
|
||||
|
||||
<p>GeoJSON is becoming a very popular data format among many GIS technologies and services — it's simple, lightweight, straightforward, and Leaflet is quite good at handling it. In this example, you'll learn how to create and interact with map vectors created from <a href="http://geojson.org/">GeoJSON</a> objects.</p>
|
||||
|
||||
<div id="map" class="map" style="height: 250px"></div>
|
||||
|
||||
<script src="sample-geojson.js"></script>
|
||||
<script>
|
||||
|
||||
var map = L.map('map').setView([39.74739, -105], 13);
|
||||
|
||||
L.tileLayer(MB_URL, {
|
||||
attribution: MB_ATTR,
|
||||
id: 'mapbox.light'
|
||||
}).addTo(map);
|
||||
|
||||
var baseballIcon = L.icon({
|
||||
iconUrl: 'baseball-marker.png',
|
||||
iconSize: [32, 37],
|
||||
iconAnchor: [16, 37],
|
||||
popupAnchor: [0, -28]
|
||||
});
|
||||
|
||||
function onEachFeature(feature, layer) {
|
||||
var popupContent = "<p>I started out as a GeoJSON " +
|
||||
feature.geometry.type + ", but now I'm a Leaflet vector!</p>";
|
||||
|
||||
if (feature.properties && feature.properties.popupContent) {
|
||||
popupContent += feature.properties.popupContent;
|
||||
}
|
||||
|
||||
layer.bindPopup(popupContent);
|
||||
}
|
||||
|
||||
L.geoJson({features: [bicycleRental, campus]}, {
|
||||
|
||||
style: function (feature) {
|
||||
return feature.properties && feature.properties.style;
|
||||
},
|
||||
|
||||
onEachFeature: onEachFeature,
|
||||
|
||||
pointToLayer: function (feature, latlng) {
|
||||
return L.circleMarker(latlng, {
|
||||
radius: 8,
|
||||
fillColor: "#ff7800",
|
||||
color: "#000",
|
||||
weight: 1,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.8
|
||||
});
|
||||
}
|
||||
}).addTo(map);
|
||||
|
||||
L.geoJson(freeBus, {
|
||||
|
||||
filter: function (feature, layer) {
|
||||
if (feature.properties) {
|
||||
// If the property "underConstruction" exists and is true, return false (don't render features under construction)
|
||||
return feature.properties.underConstruction !== undefined ? !feature.properties.underConstruction : true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
onEachFeature: onEachFeature
|
||||
}).addTo(map);
|
||||
|
||||
var coorsLayer = L.geoJson(null, {
|
||||
|
||||
pointToLayer: function (feature, latlng) {
|
||||
return L.marker(latlng, {icon: baseballIcon});
|
||||
},
|
||||
|
||||
onEachFeature: onEachFeature
|
||||
}).addTo(map);
|
||||
|
||||
coorsLayer.addData(coorsField);
|
||||
|
||||
</script>
|
||||
|
||||
<p><a href="geojson-example.html">View example on a separate page →</a></p>
|
||||
|
||||
<h3>About GeoJSON</h3>
|
||||
|
||||
<p>According to <a href="http://geojson.org">http://geojson.org</a>:</p>
|
||||
|
||||
<blockquote>GeoJSON is a format for encoding a variety of geographic data structures. A GeoJSON object may represent a geometry, a feature, or a collection of features. GeoJSON supports the following geometry types: Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, and GeometryCollection. Features in GeoJSON contain a geometry object and additional properties, and a feature collection represents a list of features.</blockquote>
|
||||
|
||||
<p>Leaflet supports all of the GeoJSON types above, but <a href="http://geojson.org/geojson-spec.html#feature-objects">Features</a> and <a href="http://geojson.org/geojson-spec.html#feature-collection-objects">FeatureCollections</a> work best as they allow you to describe features with a set of properties. We can even use these properties to style our Leaflet vectors. Here's an example of a simple GeoJSON feature:</p>
|
||||
|
||||
<pre><code>var geojsonFeature = {
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"name": "Coors Field",
|
||||
"amenity": "Baseball Stadium",
|
||||
"popupContent": "This is where the Rockies play!"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [-104.99404, 39.75621]
|
||||
}
|
||||
};
|
||||
</code></pre>
|
||||
|
||||
Beware of the switched order of latitude and longitude in GeoJSON; as per definition in [RFC 7946](https://tools.ietf.org/html/rfc7946) GeoJSON uses coordinates in (lon,lat) order instead of (lat,lon) that Leaflet uses.
|
||||
|
||||
<h3>The GeoJSON layer</h3>
|
||||
|
||||
<p>GeoJSON objects are added to the map through a <a href="http://leafletjs.com/reference.html#geojson">GeoJSON layer</a>. To create it and add it to a map, we can use the following code:</p>
|
||||
|
||||
<pre><code>L.geoJson(geojsonFeature).addTo(map);</code></pre>
|
||||
|
||||
<p>GeoJSON objects may also be passed as an array of valid GeoJSON objects.</p>
|
||||
|
||||
<pre><code>var myLines = [{
|
||||
"type": "LineString",
|
||||
"coordinates": [[-100, 40], [-105, 45], [-110, 55]]
|
||||
}, {
|
||||
"type": "LineString",
|
||||
"coordinates": [[-105, 40], [-110, 45], [-115, 55]]
|
||||
}];
|
||||
</code></pre>
|
||||
|
||||
<p>Alternatively, we could create an empty GeoJSON layer and assign it to a variable so that we can add more features to it later.</p>
|
||||
|
||||
<pre><code>var myLayer = L.geoJson().addTo(map);
|
||||
myLayer.addData(geojsonFeature);
|
||||
</code></pre>
|
||||
|
||||
<h3>Options</h3>
|
||||
|
||||
<h4>style</h4>
|
||||
|
||||
<p>The <code>style</code> option can be used to style features two different ways. First, we can pass a simple object that styles all paths (polylines and polygons) the same way:</p>
|
||||
|
||||
<pre><code>var myLines = [{
|
||||
"type": "LineString",
|
||||
"coordinates": [[-100, 40], [-105, 45], [-110, 55]]
|
||||
}, {
|
||||
"type": "LineString",
|
||||
"coordinates": [[-105, 40], [-110, 45], [-115, 55]]
|
||||
}];
|
||||
|
||||
var myStyle = {
|
||||
"color": "#ff7800",
|
||||
"weight": 5,
|
||||
"opacity": 0.65
|
||||
};
|
||||
|
||||
L.geoJson(myLines, {
|
||||
style: myStyle
|
||||
}).addTo(map);</code></pre>
|
||||
|
||||
<p>Alternatively, we can pass a function that styles individual features based on their properties. In the example below we check the "party" property and style our polygons accordingly:</p>
|
||||
|
||||
<pre><code>var states = [{
|
||||
"type": "Feature",
|
||||
"properties": {"party": "Republican"},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [[
|
||||
[-104.05, 48.99],
|
||||
[-97.22, 48.98],
|
||||
[-96.58, 45.94],
|
||||
[-104.03, 45.94],
|
||||
[-104.05, 48.99]
|
||||
]]
|
||||
}
|
||||
}, {
|
||||
"type": "Feature",
|
||||
"properties": {"party": "Democrat"},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [[
|
||||
[-109.05, 41.00],
|
||||
[-102.06, 40.99],
|
||||
[-102.03, 36.99],
|
||||
[-109.04, 36.99],
|
||||
[-109.05, 41.00]
|
||||
]]
|
||||
}
|
||||
}];
|
||||
|
||||
L.geoJson(states, {
|
||||
style: function(feature) {
|
||||
switch (feature.properties.party) {
|
||||
case 'Republican': return {color: "#ff0000"};
|
||||
case 'Democrat': return {color: "#0000ff"};
|
||||
}
|
||||
}
|
||||
}).addTo(map);</code></pre>
|
||||
|
||||
<h4>pointToLayer</h4>
|
||||
|
||||
<p>Points are handled differently than polylines and polygons. By default simple markers are drawn for GeoJSON Points. We can alter this by passing a <code>pointToLayer</code> function in a <a href="http://leafletjs.com/reference.html#geojson-options">GeoJSON options</a> object when creating the GeoJSON layer. This function is passed a <a href="http://leafletjs.com/reference.html#latlng">LatLng</a> and should return an instance of ILayer, in this case likely a <a href="http://leafletjs.com/reference.html#marker">Marker</a> or <a href="http://leafletjs.com/reference.html#circlemarker">CircleMarker</a>.</p>
|
||||
|
||||
<p>Here we're using the <code>pointToLayer</code> option to create a CircleMarker:</p>
|
||||
|
||||
<pre><code>var geojsonMarkerOptions = {
|
||||
radius: 8,
|
||||
fillColor: "#ff7800",
|
||||
color: "#000",
|
||||
weight: 1,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.8
|
||||
};
|
||||
|
||||
L.geoJson(someGeojsonFeature, {
|
||||
pointToLayer: function (feature, latlng) {
|
||||
return L.circleMarker(latlng, geojsonMarkerOptions);
|
||||
}
|
||||
}).addTo(map);</code></pre>
|
||||
|
||||
<p>We could also set the <code>style</code> property in this example — Leaflet is smart enough to apply styles to GeoJSON points if you create a vector layer like circle inside the <code>pointToLayer</code> function.</p>
|
||||
|
||||
<h4>onEachFeature</h4>
|
||||
|
||||
<p>The <code>onEachFeature</code> option is a function that gets called on each feature before adding it to a GeoJSON layer. A common reason to use this option is to attach a popup to features when they are clicked.</p>
|
||||
|
||||
<pre><code>function onEachFeature(feature, layer) {
|
||||
// does this feature have a property named popupContent?
|
||||
if (feature.properties && feature.properties.popupContent) {
|
||||
layer.bindPopup(feature.properties.popupContent);
|
||||
}
|
||||
}
|
||||
|
||||
var geojsonFeature = {
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"name": "Coors Field",
|
||||
"amenity": "Baseball Stadium",
|
||||
"popupContent": "This is where the Rockies play!"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [-104.99404, 39.75621]
|
||||
}
|
||||
};
|
||||
|
||||
L.geoJson(geojsonFeature, {
|
||||
onEachFeature: onEachFeature
|
||||
}).addTo(map);</code></pre>
|
||||
|
||||
<h4>filter</h4>
|
||||
|
||||
<p>The <code>filter</code> option can be used to control the visibility of GeoJSON features. To accomplish this we pass a function as the <code>filter</code> option. This function gets called for each feature in your GeoJSON layer, and gets passed the <code>feature</code> and the <code>layer</code>. You can then utilise the values in the feature's properties to control the visibility by returning <code>true</code> or <code>false</code>.</p>
|
||||
|
||||
<p>In the example below "Busch Field" will not be shown on the map.</p>
|
||||
|
||||
<pre><code>var someFeatures = [{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"name": "Coors Field",
|
||||
"show_on_map": true
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [-104.99404, 39.75621]
|
||||
}
|
||||
}, {
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"name": "Busch Field",
|
||||
"show_on_map": false
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [-104.98404, 39.74621]
|
||||
}
|
||||
}];
|
||||
|
||||
L.geoJson(someFeatures, {
|
||||
filter: function(feature, layer) {
|
||||
return feature.properties.show_on_map;
|
||||
}
|
||||
}).addTo(map);</code></pre>
|
||||
|
||||
<p>View the <a href="geojson-example.html">example page</a> to see in detail what is possible with the GeoJSON layer.</p>
|
||||
@@ -0,0 +1,205 @@
|
||||
---
|
||||
layout: tutorial_v2
|
||||
title: Using GeoJSON with Leaflet
|
||||
---
|
||||
|
||||
<h3>Using GeoJSON with Leaflet</h3>
|
||||
|
||||
<p>GeoJSON is becoming a very popular data format among many GIS technologies and services — it's simple, lightweight, straightforward, and Leaflet is quite good at handling it. In this example, you'll learn how to create and interact with map vectors created from <a href="http://geojson.org/">GeoJSON</a> objects.</p>
|
||||
|
||||
{% include frame.html url="example.html" %}
|
||||
|
||||
<h3>About GeoJSON</h3>
|
||||
|
||||
<p>According to <a href="http://geojson.org">http://geojson.org</a>:</p>
|
||||
|
||||
<blockquote>GeoJSON is a format for encoding a variety of geographic data structures. A GeoJSON object may represent a geometry, a feature, or a collection of features. GeoJSON supports the following geometry types: Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, and GeometryCollection. Features in GeoJSON contain a geometry object and additional properties, and a feature collection represents a list of features.</blockquote>
|
||||
|
||||
<p>Leaflet supports all of the GeoJSON types above, but <a href="http://geojson.org/geojson-spec.html#feature-objects">Features</a> and <a href="http://geojson.org/geojson-spec.html#feature-collection-objects">FeatureCollections</a> work best as they allow you to describe features with a set of properties. We can even use these properties to style our Leaflet vectors. Here's an example of a simple GeoJSON feature:</p>
|
||||
|
||||
<pre><code>var geojsonFeature = {
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"name": "Coors Field",
|
||||
"amenity": "Baseball Stadium",
|
||||
"popupContent": "This is where the Rockies play!"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [-104.99404, 39.75621]
|
||||
}
|
||||
};
|
||||
</code></pre>
|
||||
|
||||
<h3>The GeoJSON layer</h3>
|
||||
|
||||
<p>GeoJSON objects are added to the map through a <a href="http://leafletjs.com/reference.html#geojson">GeoJSON layer</a>. To create it and add it to a map, we can use the following code:</p>
|
||||
|
||||
<pre><code>L.geoJSON(geojsonFeature).addTo(map);</code></pre>
|
||||
|
||||
<p>GeoJSON objects may also be passed as an array of valid GeoJSON objects.</p>
|
||||
|
||||
<pre><code>var myLines = [{
|
||||
"type": "LineString",
|
||||
"coordinates": [[-100, 40], [-105, 45], [-110, 55]]
|
||||
}, {
|
||||
"type": "LineString",
|
||||
"coordinates": [[-105, 40], [-110, 45], [-115, 55]]
|
||||
}];
|
||||
</code></pre>
|
||||
|
||||
<p>Alternatively, we could create an empty GeoJSON layer and assign it to a variable so that we can add more features to it later.</p>
|
||||
|
||||
<pre><code>var myLayer = L.geoJSON().addTo(map);
|
||||
myLayer.addData(geojsonFeature);
|
||||
</code></pre>
|
||||
|
||||
<h3>Options</h3>
|
||||
|
||||
<h4>style</h4>
|
||||
|
||||
<p>The <code>style</code> option can be used to style features two different ways. First, we can pass a simple object that styles all paths (polylines and polygons) the same way:</p>
|
||||
|
||||
<pre><code>var myLines = [{
|
||||
"type": "LineString",
|
||||
"coordinates": [[-100, 40], [-105, 45], [-110, 55]]
|
||||
}, {
|
||||
"type": "LineString",
|
||||
"coordinates": [[-105, 40], [-110, 45], [-115, 55]]
|
||||
}];
|
||||
|
||||
var myStyle = {
|
||||
"color": "#ff7800",
|
||||
"weight": 5,
|
||||
"opacity": 0.65
|
||||
};
|
||||
|
||||
L.geoJSON(myLines, {
|
||||
style: myStyle
|
||||
}).addTo(map);</code></pre>
|
||||
|
||||
<p>Alternatively, we can pass a function that styles individual features based on their properties. In the example below we check the "party" property and style our polygons accordingly:</p>
|
||||
|
||||
<pre><code>var states = [{
|
||||
"type": "Feature",
|
||||
"properties": {"party": "Republican"},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [[
|
||||
[-104.05, 48.99],
|
||||
[-97.22, 48.98],
|
||||
[-96.58, 45.94],
|
||||
[-104.03, 45.94],
|
||||
[-104.05, 48.99]
|
||||
]]
|
||||
}
|
||||
}, {
|
||||
"type": "Feature",
|
||||
"properties": {"party": "Democrat"},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [[
|
||||
[-109.05, 41.00],
|
||||
[-102.06, 40.99],
|
||||
[-102.03, 36.99],
|
||||
[-109.04, 36.99],
|
||||
[-109.05, 41.00]
|
||||
]]
|
||||
}
|
||||
}];
|
||||
|
||||
L.geoJSON(states, {
|
||||
style: function(feature) {
|
||||
switch (feature.properties.party) {
|
||||
case 'Republican': return {color: "#ff0000"};
|
||||
case 'Democrat': return {color: "#0000ff"};
|
||||
}
|
||||
}
|
||||
}).addTo(map);</code></pre>
|
||||
|
||||
<h4>pointToLayer</h4>
|
||||
|
||||
<p>Points are handled differently than polylines and polygons. By default simple markers are drawn for GeoJSON Points. We can alter this by passing a <code>pointToLayer</code> function in a <a href="http://leafletjs.com/reference.html#geojson-options">GeoJSON options</a> object when creating the GeoJSON layer. This function is passed a <a href="http://leafletjs.com/reference.html#latlng">LatLng</a> and should return an instance of ILayer, in this case likely a <a href="http://leafletjs.com/reference.html#marker">Marker</a> or <a href="http://leafletjs.com/reference.html#circlemarker">CircleMarker</a>.</p>
|
||||
|
||||
<p>Here we're using the <code>pointToLayer</code> option to create a CircleMarker:</p>
|
||||
|
||||
<pre><code>var geojsonMarkerOptions = {
|
||||
radius: 8,
|
||||
fillColor: "#ff7800",
|
||||
color: "#000",
|
||||
weight: 1,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.8
|
||||
};
|
||||
|
||||
L.geoJSON(someGeojsonFeature, {
|
||||
pointToLayer: function (feature, latlng) {
|
||||
return L.circleMarker(latlng, geojsonMarkerOptions);
|
||||
}
|
||||
}).addTo(map);</code></pre>
|
||||
|
||||
<p>We could also set the <code>style</code> property in this example — Leaflet is smart enough to apply styles to GeoJSON points if you create a vector layer like circle inside the <code>pointToLayer</code> function.</p>
|
||||
|
||||
<h4>onEachFeature</h4>
|
||||
|
||||
<p>The <code>onEachFeature</code> option is a function that gets called on each feature before adding it to a GeoJSON layer. A common reason to use this option is to attach a popup to features when they are clicked.</p>
|
||||
|
||||
<pre><code>function onEachFeature(feature, layer) {
|
||||
// does this feature have a property named popupContent?
|
||||
if (feature.properties && feature.properties.popupContent) {
|
||||
layer.bindPopup(feature.properties.popupContent);
|
||||
}
|
||||
}
|
||||
|
||||
var geojsonFeature = {
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"name": "Coors Field",
|
||||
"amenity": "Baseball Stadium",
|
||||
"popupContent": "This is where the Rockies play!"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [-104.99404, 39.75621]
|
||||
}
|
||||
};
|
||||
|
||||
L.geoJSON(geojsonFeature, {
|
||||
onEachFeature: onEachFeature
|
||||
}).addTo(map);</code></pre>
|
||||
|
||||
<h4>filter</h4>
|
||||
|
||||
<p>The <code>filter</code> option can be used to control the visibility of GeoJSON features. To accomplish this we pass a function as the <code>filter</code> option. This function gets called for each feature in your GeoJSON layer, and gets passed the <code>feature</code> and the <code>layer</code>. You can then utilise the values in the feature's properties to control the visibility by returning <code>true</code> or <code>false</code>.</p>
|
||||
|
||||
<p>In the example below "Busch Field" will not be shown on the map.</p>
|
||||
|
||||
<pre><code>var someFeatures = [{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"name": "Coors Field",
|
||||
"show_on_map": true
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [-104.99404, 39.75621]
|
||||
}
|
||||
}, {
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"name": "Busch Field",
|
||||
"show_on_map": false
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [-104.98404, 39.74621]
|
||||
}
|
||||
}];
|
||||
|
||||
L.geoJSON(someFeatures, {
|
||||
filter: function(feature, layer) {
|
||||
return feature.properties.show_on_map;
|
||||
}
|
||||
}).addTo(map);</code></pre>
|
||||
|
||||
<p>View the <a href="example.html">example page</a> to see in detail what is possible with the GeoJSON layer.</p>
|
||||
@@ -0,0 +1,248 @@
|
||||
var freeBus = {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "LineString",
|
||||
"coordinates": [
|
||||
[-105.00341892242432, 39.75383843460583],
|
||||
[-105.0008225440979, 39.751891803969535]
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"popupContent": "This is a free bus line that will take you across downtown.",
|
||||
"underConstruction": false
|
||||
},
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "LineString",
|
||||
"coordinates": [
|
||||
[-105.0008225440979, 39.751891803969535],
|
||||
[-104.99820470809937, 39.74979664004068]
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"popupContent": "This is a free bus line that will take you across downtown.",
|
||||
"underConstruction": true
|
||||
},
|
||||
"id": 2
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "LineString",
|
||||
"coordinates": [
|
||||
[-104.99820470809937, 39.74979664004068],
|
||||
[-104.98689651489258, 39.741052354709055]
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"popupContent": "This is a free bus line that will take you across downtown.",
|
||||
"underConstruction": false
|
||||
},
|
||||
"id": 3
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var lightRailStop = {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"popupContent": "18th & California Light Rail Stop"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [-104.98999178409576, 39.74683938093904]
|
||||
}
|
||||
},{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"popupContent": "20th & Welton Light Rail Stop"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [-104.98689115047453, 39.747924136466565]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var bicycleRental = {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-104.9998241,
|
||||
39.7471494
|
||||
]
|
||||
},
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"popupContent": "This is a B-Cycle Station. Come pick up a bike and pay by the hour. What a deal!"
|
||||
},
|
||||
"id": 51
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-104.9983545,
|
||||
39.7502833
|
||||
]
|
||||
},
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"popupContent": "This is a B-Cycle Station. Come pick up a bike and pay by the hour. What a deal!"
|
||||
},
|
||||
"id": 52
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-104.9963919,
|
||||
39.7444271
|
||||
]
|
||||
},
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"popupContent": "This is a B-Cycle Station. Come pick up a bike and pay by the hour. What a deal!"
|
||||
},
|
||||
"id": 54
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-104.9960754,
|
||||
39.7498956
|
||||
]
|
||||
},
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"popupContent": "This is a B-Cycle Station. Come pick up a bike and pay by the hour. What a deal!"
|
||||
},
|
||||
"id": 55
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-104.9933717,
|
||||
39.7477264
|
||||
]
|
||||
},
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"popupContent": "This is a B-Cycle Station. Come pick up a bike and pay by the hour. What a deal!"
|
||||
},
|
||||
"id": 57
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-104.9913392,
|
||||
39.7432392
|
||||
]
|
||||
},
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"popupContent": "This is a B-Cycle Station. Come pick up a bike and pay by the hour. What a deal!"
|
||||
},
|
||||
"id": 58
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-104.9788452,
|
||||
39.6933755
|
||||
]
|
||||
},
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"popupContent": "This is a B-Cycle Station. Come pick up a bike and pay by the hour. What a deal!"
|
||||
},
|
||||
"id": 74
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var campus = {
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"popupContent": "This is the Auraria West Campus",
|
||||
"style": {
|
||||
weight: 2,
|
||||
color: "#999",
|
||||
opacity: 1,
|
||||
fillColor: "#B0DE5C",
|
||||
fillOpacity: 0.8
|
||||
}
|
||||
},
|
||||
"geometry": {
|
||||
"type": "MultiPolygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[
|
||||
[-105.00432014465332, 39.74732195489861],
|
||||
[-105.00715255737305, 39.74620006835170],
|
||||
[-105.00921249389647, 39.74468219277038],
|
||||
[-105.01067161560059, 39.74362625960105],
|
||||
[-105.01195907592773, 39.74290029616054],
|
||||
[-105.00989913940431, 39.74078835902781],
|
||||
[-105.00758171081543, 39.74059036160317],
|
||||
[-105.00346183776855, 39.74059036160317],
|
||||
[-105.00097274780272, 39.74059036160317],
|
||||
[-105.00062942504881, 39.74072235994946],
|
||||
[-105.00020027160645, 39.74191033368865],
|
||||
[-105.00071525573731, 39.74276830198601],
|
||||
[-105.00097274780272, 39.74369225589818],
|
||||
[-105.00097274780272, 39.74461619742136],
|
||||
[-105.00123023986816, 39.74534214278395],
|
||||
[-105.00183105468751, 39.74613407445653],
|
||||
[-105.00432014465332, 39.74732195489861]
|
||||
],[
|
||||
[-105.00361204147337, 39.74354376414072],
|
||||
[-105.00301122665405, 39.74278480127163],
|
||||
[-105.00221729278564, 39.74316428375108],
|
||||
[-105.00283956527711, 39.74390674342741],
|
||||
[-105.00361204147337, 39.74354376414072]
|
||||
]
|
||||
],[
|
||||
[
|
||||
[-105.00942707061768, 39.73989736613708],
|
||||
[-105.00942707061768, 39.73910536278566],
|
||||
[-105.00685214996338, 39.73923736397631],
|
||||
[-105.00384807586671, 39.73910536278566],
|
||||
[-105.00174522399902, 39.73903936209552],
|
||||
[-105.00041484832764, 39.73910536278566],
|
||||
[-105.00041484832764, 39.73979836621592],
|
||||
[-105.00535011291504, 39.73986436617916],
|
||||
[-105.00942707061768, 39.73989736613708]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var coorsField = {
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"popupContent": "Coors Field"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [-104.99404191970824, 39.756213909328125]
|
||||
}
|
||||
};
|
||||
|
After Width: | Height: | Size: 122 KiB |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
layout: redirected
|
||||
sitemap: false
|
||||
redirect_to: layers-control/example.html
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
layout: redirected
|
||||
sitemap: false
|
||||
redirect_to: layers-control/
|
||||
---
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Layers Control Tutorial
|
||||
---
|
||||
<script>
|
||||
var cities = L.layerGroup();
|
||||
|
||||
L.marker([39.61, -105.02]).bindPopup('This is Littleton, CO.').addTo(cities),
|
||||
L.marker([39.74, -104.99]).bindPopup('This is Denver, CO.').addTo(cities),
|
||||
L.marker([39.73, -104.8]).bindPopup('This is Aurora, CO.').addTo(cities),
|
||||
L.marker([39.77, -105.23]).bindPopup('This is Golden, CO.').addTo(cities);
|
||||
|
||||
|
||||
var mbAttr = 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
|
||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
|
||||
'Imagery © <a href="http://mapbox.com">Mapbox</a>',
|
||||
mbUrl = 'https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw';
|
||||
|
||||
var grayscale = L.tileLayer(mbUrl, {id: 'mapbox.light', attribution: mbAttr}),
|
||||
streets = L.tileLayer(mbUrl, {id: 'mapbox.streets', attribution: mbAttr});
|
||||
|
||||
var map = L.map('map', {
|
||||
center: [39.73, -104.99],
|
||||
zoom: 10,
|
||||
layers: [grayscale, cities]
|
||||
});
|
||||
|
||||
var baseLayers = {
|
||||
"Grayscale": grayscale,
|
||||
"Streets": streets
|
||||
};
|
||||
|
||||
var overlays = {
|
||||
"Cities": cities
|
||||
};
|
||||
|
||||
L.control.layers(baseLayers, overlays).addTo(map);
|
||||
</script>
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
layout: tutorial_v2
|
||||
title: Layer Groups and Layers Control
|
||||
---
|
||||
|
||||
## Layer Groups and Layers Control
|
||||
|
||||
This tutorial will show you how to group several layers into one, and how to use the layers control to allow users to easily switch different layers on your map.
|
||||
|
||||
{% include frame.html url="example.html" %}
|
||||
|
||||
### Layer Groups
|
||||
|
||||
Let's suppose you have a bunch of layers you want to combine into a group to handle them as one in your code:
|
||||
|
||||
var littleton = L.marker([39.61, -105.02]).bindPopup('This is Littleton, CO.'),
|
||||
denver = L.marker([39.74, -104.99]).bindPopup('This is Denver, CO.'),
|
||||
aurora = L.marker([39.73, -104.8]).bindPopup('This is Aurora, CO.'),
|
||||
golden = L.marker([39.77, -105.23]).bindPopup('This is Golden, CO.');
|
||||
|
||||
Instead of adding them directly to the map, you can do the following, using the <a href="../../reference.html#layergroup">LayerGroup</a> class:
|
||||
|
||||
var cities = L.layerGroup([littleton, denver, aurora, golden]);
|
||||
|
||||
Easy enough! Now you have a `cities` layer that combines your city markers into one layer you can add or remove from the map at once.
|
||||
|
||||
### Layers Control
|
||||
|
||||
Leaflet has a nice little control that allows your users to control which layers they see on your map. In addition to showing you how to use it, we'll also show you another handy use for layer groups.
|
||||
|
||||
There are two types of layers: (1) base layers that are mutually exclusive (only one can be visible on your map at a time), e.g. tile layers, and (2) overlays, which are all the other stuff you put over the base layers. In this example, we want to have two base layers (a grayscale and a colored base map) to switch between, and an overlay to switch on and off: the city markers we created earlier.
|
||||
|
||||
Now let's create those base layers and add the default ones to the map:
|
||||
|
||||
<pre><code>var grayscale = L.tileLayer(mapboxUrl, {id: '<a href="https://mapbox.com">MapID</a>', attribution: mapboxAttribution}),
|
||||
streets = L.tileLayer(mapboxUrl, {id: '<a href="https://mapbox.com">MapID</a>', attribution: mapboxAttribution});
|
||||
|
||||
var map = L.map('map', {
|
||||
center: [39.73, -104.99],
|
||||
zoom: 10,
|
||||
layers: [grayscale, cities]
|
||||
});</code></pre>
|
||||
|
||||
Next, we'll create two objects. One will contain our base layers and one will contain our overlays. These are just simple objects with key/value pairs. The key sets the text for the layer in the control (e.g. "Streets"), while the corresponding value is a reference to the layer (e.g. `streets`).
|
||||
|
||||
<pre><code>var baseMaps = {
|
||||
"Grayscale": grayscale,
|
||||
"Streets": streets
|
||||
};
|
||||
|
||||
var overlayMaps = {
|
||||
"Cities": cities
|
||||
};</code></pre>
|
||||
|
||||
Now, all that's left to do is to create a [Layers Control](../../reference.html#control-layers) and add it to the map. The first argument passed when creating the layers control is the base layers object. The second argument is the overlays object. Both arguments are optional: you can pass just a base layers object by omitting the second argument, or just an overlays objects by passing `null` as the first argument. In each case, the omitted layer type will not appear for the user to select.
|
||||
|
||||
<pre><code>L.control.layers(baseMaps, overlayMaps).addTo(map);</code></pre>
|
||||
|
||||
Note that we added `grayscale` and `cities` layers to the map but didn't add `streets`. The layers control is smart enough to detect what layers we've already added and have corresponding checkboxes and radioboxes set.
|
||||
|
||||
Also note that when using multiple base layers, only one of them should be added to the map at instantiation, but all of them should be present in the base layers object when creating the layers control.
|
||||
|
||||
Finally, you can style the keys when you define the objects for the layers. For example, this code will make the label for the grayscale map gray:
|
||||
|
||||
<pre><code>var baseMaps = {
|
||||
"<span style='color: gray'>Grayscale</span>": grayscale,
|
||||
"Streets": streets
|
||||
};
|
||||
</code></pre>
|
||||
|
||||
Now let's [view the result on a separate page →](example.html)
|
||||
|
||||
|
After Width: | Height: | Size: 144 KiB |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
layout: redirected
|
||||
sitemap: false
|
||||
redirect_to: map-panes/example.html
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
layout: redirected
|
||||
sitemap: false
|
||||
redirect_to: map-panes/
|
||||
---
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Custom Icons Tutorial
|
||||
---
|
||||
<script type="text/javascript" src="eu-countries.js"></script>
|
||||
|
||||
<script>
|
||||
|
||||
var map = L.map('map');
|
||||
|
||||
map.createPane('labels');
|
||||
|
||||
// This pane is above markers but below popups
|
||||
map.getPane('labels').style.zIndex = 650;
|
||||
|
||||
// Layers in this pane are non-interactive and do not obscure mouse/touch events
|
||||
map.getPane('labels').style.pointerEvents = 'none';
|
||||
|
||||
|
||||
var cartodbAttribution = '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <a href="http://cartodb.com/attributions">CartoDB</a>';
|
||||
|
||||
var positron = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', {
|
||||
attribution: cartodbAttribution
|
||||
}).addTo(map);
|
||||
|
||||
var positronLabels = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}.png', {
|
||||
attribution: cartodbAttribution,
|
||||
pane: 'labels'
|
||||
}).addTo(map);
|
||||
|
||||
geojson = L.geoJson(euCountries).addTo(map);
|
||||
|
||||
geojson.eachLayer(function (layer) {
|
||||
layer.bindPopup(layer.feature.properties.name);
|
||||
});
|
||||
|
||||
map.setView({ lat: 47.040182144806664, lng: 9.667968750000002 }, 4);
|
||||
</script>
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
layout: tutorial_v2
|
||||
title: Working with map panes
|
||||
---
|
||||
|
||||
## What are panes?
|
||||
|
||||
In Leaflet, map panes group layers together implicitly, without the developer knowing about it. This grouping allows web browsers to work with several layers at once in a more efficient way than working with layers individually.
|
||||
|
||||
Map panes use the [z-index CSS property](https://developer.mozilla.org/docs/Web/CSS/z-index) to always show some layers on top of others. The [default order](../../reference.html#map-panes) is:
|
||||
|
||||
* `TileLayer`s and `GridLayer`s
|
||||
* `Path`s, like lines, polylines, circles, or `GeoJSON` layers.
|
||||
* `Marker` shadows
|
||||
* `Marker` icons
|
||||
* `Popup`s
|
||||
|
||||
This is why, in Leaflet maps, popups always show "on top" of other layers, markers always show on top of tile layers, etc.
|
||||
|
||||
A new feature of **Leaflet 1.0.0** (not present in 0.7.x) is custom map panes, which allows for customization of this order.
|
||||
|
||||
## The default is not always right
|
||||
|
||||
In some particular cases, the default order is not the right one for the map. We can demonstrate this with the [Carto basemaps](https://cartodb.com/basemaps/) and labels:
|
||||
|
||||
|
||||
<style>
|
||||
.tiles img {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class='tiles'>
|
||||
<div style='display: inline-block'>
|
||||
<img src="http://a.basemaps.cartocdn.com/light_nolabels/0/0/0.png" class="bordered-img" /><br/>
|
||||
Basemap tile with no labels
|
||||
</div>
|
||||
|
||||
<div style='display: inline-block'>
|
||||
<img src="http://a.basemaps.cartocdn.com/light_only_labels/0/0/0.png" class="bordered-img" /><br/>
|
||||
Transparent labels-only tile
|
||||
</div>
|
||||
|
||||
<div style='display: inline-block; position:relative;'>
|
||||
<img src="http://a.basemaps.cartocdn.com/light_nolabels/0/0/0.png" class="bordered-img" />
|
||||
<img src="http://a.basemaps.cartocdn.com/light_only_labels/0/0/0.png" style='position:absolute; left:0; top:0;'/><br/>
|
||||
Labels on top of basemap
|
||||
</div>
|
||||
</div>
|
||||
|
||||
If we create a Leaflet map with these two tile layers, any marker or polygon will show on top of both, but having the labels on top [looks much nicer](http://blog.cartodb.com/let-your-labels-shine/). How can we do that?
|
||||
|
||||
{% include frame.html url="example.html" %}
|
||||
|
||||
## Custom pane
|
||||
|
||||
We can use the defaults for the basemap tiles and some overlays like GeoJSON layers, but we have to define a custom pane for the labels, so they show on top of the GeoJSON data.
|
||||
|
||||
Custom map panes are created on a per-map basis, so first create an instance of `L.Map` and the pane:
|
||||
|
||||
|
||||
var map = L.map('map');
|
||||
map.createPane('labels');
|
||||
|
||||
|
||||
The next step is setting the z-index of the pane. Looking at the [defaults](https://github.com/Leaflet/Leaflet/blob/v1.0.0/dist/leaflet.css#L87), a value of 650 will make the `TileLayer` with the labels show on top of markers but below pop-ups. By using `getPane()`, we have a reference to the [`HTMLElement`](https://developer.mozilla.org/docs/Web/API/HTMLElement) representing the pane, and change its z-index:
|
||||
|
||||
|
||||
map.getPane('labels').style.zIndex = 650;
|
||||
|
||||
|
||||
One of the problems of having image tiles on top of other map layers is that the tiles will capture clicks and touches. If a user clicks anywhere on the map, the web browser will assume she clicked on the labels tiles, and not on the GeoJSON or on the markers. This can be solved using [the `pointer-events` CSS property](https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events):
|
||||
|
||||
|
||||
map.getPane('labels').style.pointerEvents = 'none';
|
||||
|
||||
|
||||
With the pane now ready, we can add the layers, paying attention to use the `pane` option on the labels tiles:
|
||||
|
||||
|
||||
var positron = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', {
|
||||
attribution: '©OpenStreetMap, ©CartoDB'
|
||||
}).addTo(map);
|
||||
|
||||
var positronLabels = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}.png', {
|
||||
attribution: '©OpenStreetMap, ©CartoDB',
|
||||
pane: 'labels'
|
||||
}).addTo(map);
|
||||
|
||||
var geojson = L.geoJson(GeoJsonData, geoJsonOptions).addTo(map);
|
||||
|
||||
Finally, add some interaction to each feature on the GeoJSON layer:
|
||||
|
||||
geojson.eachLayer(function (layer) {
|
||||
layer.bindPopup(layer.feature.properties.name);
|
||||
});
|
||||
|
||||
map.fitBounds(geojson.getBounds());
|
||||
|
||||
|
||||
Now the [example map](example.html) is complete!
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 150 KiB |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
layout: redirected
|
||||
sitemap: false
|
||||
redirect_to: mobile/example.html
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
layout: redirected
|
||||
sitemap: false
|
||||
redirect_to: mobile/
|
||||
---
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Mobile tutorial
|
||||
css: "body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
html, body, #map {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}"
|
||||
---
|
||||
<script>
|
||||
var map = L.map('map').fitWorld();
|
||||
|
||||
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
|
||||
maxZoom: 18,
|
||||
attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
|
||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
|
||||
'Imagery © <a href="http://mapbox.com">Mapbox</a>',
|
||||
id: 'mapbox.streets'
|
||||
}).addTo(map);
|
||||
|
||||
function onLocationFound(e) {
|
||||
var radius = e.accuracy / 2;
|
||||
|
||||
L.marker(e.latlng).addTo(map)
|
||||
.bindPopup("You are within " + radius + " meters from this point").openPopup();
|
||||
|
||||
L.circle(e.latlng, radius).addTo(map);
|
||||
}
|
||||
|
||||
function onLocationError(e) {
|
||||
alert(e.message);
|
||||
}
|
||||
|
||||
map.on('locationfound', onLocationFound);
|
||||
map.on('locationerror', onLocationError);
|
||||
|
||||
map.locate({setView: true, maxZoom: 16});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
layout: tutorial_v2
|
||||
title: Leaflet on Mobile
|
||||
---
|
||||
|
||||
## Leaflet on Mobile
|
||||
|
||||
In this example, you'll learn how to create a fullscreen map tuned for mobile devices like iPhone, iPad or Android phones, and how to easily detect and use the current user location.
|
||||
|
||||
{% include frame.html url="example.html" %}
|
||||
|
||||
### Preparing the page
|
||||
|
||||
First we'll take a look at the HTML & CSS code of the page. To make our map `div` element stretch to all available space (fullscreen), we can use the following CSS code:
|
||||
|
||||
{: .css}
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
html, body, #map {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
Also, we need to tell the mobile browser to disable unwanted scaling of the page and set it to its actual size by placing the following line in the `head` section or our HTML page:
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
|
||||
### Initializing the map
|
||||
|
||||
We'll now initialize the map in the JavaScript code like we did in the [quick start guide](../quick-start/), showing the whole world:
|
||||
|
||||
<pre><code class="javascript">var map = L.map('map').fitWorld();
|
||||
|
||||
L.tileLayer('https://api.tiles.mapbox.com/v4/<a href="https://mapbox.com">MapID</a>/997/256/{z}/{x}/{y}.png?access_token={accessToken}', {
|
||||
attribution: 'Map data &copy; <span class="text-cut" data-cut="[…]"><a href="http://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="http://mapbox.com">Mapbox</a></span>',
|
||||
maxZoom: 18
|
||||
}).addTo(map);</code></pre>
|
||||
|
||||
### Geolocation
|
||||
|
||||
Leaflet has a very handy shortcut for zooming the map view to the detected location --- `locate` method with the `setView` option, replacing the usual `setView` method in the code:
|
||||
|
||||
map.locate({setView: true, maxZoom: 16});
|
||||
|
||||
Here we specify 16 as the maximum zoom when setting the map view automatically. As soon as the user agrees to share its location and it's detected by the browser, the map will set the view to it. Now we have a working fullscreen mobile map! But what if we need to do something after the geolocation completed? Here's what the `locationfound` and `locationerror` events are for. Let's for example add a marker in the detected location, showing accuracy in a popup, by adding an event listener to `locationfound` event before the `locateAndSetView` call:
|
||||
|
||||
function onLocationFound(e) {
|
||||
var radius = e.accuracy / 2;
|
||||
|
||||
L.marker(e.latlng).addTo(map)
|
||||
.bindPopup("You are within " + radius + " meters from this point").openPopup();
|
||||
|
||||
L.circle(e.latlng, radius).addTo(map);
|
||||
}
|
||||
|
||||
map.on('locationfound', onLocationFound);
|
||||
|
||||
Excellent! But it would also be nice to show an error message if the geolocation failed:
|
||||
|
||||
function onLocationError(e) {
|
||||
alert(e.message);
|
||||
}
|
||||
|
||||
map.on('locationerror', onLocationError);
|
||||
|
||||
If you have `setView` option set to true and the geolocation failed, it will set the view to the whole world.
|
||||
|
||||
Now the example is complete --- try it on your mobile phone: [View the full example →](example.html)
|
||||
|
||||
Next steps would be to take a look at the detailed [documentation](../../reference.html) and browse [other examples](../../examples.html).
|
||||
|
After Width: | Height: | Size: 389 KiB |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
layout: redirected
|
||||
sitemap: false
|
||||
redirect_to: quick-start/example.html
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
layout: redirected
|
||||
sitemap: false
|
||||
redirect_to: quick-start/
|
||||
---
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Quick Start
|
||||
customMapContainer: "true"
|
||||
---
|
||||
<div id='mapid' style='width: 600px; height: 400px;'></div>
|
||||
<script>
|
||||
|
||||
var mymap = L.map('mapid').setView([51.505, -0.09], 13);
|
||||
|
||||
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
|
||||
maxZoom: 18,
|
||||
attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
|
||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
|
||||
'Imagery © <a href="http://mapbox.com">Mapbox</a>',
|
||||
id: 'mapbox.streets'
|
||||
}).addTo(mymap);
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Quick Start
|
||||
customMapContainer: "true"
|
||||
---
|
||||
<div id='mapid' style='width: 600px; height: 400px;'></div>
|
||||
<script>
|
||||
|
||||
var mymap = L.map('mapid').setView([51.505, -0.09], 13);
|
||||
|
||||
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
|
||||
maxZoom: 18,
|
||||
attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
|
||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
|
||||
'Imagery © <a href="http://mapbox.com">Mapbox</a>',
|
||||
id: 'mapbox.streets'
|
||||
}).addTo(mymap);
|
||||
|
||||
L.marker([51.5, -0.09]).addTo(mymap);
|
||||
|
||||
L.circle([51.508, -0.11], {
|
||||
color: 'red',
|
||||
fillColor: '#f03',
|
||||
fillOpacity: 0.5,
|
||||
radius: 500
|
||||
}).addTo(mymap);
|
||||
|
||||
L.polygon([
|
||||
[51.509, -0.08],
|
||||
[51.503, -0.06],
|
||||
[51.51, -0.047]
|
||||
]).addTo(mymap);
|
||||
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Quick Start
|
||||
customMapContainer: "true"
|
||||
---
|
||||
<div id='mapid' style='width: 600px; height: 400px;'></div>
|
||||
<script>
|
||||
|
||||
var mymap = L.map('mapid').setView([51.505, -0.09], 13);
|
||||
|
||||
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
|
||||
maxZoom: 18,
|
||||
attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
|
||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
|
||||
'Imagery © <a href="http://mapbox.com">Mapbox</a>',
|
||||
id: 'mapbox.streets'
|
||||
}).addTo(mymap);
|
||||
|
||||
L.marker([51.5, -0.09]).addTo(mymap)
|
||||
.bindPopup("<b>Hello world!</b><br />I am a popup.").openPopup();
|
||||
|
||||
L.circle([51.508, -0.11], 500, {
|
||||
color: 'red',
|
||||
fillColor: '#f03',
|
||||
fillOpacity: 0.5
|
||||
}).addTo(mymap).bindPopup("I am a circle.");
|
||||
|
||||
L.polygon([
|
||||
[51.509, -0.08],
|
||||
[51.503, -0.06],
|
||||
[51.51, -0.047]
|
||||
]).addTo(mymap).bindPopup("I am a polygon.");
|
||||
|
||||
|
||||
var popup = L.popup();
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Quick Start
|
||||
customMapContainer: "true"
|
||||
---
|
||||
<div id='mapid' style='width: 600px; height: 400px;'></div>
|
||||
<script>
|
||||
|
||||
var mymap = L.map('mapid').setView([51.505, -0.09], 13);
|
||||
|
||||
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
|
||||
maxZoom: 18,
|
||||
attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
|
||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
|
||||
'Imagery © <a href="http://mapbox.com">Mapbox</a>',
|
||||
id: 'mapbox.streets'
|
||||
}).addTo(mymap);
|
||||
|
||||
L.marker([51.5, -0.09]).addTo(mymap)
|
||||
.bindPopup("<b>Hello world!</b><br />I am a popup.").openPopup();
|
||||
|
||||
L.circle([51.508, -0.11], 500, {
|
||||
color: 'red',
|
||||
fillColor: '#f03',
|
||||
fillOpacity: 0.5
|
||||
}).addTo(mymap).bindPopup("I am a circle.");
|
||||
|
||||
L.polygon([
|
||||
[51.509, -0.08],
|
||||
[51.503, -0.06],
|
||||
[51.51, -0.047]
|
||||
]).addTo(mymap).bindPopup("I am a polygon.");
|
||||
|
||||
|
||||
var popup = L.popup();
|
||||
|
||||
function onMapClick(e) {
|
||||
popup
|
||||
.setLatLng(e.latlng)
|
||||
.setContent("You clicked the map at " + e.latlng.toString())
|
||||
.openOn(mymap);
|
||||
}
|
||||
|
||||
mymap.on('click', onMapClick);
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,144 @@
|
||||
---
|
||||
layout: tutorial_v2
|
||||
title: Quick Start Guide
|
||||
---
|
||||
|
||||
## Leaflet Quick Start Guide
|
||||
|
||||
This step-by-step guide will quickly get you started on Leaflet basics, including setting up a Leaflet map, working with markers, polylines and popups, and dealing with events.
|
||||
|
||||
{% include frame.html url="example.html" %}
|
||||
|
||||
### Preparing your page
|
||||
|
||||
Before writing any code for the map, you need to do the following preparation steps on your page:
|
||||
|
||||
* Include Leaflet CSS file in the head section of your document:
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@{{ site.latest_leaflet_version}}/dist/leaflet.css"
|
||||
integrity="{{site.integrity_hash_css}}"
|
||||
crossorigin=""/>
|
||||
|
||||
* Include Leaflet JavaScript file **after** Leaflet's CSS:
|
||||
|
||||
<!-- Make sure you put this AFTER Leaflet's CSS -->
|
||||
<script src="https://unpkg.com/leaflet@{{ site.latest_leaflet_version}}/dist/leaflet.js"
|
||||
integrity="{{site.integrity_hash_uglified}}"
|
||||
crossorigin=""></script>
|
||||
|
||||
* Put a `div` element with a certain `id` where you want your map to be:
|
||||
|
||||
<div id="mapid"></div>
|
||||
|
||||
* Make sure the map container has a defined height, for example by setting it in CSS:
|
||||
|
||||
<pre><code class="css">#mapid { height: 180px; }</code></pre>
|
||||
|
||||
Now you're ready to initialize the map and do some stuff with it.
|
||||
|
||||
|
||||
### Setting up the map
|
||||
|
||||
|
||||
{% include frame.html url="example-basic.html" %}
|
||||
|
||||
Let's create a map of the center of London with pretty Mapbox Streets tiles. First we'll initialize the map and set its view to our chosen geographical coordinates and a zoom level:
|
||||
|
||||
var mymap = L.map('mapid').setView([51.505, -0.09], 13);
|
||||
|
||||
By default (as we didn't pass any options when creating the map instance), all mouse and touch interactions on the map are enabled, and it has zoom and attribution controls.
|
||||
|
||||
Note that `setView` call also returns the map object --- most Leaflet methods act like this when they don't return an explicit value, which allows convenient jQuery-like method chaining.
|
||||
|
||||
Next we'll add a tile layer to add to our map, in this case it's a Mapbox Streets tile layer. Creating a tile layer usually involves setting the [URL template](http://leafletjs.com/reference.html#url-template) for the tile images, the attribution text and the maximum zoom level of the layer. In this example we'll use the `mapbox.streets` tiles from [Mapbox's "Classic maps"](https://www.mapbox.com/api-documentation/#maps) (in order to use tiles from Mapbox, you must also [request an access token](https://www.mapbox.com/studio/account/tokens/)).
|
||||
|
||||
<pre><code class="javascript">L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', {
|
||||
attribution: 'Map data &copy; <span class="text-cut" data-cut="[…]"><a href="http://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="http://mapbox.com">Mapbox</a></span>',
|
||||
maxZoom: 18,
|
||||
id: 'mapbox.streets',
|
||||
accessToken: 'your.mapbox.access.token'
|
||||
}).addTo(mymap);</code></pre>
|
||||
|
||||
Make sure all the code is called after the `div` and `leaflet.js` inclusion. That's it! You have a working Leaflet map now.
|
||||
|
||||
It's worth noting that Leaflet is provider-agnostic, meaning that it doesn't enforce a particular choice of providers for tiles. You can try replacing `mapbox.streets` with `mapbox.satellite`, and see what happens. Also, Leaflet doesn't even contain a single provider-specific line of code, so you're free to use other providers if you need to (we'd suggest Mapbox though, it looks beautiful).
|
||||
|
||||
|
||||
### Markers, circles and polygons
|
||||
|
||||
{% include frame.html url="example-overlays.html" %}
|
||||
|
||||
|
||||
Besides tile layers, you can easily add other things to your map, including markers, polylines, polygons, circles, and popups. Let's add a marker:
|
||||
|
||||
var marker = L.marker([51.5, -0.09]).addTo(mymap);
|
||||
|
||||
Adding a circle is the same (except for specifying the radius in meters as a second argument), but lets you control how it looks by passing options as the last argument when creating the object:
|
||||
|
||||
var circle = L.circle([51.508, -0.11], {
|
||||
color: 'red',
|
||||
fillColor: '#f03',
|
||||
fillOpacity: 0.5,
|
||||
radius: 500
|
||||
}).addTo(mymap);
|
||||
|
||||
Adding a polygon is as easy:
|
||||
|
||||
var polygon = L.polygon([
|
||||
[51.509, -0.08],
|
||||
[51.503, -0.06],
|
||||
[51.51, -0.047]
|
||||
]).addTo(mymap);
|
||||
|
||||
|
||||
### Working with popups
|
||||
|
||||
{% include frame.html url="example-popups.html" %}
|
||||
|
||||
Popups are usually used when you want to attach some information to a particular object on a map. Leaflet has a very handy shortcut for this:
|
||||
|
||||
marker.bindPopup("<b>Hello world!</b><br>I am a popup.").openPopup();
|
||||
circle.bindPopup("I am a circle.");
|
||||
polygon.bindPopup("I am a polygon.");
|
||||
|
||||
Try clicking on our objects. The `bindPopup` method attaches a popup with the specified HTML content to your marker so the popup appears when you click on the object, and the `openPopup` method (for markers only) immediately opens the attached popup.
|
||||
|
||||
You can also use popups as layers (when you need something more than attaching a popup to an object):
|
||||
|
||||
var popup = L.popup()
|
||||
.setLatLng([51.5, -0.09])
|
||||
.setContent("I am a standalone popup.")
|
||||
.openOn(mymap);
|
||||
|
||||
Here we use `openOn` instead of `addTo` because it handles automatic closing of a previously opened popup when opening a new one which is good for usability.
|
||||
|
||||
|
||||
### Dealing with events
|
||||
|
||||
Every time something happens in Leaflet, e.g. user clicks on a marker or map zoom changes, the corresponding object sends an event which you can subscribe to with a function. It allows you to react to user interaction:
|
||||
|
||||
function onMapClick(e) {
|
||||
alert("You clicked the map at " + e.latlng);
|
||||
}
|
||||
|
||||
mymap.on('click', onMapClick);
|
||||
|
||||
Each object has its own set of events --- see [documentation](../../reference.html) for details. The first argument of the listener function is an event object --- it contains useful information about the event that happened. For example, map click event object (`e` in the example above) has `latlng` property which is a location at which the click occured.
|
||||
|
||||
Let's improve our example by using a popup instead of an alert:
|
||||
|
||||
var popup = L.popup();
|
||||
|
||||
function onMapClick(e) {
|
||||
popup
|
||||
.setLatLng(e.latlng)
|
||||
.setContent("You clicked the map at " + e.latlng.toString())
|
||||
.openOn(mymap);
|
||||
}
|
||||
|
||||
mymap.on('click', onMapClick);
|
||||
|
||||
Try clicking on the map and you will see the coordinates in a popup. <a target="_blank" href="example.html">View the full example →</a>
|
||||
|
||||
Now you've learned Leaflet basics and can start building map apps straight away! Don't forget to take a look at the detailed <a href="../../reference.html">documentation</a> or <a href="../../examples.html">other examples</a>.
|
||||
|
||||
|
After Width: | Height: | Size: 477 KiB |
@@ -0,0 +1,23 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Video Overlay Tutorial
|
||||
---
|
||||
<script>
|
||||
var map = L.map('map');
|
||||
|
||||
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
|
||||
maxZoom: 18,
|
||||
attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
|
||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
|
||||
'Imagery © <a href="http://mapbox.com">Mapbox</a>',
|
||||
id: 'mapbox.satellite'
|
||||
}).addTo(map);
|
||||
|
||||
bounds = L.latLngBounds([[ 32, -130], [ 13, -100]]);
|
||||
|
||||
L.rectangle(bounds).addTo(map);
|
||||
|
||||
map.fitBounds(bounds);
|
||||
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Video Overlay Tutorial
|
||||
---
|
||||
<script>
|
||||
var map = L.map('map');
|
||||
|
||||
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
|
||||
maxZoom: 18,
|
||||
attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
|
||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
|
||||
'Imagery © <a href="http://mapbox.com">Mapbox</a>',
|
||||
id: 'mapbox.satellite'
|
||||
}).addTo(map);
|
||||
|
||||
var videoUrls = [
|
||||
'https://www.mapbox.com/bites/00188/patricia_nasa.webm',
|
||||
'https://www.mapbox.com/bites/00188/patricia_nasa.mp4'
|
||||
],
|
||||
bounds = L.latLngBounds([[ 32, -130], [ 13, -100]]);
|
||||
|
||||
map.fitBounds(bounds);
|
||||
|
||||
var overlay = L.videoOverlay(videoUrls, bounds, {
|
||||
opacity: 0.8,
|
||||
interactive: true
|
||||
});
|
||||
map.addLayer(overlay);
|
||||
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Video Overlay Tutorial
|
||||
---
|
||||
<script>
|
||||
var map = L.map('map');
|
||||
|
||||
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
|
||||
maxZoom: 18,
|
||||
attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
|
||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
|
||||
'Imagery © <a href="http://mapbox.com">Mapbox</a>',
|
||||
id: 'mapbox.satellite'
|
||||
}).addTo(map);
|
||||
|
||||
var videoUrls = [
|
||||
'https://www.mapbox.com/bites/00188/patricia_nasa.webm',
|
||||
'https://www.mapbox.com/bites/00188/patricia_nasa.mp4'
|
||||
],
|
||||
bounds = L.latLngBounds([[ 32, -130], [ 13, -100]]);
|
||||
|
||||
map.fitBounds(bounds);
|
||||
|
||||
var overlay = L.videoOverlay(videoUrls, bounds, {
|
||||
opacity: 0.8,
|
||||
interactive: false,
|
||||
autoplay: false
|
||||
});
|
||||
map.addLayer(overlay);
|
||||
|
||||
overlay.on('load', function () {
|
||||
var MyPauseControl = L.Control.extend({
|
||||
onAdd: function() {
|
||||
var button = L.DomUtil.create('button');
|
||||
button.innerHTML = '⏸';
|
||||
L.DomEvent.on(button, 'click', function () {
|
||||
overlay.getElement().pause();
|
||||
});
|
||||
return button;
|
||||
}
|
||||
});
|
||||
var MyPlayControl = L.Control.extend({
|
||||
onAdd: function() {
|
||||
var button = L.DomUtil.create('button');
|
||||
button.innerHTML = '⏵';
|
||||
L.DomEvent.on(button, 'click', function () {
|
||||
overlay.getElement().play();
|
||||
});
|
||||
return button;
|
||||
}
|
||||
});
|
||||
|
||||
var pauseControl = (new MyPauseControl()).addTo(map);
|
||||
var playControl = (new MyPlayControl()).addTo(map);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
layout: tutorial_v2
|
||||
title: Leaflet on Mobile
|
||||
---
|
||||
|
||||
## Video on webpages
|
||||
|
||||
Video used to be a hard task when building a webpage, until the [`<video>` HTML element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video) was made available.
|
||||
|
||||
Nowadays, we can use the following HTML code:
|
||||
|
||||
<video width="500" controls>
|
||||
<source src="https://www.mapbox.com/bites/00188/patricia_nasa.webm" type="video/webm">
|
||||
<source src="https://www.mapbox.com/bites/00188/patricia_nasa.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
To display this video:
|
||||
|
||||
<video width="500" controls>
|
||||
<source src="https://www.mapbox.com/bites/00188/patricia_nasa.webm" type="video/webm">
|
||||
<source src="https://www.mapbox.com/bites/00188/patricia_nasa.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
If a video can be shown in a webpage in this way, then Leaflet can display it inside a map. It is important that the videos are prepared in such a way that they will fit the map: The video should have a "north-up" orientation, and its proportions should fit the map. If not, it will look out of place.
|
||||
|
||||
### Bounds of an image overlay
|
||||
|
||||
First of all, create a Leaflet map and add a background `L.TileLayer` in the usual way:
|
||||
|
||||
var map = L.map('map').setView([37.8, -96], 4);
|
||||
|
||||
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=' + mapboxAccessToken, {
|
||||
id: 'mapbox.satellite',
|
||||
attribution: ...
|
||||
}).addTo(map);
|
||||
|
||||
Then, we'll define the geographical bounds that the video will cover. This is an instance of [`L.LatLngBounds`](../../reference.html#latlngbounds), which is a rectangular shape:
|
||||
|
||||
var bounds = L.latLngBounds([[ 32, -130], [ 13, -100]]);
|
||||
|
||||
If you want to see the area covered by a `LatLngBounds`, use a [`L.Rectangle`]((../../reference.html#latlngbounds)):
|
||||
|
||||
L.rectangle(bounds).addTo(map);
|
||||
|
||||
map.fitBounds(bounds);
|
||||
|
||||
{% include frame.html url="example-bounds.html" %}
|
||||
|
||||
|
||||
### Adding the video overlay
|
||||
|
||||
Adding a video overlay works very similar to adding a image overlay. For just one image, [`L.ImageOverlay`s](../../reference.html#imageoverlay) is used like this:
|
||||
|
||||
var overlay = L.imageOverlay( imageUrl, bounds, options );
|
||||
|
||||
For a video overlay, just:
|
||||
|
||||
* Use `L.videoOverlay` instead of `L.imageOverlay`
|
||||
* Instead of the image URL, specify one video URL *or* an array of video URLs
|
||||
|
||||
```
|
||||
var videoUrls = [
|
||||
'https://www.mapbox.com/bites/00188/patricia_nasa.webm',
|
||||
'https://www.mapbox.com/bites/00188/patricia_nasa.mp4'
|
||||
];
|
||||
|
||||
var bounds = L.latLngBounds([[ 32, -130], [ 13, -100]]);
|
||||
|
||||
var videoOverlay = L.videoOverlay( videoUrls, bounds, {
|
||||
opacity: 0.8
|
||||
}).addTo(map);
|
||||
```
|
||||
|
||||
And just like that, you'll get the video on your map:
|
||||
|
||||
{% include frame.html url="example-nocontrols.html" %}
|
||||
|
||||
|
||||
Video overlays behave like any other Leaflet layer - you can add and remove them, let the user select from several videos using a [layers control](../layers-control/), etc.
|
||||
|
||||
|
||||
### A bit of control over the video
|
||||
|
||||
If you read the API documentation, you'll notice that the `L.VideoOverlay` class does not have a `play()` or `pause()` method.
|
||||
|
||||
For this, the `getElement()` method of the video overlay is useful. It returns the [`HTMLVideoElement`](https://developer.mozilla.org/docs/Web/API/HTMLImageElement) (which inherits from [`HTMLMediaElement`](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement)) for the overlay - and that has methods like `play()` and `pause()`, e.g.
|
||||
|
||||
```
|
||||
videoOverlay.getElement().pause();
|
||||
```
|
||||
|
||||
This allows us to build custom interfaces. For example, we can build a small subclass of `L.Control` to play/pause this video overlay once it's loaded:
|
||||
|
||||
```
|
||||
videoOverlay.on('load', function () {
|
||||
var MyPauseControl = L.Control.extend({
|
||||
onAdd: function() {
|
||||
var button = L.DomUtil.create('button');
|
||||
button.innerHTML = '⏸';
|
||||
L.DomEvent.on(button, 'click', function () {
|
||||
videoOverlay.getElement().pause();
|
||||
});
|
||||
return button;
|
||||
}
|
||||
});
|
||||
var MyPlayControl = L.Control.extend({
|
||||
onAdd: function() {
|
||||
var button = L.DomUtil.create('button');
|
||||
button.innerHTML = '⏵';
|
||||
L.DomEvent.on(button, 'click', function () {
|
||||
videoOverlay.getElement().play();
|
||||
});
|
||||
return button;
|
||||
}
|
||||
});
|
||||
|
||||
var pauseControl = (new MyPauseControl()).addTo(map);
|
||||
var playControl = (new MyPlayControl()).addTo(map);
|
||||
});
|
||||
```
|
||||
|
||||
{% include frame.html url="example.html" %}
|
||||
|
After Width: | Height: | Size: 381 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 196 KiB |
@@ -0,0 +1,18 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: WMS example
|
||||
---
|
||||
<script type='text/javascript'>
|
||||
|
||||
var map = L.map('map', {
|
||||
center: [0, 0],
|
||||
zoom: 1,
|
||||
crs: L.CRS.EPSG4326
|
||||
});
|
||||
|
||||
var wmsLayer = L.tileLayer.wms('http://demo.opengeo.org/geoserver/ows?', {
|
||||
layers: 'nasa:bluemarble'
|
||||
}).addTo(map);
|
||||
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: WMS example
|
||||
---
|
||||
<script type='text/javascript'>
|
||||
|
||||
var map = L.map('map', {
|
||||
center: [-17, -67],
|
||||
zoom: 3
|
||||
});
|
||||
|
||||
var wmsLayer = L.tileLayer.wms('http://demo.opengeo.org/geoserver/ows?', {
|
||||
layers: 'ne:ne'
|
||||
}).addTo(map);
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: WMS example
|
||||
---
|
||||
<script type='text/javascript'>
|
||||
|
||||
var map = L.map('map', {
|
||||
center: [-17, -67],
|
||||
zoom: 3
|
||||
});
|
||||
|
||||
var wmsLayer = L.tileLayer.wms('http://demo.opengeo.org/geoserver/ows?', {
|
||||
layers: 'nasa:bluemarble'
|
||||
}).addTo(map);
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: WMS example
|
||||
---
|
||||
<script type='text/javascript'>
|
||||
|
||||
var map = L.map('map', {
|
||||
center: [-17, -67],
|
||||
zoom: 3
|
||||
});
|
||||
|
||||
var basemaps = {
|
||||
Countries: L.tileLayer.wms('http://demo.opengeo.org/geoserver/ows?', {
|
||||
layers: 'ne:ne_10m_admin_0_countries'
|
||||
}),
|
||||
|
||||
Boundaries: L.tileLayer.wms('http://demo.opengeo.org/geoserver/ows?', {
|
||||
layers: 'ne:ne_10m_admin_0_boundary_lines_land'
|
||||
}),
|
||||
|
||||
'Countries, then boundaries': L.tileLayer.wms('http://demo.opengeo.org/geoserver/ows?', {
|
||||
layers: 'ne:ne_10m_admin_0_countries,ne:ne_10m_admin_0_boundary_lines_land'
|
||||
}),
|
||||
|
||||
'Boundaries, then countries': L.tileLayer.wms('http://demo.opengeo.org/geoserver/ows?', {
|
||||
layers: 'ne:ne_10m_admin_0_boundary_lines_land,ne:ne_10m_admin_0_countries'
|
||||
})
|
||||
};
|
||||
|
||||
L.control.layers(basemaps, {}, {collapsed: false}).addTo(map);
|
||||
|
||||
basemaps.Countries.addTo(map);
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: WMS example
|
||||
---
|
||||
<script type='text/javascript'>
|
||||
|
||||
var map = L.map('map', {
|
||||
center: [-17, -67],
|
||||
zoom: 3
|
||||
});
|
||||
|
||||
var tms_ne = L.tileLayer('http://demo.opengeo.org/geoserver/gwc/service/tms/1.0.0/ne:ne@EPSG:900913@png/{z}/{x}/{y}.png', {
|
||||
tms: true
|
||||
}).addTo(map);
|
||||
|
||||
var tms_bluemarble = L.tileLayer('http://demo.opengeo.org/geoserver/gwc/service/tms/1.0.0/nasa:bluemarble@EPSG:900913@jpg/{z}/{x}/{y}.jpg', {
|
||||
tms: true
|
||||
});
|
||||
|
||||
var basemaps = {
|
||||
'Natural Earth': tms_ne,
|
||||
'NASA Blue Marble': tms_bluemarble
|
||||
};
|
||||
|
||||
L.control.layers(basemaps, {}, {collapsed: false}).addTo(map);
|
||||
|
||||
basemaps.Countries.addTo(map);
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,157 @@
|
||||
---
|
||||
layout: tutorial_v2
|
||||
title: Using WMS and TMS services
|
||||
---
|
||||
|
||||
<style>
|
||||
iframe {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<br/>
|
||||
|
||||
WMS, short for [*web map service*](https://en.wikipedia.org/wiki/Web_Map_Service), is a popular way of publishing maps by professional GIS software (and seldomly used by non-GISers). This format is similar to map tiles, but more generic and not so well optimized for use in web maps. A WMS image is defined by the coordinates of its corners - a calculation that Leaflet does under the hood.
|
||||
|
||||
TMS stands for *tiled map service*, and is a map tiling standard more focused on web maps, very similar to the map tiles that Leaflet expects in a `L.TileLayer`.
|
||||
|
||||
## WMS in Leaflet
|
||||
|
||||
When somebody publishes a WMS service, most likely they link to something called a `GetCapabilities` document. For this tutorial, we'll use the demo map services from GeoServer, at http://demo.opengeo.org/geoserver/web/. As you can see in that page, "WMS" links to the following URL:
|
||||
|
||||
http://demo.opengeo.org/geoserver/ows?service=wms&version=1.3.0&request=GetCapabilities
|
||||
|
||||
Leaflet does not understand WMS `GetCapabilities` documents. Instead, you have to create a `L.TileLayer.WMS` layer, provide the base WMS URL, and specify whatever WMS options you need.
|
||||
|
||||
The base WMS URL is simply the `GetCapabilities` URL, without any parameters, like so:
|
||||
|
||||
http://demo.opengeo.org/geoserver/ows?
|
||||
|
||||
And the way to use that in a Leaflet map is simply:
|
||||
|
||||
var map = L.map(mapDiv, mapOptions);
|
||||
|
||||
var wmsLayer = L.tileLayer.wms('http://demo.opengeo.org/geoserver/ows?', wmsOptions).addTo(map);
|
||||
|
||||
An instance of `L.TileLayer.WMS` needs at least one option: `layers`. Be careful, as the concept of "layer" in Leaflet is different from the concept of "layer" in WMS!
|
||||
|
||||
WMS servers define a set of *layers* in the service. These are defined in the `GetCapabilities` XML document, which most times is tedious and difficult to understand. Usually it's a good idea to use software such as [QGIS to see what layers are available in a WMS server](http://www.qgistutorials.com/en/docs/working_with_wms.html) to see the layer names available:
|
||||
|
||||

|
||||
|
||||
We can see that the OpenGeo demo WMS has a WMS layer named `ne:ne` with a basemap. Let's see how it looks:
|
||||
|
||||
var wmsLayer = L.tileLayer.wms('http://demo.opengeo.org/geoserver/ows?', {
|
||||
layers: 'ne:ne'
|
||||
}).addTo(map);
|
||||
|
||||
{% include frame.html url="wms-example1.html" %}
|
||||
|
||||
|
||||
Or we can try the `nasa:bluemarble` WMS layer:
|
||||
|
||||
var wmsLayer = L.tileLayer.wms('http://demo.opengeo.org/geoserver/ows?', {
|
||||
layers: 'nasa:bluemarble'
|
||||
}).addTo(map);
|
||||
|
||||
{% include frame.html url="wms-example2.html" %}
|
||||
|
||||
|
||||
The `layers` option is a comma-separated list of layers. If a WMS service has defined several layers, then a request for a map image can refer to more than one layer.
|
||||
|
||||
For the example WMS server we're using, there is a `ne:ne_10m_admin_0_countries` WMS layer showing country landmasses and country names, and a `ne:ne_10m_admin_0_boundary_lines_land` WMS layer showing country boundaries. The WMS server will compose both layers in one image if we request both, separated with a comma:
|
||||
|
||||
var countriesAndBoundaries = L.tileLayer.wms('http://demo.opengeo.org/geoserver/ows?', {
|
||||
layers: 'ne:ne_10m_admin_0_countries,ne:ne_10m_admin_0_boundary_lines_land'
|
||||
}).addTo(map);
|
||||
|
||||
Note this will request *one* image to the WMS server. This is different than creating a `L.TileLayer.WMS` for the countries, another one for the boundaries, and adding them both to the map. In the first case, there is one image request and it's the WMS server who decides how to compose (put on top of each other) the image. In the second case, there would be two image requests and it's the Leaflet code running in the web browser who decides how to compose them.
|
||||
|
||||
If we combine this with the [layers control](/examples/layers-control.html), then we can build a simple map to see the difference:
|
||||
|
||||
var basemaps = {
|
||||
Countries: L.tileLayer.wms('http://demo.opengeo.org/geoserver/ows?', {
|
||||
layers: 'ne:ne_10m_admin_0_countries'
|
||||
}),
|
||||
|
||||
Boundaries: L.tileLayer.wms('http://demo.opengeo.org/geoserver/ows?', {
|
||||
layers: 'ne:ne_10m_admin_0_boundary_lines_land'
|
||||
}),
|
||||
|
||||
'Countries, then boundaries': L.tileLayer.wms('http://demo.opengeo.org/geoserver/ows?', {
|
||||
layers: 'ne:ne_10m_admin_0_countries,ne:ne_10m_admin_0_boundary_lines_land'
|
||||
}),
|
||||
|
||||
'Boundaries, then countries': L.tileLayer.wms('http://demo.opengeo.org/geoserver/ows?', {
|
||||
layers: 'ne:ne_10m_admin_0_boundary_lines_land,ne:ne_10m_admin_0_countries'
|
||||
})
|
||||
};
|
||||
|
||||
L.control.layers(basemaps).addTo(map);
|
||||
|
||||
basemaps.Countries.addTo(map);
|
||||
|
||||
Change to the "Countries, then boundaries" option, so you can see the boundaries "on top" of the landmasses, but the WMS server is clever enough to display building labels on top of that. It's up to the WMS server how to compose layers when asked for many.
|
||||
|
||||
{% include frame.html url="wms-example3.html" %}
|
||||
|
||||
|
||||
### Notes to GIS users of WMS services
|
||||
|
||||
From a GIS point of view, WMS handling in Leaflet is quite basic. There's no `GetCapabilities` support, no legend support, and no `GetFeatureInfo` support.
|
||||
|
||||
`L.TileLayer.WMS` has extra options, which can be found in [Leaflet's API documentation](../../reference.html#tilelayer-wms-options). Any option not described there will be passed to the WMS server in the `getImage` URLs.
|
||||
|
||||
Also note that Leaflet supports very few [coordinate systems](https://en.wikipedia.org/wiki/Spatial_reference_system): `CRS:3857`, `CRS:3395` and `CRS:4326` (See the documentation for `L.CRS`). If your WMS service doesn't serve images in those coordinate systems, you might need to use [Proj4Leaflet](https://github.com/kartena/Proj4Leaflet) to use a different coordinate system in Leaflet. Other than that, just use the right CRS when initializing your map, and any WMS layers added will use it:
|
||||
|
||||
var map = L.map('map', {
|
||||
crs: L.CRS.EPSG4326
|
||||
});
|
||||
|
||||
var wmsLayer = L.tileLayer.wms('http://demo.opengeo.org/geoserver/ows?', {
|
||||
layers: 'nasa:bluemarble'
|
||||
}).addTo(map);
|
||||
|
||||
{% include frame.html url="wms-example-crs.html" %}
|
||||
|
||||
|
||||
## TMS in Leaflet
|
||||
|
||||
Leaflet doesn't have explicit support for TMS services, but the tile naming structure is so similar to the common `L.TileLayer` naming scheme, that displaying a TMS service is almost trivial.
|
||||
|
||||
Using the same OpenGeo WMS/TMS server demo, we can see there's a TMS endpoint at:
|
||||
|
||||
http://demo.opengeo.org/geoserver/gwc/service/tms/1.0.0
|
||||
|
||||
Checking the [MapCache help about TMS](http://mapserver.org/mapcache/services.html) and the [TMS specification](https://wiki.osgeo.org/wiki/Tile_Map_Service_Specification) you can see that the URL for a map tile in TMS looks like:
|
||||
|
||||
http://base_url/tms/1.0.0/ {tileset} / {z} / {x} / {y} .png
|
||||
|
||||
To use the OpenGeo TMS services as a `L.TileLayer`, we can check the capabilities document (the same as the base endpoint, in our case [`http://demo.opengeo.org/geoserver/gwc/service/tms/1.0.0`](http://demo.opengeo.org/geoserver/gwc/service/tms/1.0.0)) to see what `tileset`s are available, and build our base URLs:
|
||||
|
||||
http://demo.opengeo.org/geoserver/gwc/service/tms/1.0.0/ne:ne@EPSG:900913@png/{z}/{x}/{y}.png
|
||||
|
||||
http://demo.opengeo.org/geoserver/gwc/service/tms/1.0.0/nasa:bluemarble@EPSG:900913@jpg/{z}/{x}/{y}.jpg
|
||||
|
||||
|
||||
And use the `tms:true` option when instantiating the layers, like so:
|
||||
|
||||
var tms_ne = L.tileLayer('http://demo.opengeo.org/geoserver/gwc/service/tms/1.0.0/ne:ne@EPSG:900913@png/{z}/{x}/{y}.png', {
|
||||
tms: true
|
||||
}).addTo(map);
|
||||
|
||||
var tms_bluemarble = L.tileLayer('http://demo.opengeo.org/geoserver/gwc/service/tms/1.0.0/nasa:bluemarble@EPSG:900913@jpg/{z}/{x}/{y}.jpg', {
|
||||
tms: true
|
||||
});
|
||||
|
||||
{% include frame.html url="wms-example4.html" %}
|
||||
|
||||
|
||||
A new feature in **Leaflet 1.0** is the ability to use `{-y}` in the URL instead of a `tms: true` option, e.g.:
|
||||
|
||||
var layer = L.tileLayer('http://base_url/tms/1.0.0/tileset/{z}/{x}/{-y}.png');
|
||||
|
||||
The `tms: true` option (in Leaflet 0.7) or `{-y}` (in Leaflet 1.0) are needed because the origin of coordinates of vanilla `L.TileLayer`s is the top left corner, so the Y coordinate goes *down*. In TMS, the origin of coordinates is the *bottom* left corner so the Y coordinate goes *up*.
|
||||
|
||||
Besides the difference in the `y` coordinate and the discovery of tilesets, TMS services serve tiles exactly in the way that `L.TileLayer` expects.
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Zoom Levels Tutorial
|
||||
---
|
||||
<script>
|
||||
|
||||
var map = L.map('map', {
|
||||
minZoom: 0,
|
||||
maxZoom: 18,
|
||||
zoomSnap: 0,
|
||||
zoomDelta: 0.25
|
||||
});
|
||||
|
||||
var cartodbAttribution = '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <a href="http://cartodb.com/attributions">CartoDB</a>';
|
||||
|
||||
var positron = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {
|
||||
attribution: cartodbAttribution
|
||||
}).addTo(map);
|
||||
|
||||
var ZoomViewer = L.Control.extend({
|
||||
onAdd: function(){
|
||||
|
||||
var container= L.DomUtil.create('div');
|
||||
var gauge = L.DomUtil.create('div');
|
||||
container.style.width = '200px';
|
||||
container.style.background = 'rgba(255,255,255,0.5)';
|
||||
container.style.textAlign = 'left';
|
||||
map.on('zoomstart zoom zoomend', function(ev){
|
||||
gauge.innerHTML = 'Zoom level: ' + map.getZoom();
|
||||
})
|
||||
container.appendChild(gauge);
|
||||
|
||||
return container;
|
||||
}
|
||||
});
|
||||
|
||||
(new ZoomViewer).addTo(map);
|
||||
|
||||
map.setView([0, 0], 0);
|
||||
</script>
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Zoom Levels Tutorial
|
||||
---
|
||||
<script>
|
||||
|
||||
var map = L.map('map', {
|
||||
minZoom: 0,
|
||||
maxZoom: 1,
|
||||
zoomSnap: 0.25,
|
||||
dragging: false
|
||||
});
|
||||
|
||||
var cartodbAttribution = '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <a href="http://cartodb.com/attributions">CartoDB</a>';
|
||||
|
||||
var positron = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {
|
||||
attribution: cartodbAttribution
|
||||
}).addTo(map);
|
||||
|
||||
|
||||
function zoomCycle(){
|
||||
map.setZoom(0);
|
||||
timeouts = [];
|
||||
timeouts.push(setTimeout(function(){ map.setZoom(0.25); }, 1000));
|
||||
timeouts.push(setTimeout(function(){ map.setZoom(0.50); }, 2000));
|
||||
timeouts.push(setTimeout(function(){ map.setZoom(0.75); }, 3000));
|
||||
timeouts.push(setTimeout(function(){ map.setZoom(1); }, 4000));
|
||||
timeouts.push(setTimeout(function(){ map.setZoom(0.75); }, 5000));
|
||||
timeouts.push(setTimeout(function(){ map.setZoom(0.50); }, 6000));
|
||||
timeouts.push(setTimeout(function(){ map.setZoom(0.25); }, 7000));
|
||||
}
|
||||
zoomCycle();
|
||||
|
||||
var zoomingInterval = setInterval(zoomCycle, 8000);
|
||||
|
||||
var ZoomViewer = L.Control.extend({
|
||||
onAdd: function(){
|
||||
|
||||
var container= L.DomUtil.create('div');
|
||||
var gauge = L.DomUtil.create('div');
|
||||
container.style.width = '200px';
|
||||
container.style.background = 'rgba(255,255,255,0.5)';
|
||||
container.style.textAlign = 'left';
|
||||
map.on('zoomstart zoom zoomend', function(ev){
|
||||
gauge.innerHTML = 'Zoom level: ' + map.getZoom();
|
||||
})
|
||||
container.appendChild(gauge);
|
||||
|
||||
return container;
|
||||
}
|
||||
});
|
||||
|
||||
(new ZoomViewer).addTo(map);
|
||||
|
||||
map.setView([0, 0], 0);
|
||||
</script>
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Zoom Levels Tutorial
|
||||
---
|
||||
<script>
|
||||
|
||||
var map = L.map('map', {
|
||||
minZoom: 1,
|
||||
maxZoom: 1,
|
||||
dragging: false
|
||||
});
|
||||
|
||||
var cartodbAttribution = '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <a href="http://cartodb.com/attributions">CartoDB</a>';
|
||||
|
||||
var positron = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {
|
||||
attribution: cartodbAttribution
|
||||
}).addTo(map);
|
||||
|
||||
L.control.scale({maxWidth: 150}).addTo(map);
|
||||
|
||||
setInterval(function(){
|
||||
map.setView([0, 0], 0, {duration: 1, animate: true});
|
||||
setTimeout(function(){
|
||||
map.setView([60, 0], 0, {duration: 1, animate: true});
|
||||
}, 2000);
|
||||
}, 4000);
|
||||
|
||||
map.setView([0, 0], 0);
|
||||
</script>
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Zoom Levels Tutorial
|
||||
---
|
||||
<script>
|
||||
|
||||
var map = L.map('map', {
|
||||
minZoom: 0,
|
||||
maxZoom: 1
|
||||
});
|
||||
|
||||
var cartodbAttribution = '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <a href="http://cartodb.com/attributions">CartoDB</a>';
|
||||
|
||||
var positron = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {
|
||||
attribution: cartodbAttribution
|
||||
}).addTo(map);
|
||||
|
||||
setInterval(function(){
|
||||
|
||||
map.setZoom(0);
|
||||
|
||||
setTimeout(function(){
|
||||
map.setZoom(1);
|
||||
}, 2000);
|
||||
|
||||
}, 4000);
|
||||
|
||||
var ZoomViewer = L.Control.extend({
|
||||
onAdd: function(){
|
||||
var gauge = L.DomUtil.create('div');
|
||||
gauge.style.width = '200px';
|
||||
gauge.style.background = 'rgba(255,255,255,0.5)';
|
||||
gauge.style.textAlign = 'left';
|
||||
map.on('zoomstart zoom zoomend', function(ev){
|
||||
gauge.innerHTML = 'Zoom level: ' + map.getZoom();
|
||||
})
|
||||
return gauge;
|
||||
}
|
||||
});
|
||||
|
||||
(new ZoomViewer).addTo(map);
|
||||
|
||||
map.setView([0, 0], 0);
|
||||
</script>
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
layout: tutorial_frame
|
||||
title: Zoom Levels Tutorial
|
||||
---
|
||||
<script>
|
||||
|
||||
var map = L.map('map', {
|
||||
minZoom: 0,
|
||||
maxZoom: 0
|
||||
});
|
||||
|
||||
var cartodbAttribution = '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <a href="http://cartodb.com/attributions">CartoDB</a>';
|
||||
|
||||
var positron = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {
|
||||
attribution: cartodbAttribution
|
||||
}).addTo(map);
|
||||
|
||||
map.setView([0, 0], 0);
|
||||
</script>
|
||||
@@ -0,0 +1,275 @@
|
||||
---
|
||||
layout: tutorial_v2
|
||||
title: Zoom levels
|
||||
---
|
||||
|
||||
<style>
|
||||
.tiles img {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
margin: 5px;
|
||||
}
|
||||
.tiles.small img {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
margin: 1px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
.tiles {
|
||||
line-height: 0;
|
||||
}
|
||||
.tiles.legend {
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
## Zoom levels
|
||||
|
||||
Leaflet works with [latitude](https://en.wikipedia.org/wiki/Latitude), [longitude](https://en.wikipedia.org/wiki/Longitude) and "zoom level".
|
||||
|
||||
Lower zoom levels means that the map shows entire continents, while higher zoom
|
||||
levels means that the map can show details of a city.
|
||||
|
||||
To understand how zoom levels work, first we need a basic introduction to <i>geodesy</i>.
|
||||
|
||||
## The shape of the earth
|
||||
|
||||
Let's have a look at a simple map locked at zoom zero:
|
||||
|
||||
```
|
||||
var map = L.map('map', {
|
||||
minZoom: 0,
|
||||
maxZoom: 0
|
||||
});
|
||||
|
||||
var positron = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {
|
||||
attribution: cartodbAttribution
|
||||
}).addTo(map);
|
||||
|
||||
map.setView([0, 0], 0);
|
||||
```
|
||||
|
||||
{% include frame.html url="example-zero.html" %}
|
||||
|
||||
Notice that the "whole earth" is just one image, 256 pixels wide and 256 pixels high:
|
||||
|
||||
<div class='tiles' style='text-align: center'>
|
||||
<img src="http://a.basemaps.cartocdn.com/light_all/0/0/0.png" class="bordered-img" alt=""/>
|
||||
</div>
|
||||
|
||||
Just to be clear: the earth is not a square. Rather, the earth is shaped like [a weird potato](https://commons.wikimedia.org/wiki/File:GRACE_globe_animation.gif) that can be approximated to [something similar to a sphere](https://en.wikipedia.org/wiki/Geoid).
|
||||
|
||||
<div class='tiles legend' style='text-align: center'>
|
||||
<a title="By NASA/JPL/University of Texas Center for Space Research. (http://photojournal.jpl.nasa.gov/catalog/PIA12146) [Public domain], via Wikimedia Commons" href="https://commons.wikimedia.org/wiki/File%3AGRACE_globe_animation.gif"><img width="256" alt="GRACE globe animation" src="https://upload.wikimedia.org/wikipedia/commons/7/78/GRACE_globe_animation.gif"/>
|
||||
<br/>
|
||||
Potato earth image by NASA/JPL/University of Texas Center for Space Research</a>
|
||||
with help of the <a href='https://en.wikipedia.org/wiki/Gravity_Recovery_and_Climate_Experiment'>GRACE satellites</a>.
|
||||
</div>
|
||||
|
||||
So we *assume* that the earth is mosly round. To make it flat, we put an imaginary cylinder around, unroll it, and cut it so it looks square:
|
||||
|
||||
<div class='tiles legend' style='text-align: center'>
|
||||
<a title="By derived from US Government USGS [Public domain], via Wikimedia Commons" href="https://en.wikipedia.org/wiki/Map_projection#Cylindrical"><img width="512" alt="Usgs map mercator" src="https://upload.wikimedia.org/wikipedia/commons/thumb/6/62/Usgs_map_mercator.svg/512px-Usgs_map_mercator.svg.png"/>
|
||||
<br/>
|
||||
This is called a "cylindrical map projection".
|
||||
</a>
|
||||
</div>
|
||||
|
||||
This is not the only way of displaying the surface on the earth on a plane. There
|
||||
are [hundreds of ways](https://en.wikipedia.org/wiki/Map_projection), each of them
|
||||
with its own advantages and disadvantages. The following 6-minute video is a nice
|
||||
introduction to the topic:
|
||||
|
||||
<center><iframe width="696" height="392" src="https://www.youtube.com/embed/kIID5FDi2JQ" frameborder="0" allowfullscreen></iframe></center>
|
||||
|
||||
Things like geodesy, map projections and coordinate systems are hard, *very hard*
|
||||
(and out of scope for this tutorial). Assuming that the earth is a square is not
|
||||
always the right thing to do, but most of the time works fine enough, makes things
|
||||
simpler, and allows Leaflet (and other map libraries) to be fast.
|
||||
|
||||
## Powers of two
|
||||
|
||||
For now, let's just ***assume*** that the world is a square:
|
||||
|
||||
<div class='tiles' style='text-align: center'>
|
||||
<img src="http://a.basemaps.cartocdn.com/light_all/0/0/0.png" class="bordered-img" alt=""/>
|
||||
</div>
|
||||
|
||||
When we represent the world at zoom level **zero**, it's 256 pixels wide and high. When we go into zoom level **one**, it doubles its width and height, and can be represented by four 256-pixel-by-256-pixel images:
|
||||
|
||||
<div class='tiles' style='text-align: center'>
|
||||
<div>
|
||||
<img src="http://a.basemaps.cartocdn.com/light_all/1/0/0.png" class="bordered-img" alt=""/><img src="http://a.basemaps.cartocdn.com/light_all/1/1/0.png" class="bordered-img" alt=""/>
|
||||
</div>
|
||||
<div>
|
||||
<img src="http://a.basemaps.cartocdn.com/light_all/1/0/1.png" class="bordered-img" alt=""/><img src="http://a.basemaps.cartocdn.com/light_all/1/1/1.png" class="bordered-img" alt=""/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
At each zoom level, each tile is divided in four, and its size doubles (in other words, the width and height of the world is <code>256·2<sup>zoomlevel</sup></code> pixels):
|
||||
|
||||
<table><tr><td>
|
||||
<div class='tiles small' style='text-align: center'>
|
||||
<img src="http://a.basemaps.cartocdn.com/light_all/0/0/0.png" class="bordered-img" alt=""/>
|
||||
</div>
|
||||
</td><td>
|
||||
<div class='tiles small' style='text-align: center'>
|
||||
<div>
|
||||
<img src="http://a.basemaps.cartocdn.com/light_all/1/0/0.png" class="bordered-img" alt=""/><img src="http://a.basemaps.cartocdn.com/light_all/1/1/0.png" class="bordered-img" alt=""/>
|
||||
</div>
|
||||
<div>
|
||||
<img src="http://a.basemaps.cartocdn.com/light_all/1/0/1.png" class="bordered-img" alt=""/><img src="http://a.basemaps.cartocdn.com/light_all/1/1/1.png" class="bordered-img" alt=""/>
|
||||
</div>
|
||||
</div>
|
||||
</td><td>
|
||||
<div class='tiles small' style='text-align: center'>
|
||||
<div>
|
||||
<img src="http://a.basemaps.cartocdn.com/light_all/2/0/0.png" class="bordered-img" alt=""/><img src="http://a.basemaps.cartocdn.com/light_all/2/1/0.png" class="bordered-img" alt=""/><img src="http://a.basemaps.cartocdn.com/light_all/2/2/0.png" class="bordered-img" alt=""/><img src="http://a.basemaps.cartocdn.com/light_all/2/3/0.png" class="bordered-img" alt=""/>
|
||||
</div>
|
||||
<div>
|
||||
<img src="http://a.basemaps.cartocdn.com/light_all/2/0/1.png" class="bordered-img" alt=""/><img src="http://a.basemaps.cartocdn.com/light_all/2/1/1.png" class="bordered-img" alt=""/><img src="http://a.basemaps.cartocdn.com/light_all/2/2/1.png" class="bordered-img" alt=""/><img src="http://a.basemaps.cartocdn.com/light_all/2/3/1.png" class="bordered-img" alt=""/>
|
||||
</div>
|
||||
<div>
|
||||
<img src="http://a.basemaps.cartocdn.com/light_all/2/0/2.png" class="bordered-img" alt=""/><img src="http://a.basemaps.cartocdn.com/light_all/2/1/2.png" class="bordered-img" alt=""/><img src="http://a.basemaps.cartocdn.com/light_all/2/2/2.png" class="bordered-img" alt=""/><img src="http://a.basemaps.cartocdn.com/light_all/2/3/2.png" class="bordered-img" alt=""/>
|
||||
</div>
|
||||
<div>
|
||||
<img src="http://a.basemaps.cartocdn.com/light_all/2/0/3.png" class="bordered-img" alt=""/><img src="http://a.basemaps.cartocdn.com/light_all/2/1/3.png" class="bordered-img" alt=""/><img src="http://a.basemaps.cartocdn.com/light_all/2/2/3.png" class="bordered-img" alt=""/><img src="http://a.basemaps.cartocdn.com/light_all/2/3/3.png" class="bordered-img" alt=""/>
|
||||
</div>
|
||||
</div>
|
||||
</td></tr>
|
||||
<tr><td>Zoom 0</td><td>Zoom 1</td><td>Zoom 2</td></tr></table>
|
||||
|
||||
This goes on and on. Most tile services offer tiles up to zoom level 18, depending on
|
||||
their coverage. This is enough to see a few city blocks per tile.
|
||||
|
||||
## A note about scale
|
||||
|
||||
One of the disadvantages of using a cylindrical projection is that the scale is not
|
||||
constant, and measuring distances or sizes is not reliable, specially at low zoom levels.
|
||||
|
||||
In [technical terms](https://en.wikipedia.org/wiki/Map_projection#Projections_by_preservation_of_a_metric_property),
|
||||
the cylindrical projection that Leaflet uses is <i>conformal</i> (preserves shapes),
|
||||
but not <i>equidistant</i> (does not preserve distances), and not <i>equal-area</i>
|
||||
(does not preserve areas, as things near the equator appear smaller than they are).
|
||||
|
||||
By adding a `L.Control.Scale` to a map, and panning to the equator and to 60° north,
|
||||
we can see how the scale factor <b>doubles</b>. The following example uses
|
||||
[javascript timeouts](https://developer.mozilla.org/docs/Web/API/WindowTimers/setTimeout)
|
||||
to do this automatically:
|
||||
|
||||
```
|
||||
L.control.scale().addTo(map);
|
||||
|
||||
setInterval(function(){
|
||||
map.setView([0, 0]);
|
||||
setTimeout(function(){
|
||||
map.setView([60, 0]);
|
||||
}, 2000);
|
||||
}, 4000);
|
||||
```
|
||||
|
||||
{% include frame.html url="example-scale.html" %}
|
||||
|
||||
`L.Control.Scale` shows the scale which applies to the center point of the map.
|
||||
At high zoom levels, the scale changes very little, and is not noticeable.
|
||||
|
||||
|
||||
## Controlling the zoom
|
||||
|
||||
A leaflet map has several ways to control the zoom level shown, but the most obvious
|
||||
one is [`setZoom()`](../../reference-1.0.3.html#map-setzoom). For example, `map.setZoom(0);`
|
||||
will set the zoom level of `map` to `0`.
|
||||
|
||||
This example again uses timeouts to alternate between zoom levels `0` and `1` automatically:
|
||||
|
||||
```
|
||||
setInterval(function(){
|
||||
map.setZoom(0);
|
||||
setTimeout(function(){
|
||||
map.setZoom(1);
|
||||
}, 2000);
|
||||
}, 4000);
|
||||
```
|
||||
|
||||
{% include frame.html url="example-setzoom.html" %}
|
||||
|
||||
Notice how the images shown at zoom levels 0 and one correspond with the images
|
||||
shown in the previous section!
|
||||
|
||||
Other ways of setting the zoom are:
|
||||
|
||||
* [`setView(center, zoom)`](../../reference-1.0.3.html#map-setview), which also sets the map center
|
||||
* [`flyTo(center, zoom)`](../../reference-1.0.3.html#map-flyto), like `setView` but with a smooth animation
|
||||
* [`zoomIn()` / `zoomIn(delta)`](../../reference-1.0.3.html#map-zoomin), zooms in `delta` zoom levels, `1` by default
|
||||
* [`zoomOut()` / `zoomOut(delta)`](../../reference-1.0.3.html#map-zoomout), zooms out `delta` zoom levels, `1` by default
|
||||
* [`setZoomAround(fixedPoint, zoom)`](../../reference-1.0.3.html#map-setzoomaround), sets the zoom level while keeping a point fixed (what scrollwheel zooming does)
|
||||
* [`fitBounds(bounds)`](../../reference-1.0.3.html#map-fitbounds), automatically calculates the zoom to fit a rectangular area on the map
|
||||
|
||||
|
||||
## Fractional zoom
|
||||
|
||||
A feature introduced in Leaflet 1.0.0 was the concept of <em>fractional zoom</em>.
|
||||
Before this, the zoom level of the map could be only an integer number (`0`, `1`, `2`, and so on);
|
||||
but now you can use fractional numbers like `1.5` or `1.25`.
|
||||
|
||||
Fractional zoom is disabled by default. To enable it, use the
|
||||
[map's `zoomSnap` option](http://leafletjs.com/reference-1.0.3.html#map-zoomsnap).
|
||||
The `zoomSnap` option has a default value of `1` (which means that the zoom level
|
||||
of the map can be `0`, `1`, `2`, and so on).
|
||||
|
||||
If you set the value of `zoomSnap` to `0.5`, the valid zoom levels of the map
|
||||
will be `0`, `0.5`, `1`, `1.5`, `2`, and so on.
|
||||
|
||||
If you set a value of `0.1`, the valid zoom levels of the map will be `0`, `0.1`,
|
||||
`0.2`, `0.3`, `0.4`, and so on.
|
||||
|
||||
The following example uses a `zoomSnap` value of `0.25`:
|
||||
|
||||
```
|
||||
var map = L.map('map', {
|
||||
zoomSnap: 0.25
|
||||
});
|
||||
```
|
||||
|
||||
{% include frame.html url="example-fractional.html" %}
|
||||
|
||||
As you can see, Leaflet will only load the tiles for zoom levels `0` or `1`, and will scale them
|
||||
as needed.
|
||||
|
||||
Leaflet will <em>snap</em> the zoom level to the closest valid one. For example,
|
||||
if you have `zoomSnap: 0.25` and you try to do `map.setZoom(0.8)`, the zoom will
|
||||
snap back to `0.75`. The same happens with `map.fitBounds(bounds)`, or when ending
|
||||
a pinch-zoom gesture on a touchscreen.
|
||||
|
||||
`zoomSnap` can be set to zero. This means that Leaflet will <strong>not</strong>
|
||||
snap the zoom level.
|
||||
|
||||
There is another important map option related to `zoomSnap`: [the `zoomDelta` option](http://leafletjs.com/reference-1.0.3.html#map-zoomdelta).
|
||||
This controls how many zoom levels to zoom in/out when using the zoom buttons
|
||||
(from the default [`L.Control.Zoom`](http://leafletjs.com/reference-1.0.3.html#control-zoom))
|
||||
or the `+`/`-` keys in your keyboard.
|
||||
|
||||
For the mousewheel zoom, the [`wheelPxPerZoomLevel`](http://leafletjs.com/reference-1.0.3.html#map-wheelpxperzoomlevel)
|
||||
option controls how fast the mousewheel zooms in our out.
|
||||
|
||||
Here is an example with `zoomSnap` set to zero:
|
||||
|
||||
```
|
||||
var map = L.map('map', {
|
||||
zoomDelta: 0.25,
|
||||
zoomSnap: 0
|
||||
});
|
||||
```
|
||||
|
||||
Try the following, and see how the zoom level changes:
|
||||
|
||||
* Pinch-zoom if you have a touchscreen
|
||||
* Zoom in/out with your mousewheel
|
||||
* Do a box zoom (drag with your mouse while pressing the `shift` key in your keyboard)
|
||||
* Use the zoom in/out buttons
|
||||
|
||||
{% include frame.html url="example-delta.html" %}
|
||||
|
||||
|
||||
That concludes this tutorial. Now play with your zoom levels in your maps!
|
||||
|
After Width: | Height: | Size: 100 KiB |