7.6. 演练:在 Leaflet 中添加交互式GeoJSON层

本演练构建在本课程前面的部分基础上,演示如何使用Leaflet将交互式GeoJSON层添加到web地图中。您将构建一个包含费城基础地图瓦片和两个GeoJSON层的地图,上面表示城市花园和食品储藏室(即食品库)。用户可以单击其中一个花园或食品储藏室,查看地图下方功能的名称(作为使用弹出窗口的替代方法)。单击的特征在选中时会更改颜色。希望您能想出很多方法将这些功能应用到您为学期项目构建的web地图。

../../_images/walkthrough_thumbnail.png

图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

  1. 找到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>的部分来指向您自己的磁贴,否则您将看不到地图。

  1. 现在我们将设置一些变量,以便在这个init()函数中使用。更换 . . .在上面的代码中包含以下内容:

var gardenLayer;
var pantryLayer;

var selection;
var selectedLayer;

. . .

这些还没有发生什么,但重要的是,您要了解它们在代码中的未来用途:

-gardenLayer和pantryLayer最终将成为引用GeoJSON的 Leaflet 层。

-选择将引用当前选定的功能。如果未选择任何功能,则此变量的值将设置为空。

-selected layer将引用最近选择的层。

  1. 现在我们将建立我们最终将在花园层使用的样式。更换 . . .在上面的代码中包含以下内容:

// 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
  };
}

. . .

这与本课上一节中的代码几乎相同,因此我不会详细解释。它只是为花园层定义了两个不同的符号(默认符号和选定符号)。

  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(),以便如果有人单击某个功能,则只运行功能单击事件处理程序代码,而不运行地图单击事件处理程序代码。

  1. 现在我们已经得到了所有我们需要添加到地图的花园层。更换 . . .在上面的代码中包含以下内容:

// 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事件处理函数。

  1. 现在我们将重复整个过程与食品间层。更换 . . .在上面的代码中包含以下内容:

// 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 标记的放置位置和方式。

  1. 现在所有图层都添加完毕,让我们将焦点切换到地图上。我们必须处理有人点击地图但没有点击功能的情况。在这种情况下,一切都应该取消选择。更换. . .在上面的代码中包含以下内容:

// 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方法。请注意,在上面的代码中,地图下面的标签也会重置为消息“单击地图上的花园或食品储藏室以获取更多信息”

  1. 现在我们将添加上面的层事件处理程序代码中使用的一些函数。第一个选项查看先前选定的图层,并将选定要素的符号设置回默认值。更换 . . . 在上面的代码中包含以下内容:

// 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 中具有不同的样式语法。

  1. 最后,我们需要这个函数来为地图下面的摘要标签构建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”),则我们需要在上面添加更多代码来处理这些情况。

  1. 运行您的页面,然后单击一些花园和餐具室功能。单击某个要素时,该要素应变为蓝色,要素名称应显示在地图下方。当您单击远离某个特征(或单击其他特征)时,该特征应返回其原始颜色,并且应删除该特征名称(如果单击其他特征,则应更新该名称)。

../../_images/lesson7_walkthrough_output.png

图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>