7.6. 演练:在 Leaflet 中添加交互式GeoJSON层¶
本演练构建在本课程前面的部分基础上,演示如何使用Leaflet将交互式GeoJSON层添加到web地图中。您将构建一个包含费城基础地图瓦片和两个GeoJSON层的地图,上面表示城市花园和食品储藏室(即食品库)。用户可以单击其中一个花园或食品储藏室,查看地图下方功能的名称(作为使用弹出窗口的替代方法)。单击的特征在选中时会更改颜色。希望您能想出很多方法将这些功能应用到您为学期项目构建的web地图。
图7.3
请注意,这是从OpenStreetMap中挑选出来的一个样本数据集,并不是费城这些功能的综合列表。如果你知道任何其他花园或食品储藏室,请将它们添加到OpenStreetMap中(第9章对此有更多介绍)!
7.6.1. 下载数据¶
在继续之前,请下载该项目的`数据并解压缩<https://www.e-education.psu.edu/geog585/sites/www.e-education.psu.edu.geog585/files/lesson7/lesson7_data_leaflet。 zip>`_。将其内容复制到您的Jetty主文件夹中,该文件夹应具有以下路径:
c:\Program Files\GeoServer 2.x.x\webapps\geog585\
这是保存第6课演练的文件夹,也是本地样式表style.css(本练习所需)所在的文件夹。
这个文件夹包含两个包含GeoJSON数据的JavaScript文件。gardens.js使用polygon GeoJSON保存gardensData变量,pantriesData.js使用point GeoJSON保存pantriesData变量。
还有两个SVG(可缩放矢量图形)文件将用于表示食品储藏室。黄色符号表示未选定的特征,蓝色符号表示选定的特征。
有几种方法可以为您自己的应用程序获取此类数据。
QGIS可以将任何矢量层保存为GeoJSON格式。
GDAL库中的ogr2ogr可以将shapefile和其他类型转换为GeoJSON
在这两种情况下,您都需要将数据保存为JS文件并将GeoJSON定义为变量(我们在这里采用的方法),或者使用扩展名(如LeafletAJAX)直接从文件中读取数据(超出本课程的范围)。
要获取图标,QGIS中可用的所有图标都可以在您的计算机上的文件夹中找到,该文件夹名为:
C:\Program Files\QGIS <name of your version>\apps\qgis\svg
我使用了开源程序Inkscape来改变图标的颜色。
7.6.2. 设置HTML文件¶
在深入研究JavaScript代码之前,请创建一个空文本文件并插入以下代码。然后将其另存为lesson7.html,与您刚刚下载并复制到主文件夹中的所有其他文件相邻。
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Food resources: Community gardens and food pantries</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.css" type="text/css" crossorigin="">
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.js" crossorigin=""></script>
<script src="gardens.js"></script>
<script src="pantries.js"></script>
<link rel="stylesheet" href="style.css" type="text/css">
<script type="text/javascript">
. . .
</script>
</head>
<body onload="init()">
<h1 id="title">Food resources: Community gardens and food pantries</h1>
<div id="mapid"></div>
<div id="summaryLabel">
<p>Click a garden or food pantry on the map to get more information.</p>
</div>
</body>
</html>
如果打开它,您应该会看到一个空白的地图框架,周围环绕着一个HTML标题和描述性文本。在脚本和链接标签中,注意我们正在加载 Leaflet 代码和样式表,以及gardens和pantries.js文件。
现在让我们添加创建地图和图层的LeafletJavaScript代码。
7.6.3. 编写JavaScript¶
找到JavaScript代码的脚本标记并替换 . . . 使用以下行,以便:
var map; function init() { // create map and set center and zoom level map = new L.map('mapid'); map.setView([39.960,-75.210],14); // create and add the tile layer var tiles = L.tileLayer('http://personal.psu.edu/<Your PSU ID>/tiles/PhillyBasemap/{z}/{x}/{y}.png', { attribution: 'Data copyright OpenStreetMap contributors'}); tiles.addTo(map); . . . }上面的代码为地图创建一个全局变量,然后定义当页面加载时将运行的初始化函数。这个init()函数将包含大部分JavaScript代码。
地图视图设置为放大到西费城,这样我们可以在社区一级详细查看食物资源。
费城基础地图瓦片也添加在这里。您必须修改上面代码中的URL,通过修改标记为<your PSU ID>的部分来指向您自己的磁贴,否则您将看不到地图。
现在我们将设置一些变量,以便在这个init()函数中使用。更换 . . .在上面的代码中包含以下内容:
var gardenLayer; var pantryLayer; var selection; var selectedLayer; . . .这些还没有发生什么,但重要的是,您要了解它们在代码中的未来用途:
-gardenLayer和pantryLayer最终将成为引用GeoJSON的 Leaflet 层。
-选择将引用当前选定的功能。如果未选择任何功能,则此变量的值将设置为空。
-selected layer将引用最近选择的层。
现在我们将建立我们最终将在花园层使用的样式。更换 . . .在上面的代码中包含以下内容:
// define the styles for the garden layer (unselected and selected) function gardenStyle(feature) { return { fillColor: "#FF00FF", fillOpacity: 1, color: '#B04173', }; } function gardenSelectedStyle(feature) { return { fillColor: "#00FFFB", color: '#0000FF', fillOpacity: 1 }; } . . .这与本课上一节中的代码几乎相同,因此我不会详细解释。它只是为花园层定义了两个不同的符号(默认符号和选定符号)。
现在,让我们添加一个函数,它将告诉每个花园功能在单击时如何操作。更换. . . 在上面的代码中包含以下内容:
// handle click events on garden features function gardenOnEachFeature(feature, layer){ layer.on({ click: function(e) { if (selection) { resetStyles(); } e.target.setStyle(gardenSelectedStyle()); selection = e.target; selectedLayer = gardenLayer; // Insert some HTML with the feature name buildSummaryLabel(feature); L.DomEvent.stopPropagation(e); // stop click event from being propagated further } }); } . . .这里发生了很多事,所以我们一次拿一件。 Leaflet 最有用的一点是它能够为每个特性定义一个函数,说明该特性应如何响应某些事件。在上面的函数中,我们告诉每个花园功能都要监听单击事件。如果单击了一个花园,函数将运行一个事件参数(由变量e表示),这将帮助我们处理单击的功能。此代码称为事件处理程序代码,因为它处理发生单击事件的情况。
在上面的情况下,如果点击了一个特性,我们将采取任何可能突出显示的特性,并将其样式设置回默认值。这是在resetStyles()函数中完成的,我们稍后将添加该函数的代码。
然后我们获取单击的特征(e.target),并使用为选定特征设置的特殊样式对其进行样式设置。我们还更新selection和selectedLayer变量,以引用新单击的功能。
最后,我们构建一些HTML,将其放在地图下面的描述性层中。此代码将在名为buildSummaryLabel()的函数中找到,稍后我们将添加该函数。
请注意,需要调用L.DomEvent.stopprogation(),以便如果有人单击某个功能,则只运行功能单击事件处理程序代码,而不运行地图单击事件处理程序代码。
现在我们已经得到了所有我们需要添加到地图的花园层。更换 . . .在上面的代码中包含以下内容:
// add the gardens GeoJSON layer using the gardensData variable from gardens.js var gardenLayer = new L.geoJSON(gardensData,{ style: gardenStyle, onEachFeature: gardenOnEachFeature }); gardenLayer.addTo(map); . . .注意上面的代码如何引用gardens.js中包含的gardensData变量。如果页面顶部没有引用gardens.js的脚本标记,那么将无法识别变量gardensData。通过将gardenStyle用于style属性,我们可以确保图层将根据之前的定义进行样式设置。类似地,通过使用上面为onEachFeature属性定义的gardenOnEachFeature函数,我们实现了层中的每个特性都将附加在gardenOnEachFeature中定义的click事件处理函数。
现在我们将重复整个过程与食品间层。更换 . . .在上面的代码中包含以下内容:
// create icons for pantries (selected and unselected) var pantriesIcon = L.icon({ iconUrl: 'pantries.svg', iconSize: [20,20] }); var selectedPantriesIcon = L.icon({ iconUrl: 'pantries_selected.svg', iconSize: [20,20] }); // handle click events on pantry features function pantriesOnEachFeature(feature, layer){ layer.on({ click: function(e) { if (selection) { resetStyles(); } e.target.setIcon(selectedPantriesIcon); selection = e.target; selectedLayer = pantryLayer; // Insert some HTML with the feature name buildSummaryLabel(feature); L.DomEvent.stopPropagation(e); // stop click event from being propagated further } }); } // add the gardens GeoJSON layer using the pantriesData variable from pantries.js pantryLayer = new L.geoJSON(pantriesData,{ pointToLayer: function (feature, latlng) { return L.marker(latlng, {icon: pantriesIcon}); }, onEachFeature: pantriesOnEachFeature } ); pantryLayer.addTo(map); . . .此代码与gardens代码之间的唯一根本区别是使用SVG图标为点定义样式的方式。请注意,创建 Leaflet 层时,必须定义pointToLayer函数,说明每个 Leaflet 标记的放置位置和方式。
现在所有图层都添加完毕,让我们将焦点切换到地图上。我们必须处理有人点击地图但没有点击功能的情况。在这种情况下,一切都应该取消选择。更换. . .在上面的代码中包含以下内容:
// handle clicks on the map that didn't hit a feature map.addEventListener('click', function(e) { if (selection) { resetStyles(); selection = null; document.getElementById('summaryLabel').innerHTML = '<p>Click a garden or food pantry on the map to get more information.</p>'; } }); . . .有几种方法可以在 Leaflet 中添加事件处理程序。上面的代码使用addEventListener方法。请注意,在上面的代码中,地图下面的标签也会重置为消息“单击地图上的花园或食品储藏室以获取更多信息”
现在我们将添加上面的层事件处理程序代码中使用的一些函数。第一个选项查看先前选定的图层,并将选定要素的符号设置回默认值。更换 . . . 在上面的代码中包含以下内容:
// function to set the old selected feature back to its original symbol. Used when the map or a feature is clicked. function resetStyles(){ if (selectedLayer === pantryLayer) selection.setIcon(pantriesIcon); else if (selectedLayer === gardenLayer) selectedLayer.resetStyle(selection); } . . .上面的配餐室和花园层需要单独的代码行,因为用图标和多边形表示的点在 Leaflet 中具有不同的样式语法。
最后,我们需要这个函数来为地图下面的摘要标签构建HTML字符串。更换 . . . 在上面的代码中包含以下内容:
// function to build the HTML for the summary label using the selected feature's "name" property function buildSummaryLabel(currentFeature){ var featureName = currentFeature.properties.name || "Unnamed feature"; document.getElementById('summaryLabel').innerHTML = '<p style="font-size:18px"><b>' + featureName + '</b></p>'; }上面的函数将引入当前选择的功能并读取其“名称”属性。然后,它获取ID为“ summaryLabel”的HTML元素,并将其innerHTML设置为精心构造的HTML字符串,在其中插入名称(由变量featureName表示)。请注意,如果我们的花园和餐具室层具有不同的属性字段名称(例如“ PANTRYNAME”和“ GARDENNAME”),则我们需要在上面添加更多代码来处理这些情况。
运行您的页面,然后单击一些花园和餐具室功能。单击某个要素时,该要素应变为蓝色,要素名称应显示在地图下方。当您单击远离某个特征(或单击其他特征)时,该特征应返回其原始颜色,并且应删除该特征名称(如果单击其他特征,则应更新该名称)。
图7.4
7.6.4. 演练的最终代码¶
如果您的页面不起作用,请仔细将您的代码与下面的完整代码进行比较,以确保您在正确的位置插入了所有内容。也:
在运行页面时验证您是否已连接到Internet,以便您可以从CDN检索 Leaflet 代码。
在引用瓦片的基本地图时,请确保已将URL插入到自己的传递空间。
确保GeoServer已启动(因为您正在通过其Jetty web servlet运行页面),并且您正在通过类似于以下内容的URL引用页面:http://localhost:8080/geog585/lesson7.html。
一个OpenLayers 3 version of the walkthrough code 为好奇的人提供。请注意,必须调整GeoJSON文件才能使此版本的演练正常工作。它必须是纯GeoJSON,并且不包含任何声明的变量或JavaScript代码。
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Food resources: Community gardens and food pantries</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.css" type="text/css" crossorigin="">
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.js" crossorigin=""></script>
<script src="gardens.js"></script>
<script src="pantries.js"></script>
<link rel="stylesheet" href="style.css" type="text/css">
<script type="text/javascript">
var map;
function init() {
// create map and set center and zoom level
map = new L.map('mapid');
map.setView([39.960,-75.210],14);
// create and add the tile layer
var tiles = L.tileLayer('http://personal.psu.edu/juw30/tiles/PhillyBasemap/{z}/{x}/{y}.png', { attribution: 'Data copyright OpenStreetMap contributors'});
tiles.addTo(map);
var gardenLayer;
var pantryLayer;
var selection;
var selectedLayer;
// define the styles for the garden layer (unselected and selected)
function gardenStyle(feature) {
return {
fillColor: "#FF00FF",
fillOpacity: 1,
color: '#B04173',
};
}
function gardenSelectedStyle(feature) {
return {
fillColor: "#00FFFB",
color: '#0000FF',
fillOpacity: 1
};
}
// handle click events on garden features
function gardenOnEachFeature(feature, layer){
layer.on({
click: function(e) {
if (selection) {
resetStyles();
}
e.target.setStyle(gardenSelectedStyle());
selection = e.target;
selectedLayer = gardenLayer;
// Insert some HTML with the feature name
buildSummaryLabel(feature);
L.DomEvent.stopPropagation(e); // stop click event from being propagated further
}
});
}
// add the gardens GeoJSON layer using the gardensData variable from gardens.js
var gardenLayer = new L.geoJSON(gardensData,{
style: gardenStyle,
onEachFeature: gardenOnEachFeature
});
gardenLayer.addTo(map);
// create icons for pantries (selected and unselected)
var pantriesIcon = L.icon({
iconUrl: 'pantries.svg',
iconSize: [20,20]
});
var selectedPantriesIcon = L.icon({
iconUrl: 'pantries_selected.svg',
iconSize: [20,20]
});
// handle click events on pantry features
function pantriesOnEachFeature(feature, layer){
layer.on({
click: function(e) {
if (selection) {
resetStyles();
}
e.target.setIcon(selectedPantriesIcon);
selection = e.target;
selectedLayer = pantryLayer;
// Insert some HTML with the feature name
buildSummaryLabel(feature);
L.DomEvent.stopPropagation(e); // stop click event from being propagated further
}
});
}
// add the gardens GeoJSON layer using the pantriesData variable from pantries.js
pantryLayer = new L.geoJSON(pantriesData,{
pointToLayer: function (feature, latlng) {
return L.marker(latlng, {icon: pantriesIcon});
},
onEachFeature: pantriesOnEachFeature
}
);
pantryLayer.addTo(map);
// handle clicks on the map that didn't hit a feature
map.addEventListener('click', function(e) {
if (selection) {
resetStyles();
selection = null;
document.getElementById('summaryLabel').innerHTML = '<p>Click a garden or food pantry on the map to get more information.</p>';
}
});
// function to set the old selected feature back to its original symbol. Used when the map or a feature is clicked.
function resetStyles(){
if (selectedLayer === pantryLayer) selection.setIcon(pantriesIcon);
else if (selectedLayer === gardenLayer) selectedLayer.resetStyle(selection);
}
// function to build the HTML for the summary label using the selected feature's "name" property
function buildSummaryLabel(currentFeature){
var featureName = currentFeature.properties.name || "Unnamed feature";
document.getElementById('summaryLabel').innerHTML = '<p style="font-size:18px"><b>' + featureName + '</b></p>';
}
}
</script>
</head>
<body onload="init()">
<h1 id="title">Food resources: Community gardens and food pantries</h1>
<div id="mapid"></div>
<div id="summaryLabel">
<p>Click a garden or food pantry on the map to get more information.</p>
</div>
</body>
</html>