6.5. 演练: 使用 Leaflet 在瓦片地图上叠加的 WMS

本演练的目标是获得在 Leaflet 中覆盖不同类型web服务的一些实践。你将首先发布一个显示费城农贸市场的WMS。然后,您将使用 Leaflet 将此层放置在上一课中使用TileMill制作的费城基础地图瓦片的顶部。您还将添加代码,以便应用程序的用户可以单击任何农贸市场并在弹出窗口中查看更多信息。

../../_images/leaflet_popup.png

图6.4

6.5.1. 建立农贸市场

第一步是建立一个(相当)好看的WMS,展示费城的农贸市场。在这个应用中,农贸市场WMS将扮演业务层的角色。

设置这个WMS将是对您在第4课中学到的一些技能的一个很好的回顾。在缺少分步说明的地方,您应该能够返回到第4课演练以记住这些过程。

  1. 下载费城农民市场的这个 shapefile 。这是通过PASDA库从费城获得的。如果将其解压缩到 C:\data\Philadelphia 文件夹中,则可能是最简单的。

  2. 打开GeoServer web管理页面,使用坐标系EPSG:3857在GeoServer中将农贸市场形状文件作为一个图层发布。把它放进你的geog585工作区。预览图层时应该如下所示。

../../_images/geoserver_2_8_3_fig_6_5.png

图6.5

  1. 使用SLD Cookbook示例Point With Styled Label ,创建一个名为point_pointwithstyledlabel的SLD。如果你不记得怎么做,请参阅第4课演练。在本课中,您使用多边形示例执行了完全相同的操作。

请记住,当读取包含标签名称的字段时,SLD是区分大小写的。在此FarmersMarkets shapefile中,要用于标签文本的字段在所有大写字母中称为“名称”,因此,必须更改SLD代码,使名称在所有大写字母中:

<Label>
    <ogc:PropertyName>NAME</ogc:PropertyName>
</Label>
  1. 将point_pointwithstyledlabel SLD应用于您的农贸市场WMS,以便它是唯一可用的SLD,或者至少是默认的SLD。如果你记不起怎么做的话,请再看第4课演练。

成功应用SLD后,在GeoServer中预览图层时,WMS应如下所示:

../../_images/geoserver_2_8_3_fig_6_6.png

图6.6

现在,您已成功准备好WMS,将在本演练的稍后部分覆盖在瓦片地图上。

6.5.2. 准备web开发环境

现在您将为编写一个简单的 Leaflet 应用程序做一些准备。我们将在GeoServer安装的名为Jetty的小型web服务器上托管此应用程序。请记住,在“现实世界”中,您可能会让您的IT人员安装企业级web服务器,例如可以在其中运行GeoServer和HTML页面的Apache。如果你没有一个IT人员,你甚至有幸有一天自己做到这一点!:-)

  1. 停止GeoServer。(所有程序>GeoServer 2.x.x>停止GeoServer)

  2. 创建文件夹c:程序文件地理服务器2.x.xwebappsgeog585

    当您开发web页面时,您将把所有HTML页面和样式表放在这个文件夹中。

  3. 启动GeoServer(所有程序>GeoServer 2.x.x>启动GeoServer)

    只要启动GeoServer,您现在就应该能够通过URL来访问HTML页面,例如http:// localhost:8080 / geog585 / mypage.html(其中mypage.html需要替换为实际名称) html文件)。

    您的HTML页面通常会使用样式表来定义页面的绘制方式。

  4. 创建新的文本文件并粘贴到以下代码中:

#mapid {
    width: 512px;
    height: 512px;
    border: 1px solid #ccc;
}

.leaflet-container {
    background: #fff;
}

上面的CSS设置地图容器的大小、地图边框的大小和颜色以及背景的颜色。

  1. 将该文件保存在新的Jetty文件夹中,格式为c:\Program Files\GeoServer 2.x.x\webappsgeog585\style.css。如果出现拒绝访问错误,则必须修改geog585文件夹的访问权限,以便用户对该文件夹具有读写访问权限。或者,您可以尝试在管理员模式下运行文本编辑器。

  2. 通过打开http://localhost:8080/geog585/style.CSS浏览器来测试您的CSS是否可访问。您应该看到相同的CSS文件。

    如果您收到一个HTTP 404“Page not found”错误,并且您确定您的URL是正确的(或者尝试不使用.css扩展名HTTP://localhost:8080/geog585/style),那么Jetty识别您的新geog585文件夹时可能出了问题。我可以通过完全重启机器并再次启动GeoServer来解决这种情况。之后,文件夹被识别。

    请注意,当您还将HTML页面放在这个文件夹中时,您可以在HTML代码中将这个文件简单地称为style.css,而不是提供整个URL。

  3. 保存并关闭style.css。

6.5.3. 创建HTML页面并编写代码

现在您将创建一个HTML页面并插入配置地图和弹出窗口的JavaScript代码。下面的步骤并不是线性地提供代码;您需要按照说明中给出的正确位置插入代码。代码是分层的,从某种意义上说,有些块在另一些块中运行。描述代码块比按其确切顺序给出代码更直观。如果您对代码的去向感到困惑,请参阅本演练页末尾的完整示例代码。

  1. 创建一个空文本文件,并将其保存为Jetty web文件夹中的markets.html(换句话说,c:\程序文件\GeoServer 2.x.x\ webappsgeog585\)。

    提示:如果您稍后在课程中尝试编辑此文件,而文本编辑器不允许您保存它,请停止GeoServer。保存编辑并准备好预览后,可以启动GeoServer。

  2. 将以下代码放在文本文件中(与Python不同,缩进在HTML中无关紧要,因此不必担心缩进没有完全通过):

<!DOCTYPE html>
    <html>
      <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>Farmers markets in Philadelphia</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="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
        <link rel="stylesheet" href="style.css" type="text/css">

        <script type="text/javascript">


        </script>
      </head>
      <body onload="init()">
        <h1 id="title">Farmers markets in Philadelphia</h1>

        <div id="mapid">
        </div>

        <div id="docs">
            <p>This page shows farmers markets in Philadelphia, Pennsylvania. Click a market to get more information.</p>
        </div>

      </body>
    </html>

上面的代码包含HTML头和体。这会给你一个页面的外壳,虽然你还看不到地图。

在头部,注意对 Leaflet JavaScript和CSS文件的引用,它们都运行在CloudFlare CDN上。还有一个对前面放置在web文件夹中的style.css的引用,在某些情况下,它会覆盖 Leaflet 样式表。最后,还提到了运行在Google CDN上的jQuery JavaScript库。jQuery是一个助手库,它极大地简化了一些JavaScript任务。我们将在这里使用它来发出一个web请求来查询WMS,因为没有简单的方法可以在 Leaflet 中执行WMS GetFeatureInfo请求。

在正文中,注意有一个id为“mapid”的div元素,用于保存我们的 Leaflet web地图。此元素的CSS样式在style.CSS中定义,它以像素为单位设置地图的宽度和高度。如果你想改变地图的宽度和高度,你可以修改CSS。

页面主体加载后,它将从稍后添加的JavaScript代码中运行init()函数。这就是为什么你会看到<body onload=“init()”>。

从现在起,我们不会对头部或身体做任何改变。我们将在<script>标记中插入JavaScript逻辑。

  1. 在<script type=“text/javascript”></script>标记中,插入以下代码:

var map;
function init() {
   // create map and set center and zoom level
   map = new L.map('mapid');
   map.setView([39.9526,-75.1652],13);
   . . .
}

这将设置一个名为map的全局变量,可在整个JavaScript代码中使用。然后定义了一个名为init()的初始化函数,您会记得它是您设置为在页面主体加载时运行的函数。该函数首先在mapid div中实例化一个Leaflet地图对象,然后在缩放级别13处将地图居中39.9526 N 75.1652 W。

在本演练中,您将在init()函数中看到的位置插入剩余的JavaScript . . .上面。

  1. 现在您将添加一些代码来引入您的费城基础地图块。在代码的init()函数中,直接在调用map.setView()的行之后插入以下内容。

//创建瓦片层并将其添加到map var tiles=L.tillelayer('http://personal.psu.edu/<Your psu ID>/tiles/PhillyBasemap/{z}/{x}/{y}.png');tiles.add to(map);

上面的代码创建了一个 Leaflet 瓦片层,该层引用您在第5课中使用TileMill制作的PhillyBasemap。在上面的代码中,必须修改URL以包含PSU访问帐户ID,以便URL正确地指向您的传递空间。如果瓦片的URL略有不同,则代码可能需要进行其他修改。如果有疑问,请检查通行证空间文件夹结构。

瓦片的URL以通用格式提供,其中z表示缩放级别编号,x表示列编号,y表示行编号。只要为Leaflet提供此图块URL结构,当您平移和缩放地图时,它就会足够聪明以请求正确的图块。

注意tillelayer.add to()方法是如何用于将图层添加到地图的。地图对象作为参数传入。在其他一些地图API(如OpenLayers)中,可以调用 map 本身的add方法,并将该层作为参数提供。

  1. 在上一步输入的代码之后立即添加以下代码:

// create wms layer
 var farmerMarkets = L.tileLayer.wms('http://localhost:8080/geoserver/geog585/wms', {
  layers: 'philadelphia:FarmersMarkets',
  format: 'image/png',
  transparent: true
});

farmerMarkets.addTo(map);

. . .

这会将农贸市场的WMS图层添加到地图中。请注意,Leaflet的wms类是如何用于此目的的。您为其提供了一些属性,例如所需的URL,图层和图像格式(全部使用友好的WMS语法),Leaflet负责格式化和发送GetMap请求并在用户围绕地图缩放时显示响应 。

到目前为止,创建地图和添加图层非常简单。它点击WMS并看到一个更复杂的弹出窗口。为此,必须向服务器发送WMS GetFeatureInfo请求。 Leaflet 对此没有任何选择,因此您必须自己构建一个URL,将其发送到服务器,然后读取响应。那么,如何使用JavaScript发出这样的web请求呢?

一种方法是使用jQuery,这是一个开源API,旨在简化JavaScript程序员日复一日要做的事情。其中一个任务是在最终用户与网页交互(即单击地图)时向服务器发送web请求。请求是使用AJAX(异步JavaScript和XML)技术发送的,该技术避免了整个页面刷新。

考虑到这一点,让我们编写一个识别函数,只要有人点击地图,我们就可以触发它。此函数将构造一个WMS GetFeatureInfo请求,使用AJAX将请求发送到服务器,并将响应放入一个弹出窗口中。

  1. 将以下内容直接添加到代码的上面添加的行之后 . . .:

// define event handler function for click events and register it
function Identify(e)
{
  . . .
}
map.addEventListener('click', Identify);

上面是每次触发鼠标事件时都会运行的“识别”功能。我还没有提供全部功能,因为它很长; 但是,插入了…,因此您可以看到,连同此函数的定义一起,在地图上添加了“事件监听器”,以保持对发生的任何鼠标单击的警报。如果有人单击地图,则将触发“识别”功能,并且一个'Leaflet事件对象<https://leafletjs.com/reference-1.0.3.html#event-objects>`_名为'e',其中包含 单击的点将被带入功能。

  1. 将上面…中的代码替换为以下内容:

// set parameters needed for GetFeatureInfo WMS request
var sw = map.options.crs.project(map.getBounds().getSouthWest());
var ne = map.options.crs.project(map.getBounds().getNorthEast());
var BBOX = sw.x + "," + sw.y + "," + ne.x + "," + ne.y;
var WIDTH = map.getSize().x;
var HEIGHT = map.getSize().y;
. . .

为了构造GetFeatureInfo请求,我们需要一些关键的东西:地图的边界坐标、以像素为单位的地图宽度和以像素为单位的地图高度。Leaflet地图对象提供了获取这些属性的方法。上面的代码就是这样的。将检索地图的西南角和东北角坐标,并按照BBOX参数所需的语法将其格式化为逗号分隔的字符串。然后从 Leaflet 地图中提取宽度和高度。

  1. 继续填写标识功能,将上面代码中的…替换为以下内容:

var X = Math.trunc(map.layerPointToContainerPoint(e.layerPoint).x);
var Y = Math.trunc(map.layerPointToContainerPoint(e.layerPoint).y);
// compose the URL for the request
var URL = 'http://localhost:8080/geoserver/geog585/wms?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetFeatureInfo&LAYERS=philadelphia:FarmersMarkets&QUERY_LAYERS=philadelphia:FarmersMarkets&BBOX='+BBOX+'&FEATURE_COUNT=1&HEIGHT='+HEIGHT+'&WIDTH='+WIDTH+'&INFO_FORMAT=application%2Fjson&TILED=false&CRS=EPSG%3A3857&I='+X+'&J='+Y;
. . .

这段代码的目的是找出点击点并构造请求URL。尽管上面的行看起来有点复杂,但它们实际上是从鼠标单击生成的事件对象(名为“e”)中获取单击像素的行和列。然后为GetFeatureInfo请求构造一个URL,插入我们在上面为BBOX、WIDTH、HEIGHT、I和J参数导出的值。

请注意,如果您将WMS命名为FarmersMarkets以外的其他名称,将其放置在费城以外的工作区中,或将其放置在geog585以外的文件夹中,则需要在上述代码中修改URL。

现在让我们添加一些代码来发送此请求。

  1. 用下面的代码替换上面代码中的…继续填写Identify函数。

//send GetFeatureInfo as asynchronous HTTP request using jQuery $.ajax
$.ajax({
    url: URL,
    dataType: "json",
    type: "GET",
    success: function(data)
    {
       if(data.features.length !== 0) {  // at least one feature returned in response
         var returnedFeature = data.features[0]; // first feature from response

         // Set up popup for clicked feature and open it
         var popup = new L.Popup({
            maxWidth: 300
         });

         popup.setContent("<b>" + returnedFeature.properties.NAME + "</b><br />" + returnedFeature.properties.ADDRESS);
         popup.setLatLng(e.latlng);
         map.openPopup(popup);
    }
  }
});

上面的函数使用jQuery使用AJAX发出GetFeatureInfo请求。通常当你看到$时。在一段JavaScript代码中,这意味着正在调用jQuery函数。请记住,我们在页面顶部引入了对jQuery库的引用,允许我们使用这些函数。

要发出web请求,AJAX函数需要传入许多选项,包括URL、数据类型、请求类型等。另一个重要的事情是如何处理响应;因此,如果请求成功,我们定义一个小函数来处理响应数据。首先,这个函数检查一个特性是否返回,因为有人可以非常可行地单击地图的空白区域而不返回任何特性。任何返回的特性都在数组中提供,该数组中的第一个特性在上面使用变量returned feature引用。为了简单起见,我们不处理返回多个功能的情况。

业务的下一个顺序是检查returnedFeature并使用其属性构造一个弹出窗口。使用L.popup 上课。然后用一小块HTML填充它,HTML是根据returnedFeature的一些属性构建的,即所选农贸市场的名称和地址字段。

弹出窗口需要“锚定”到地图的某个地方,因此再次引用鼠标单击事件对象“e”来构造锚定点。最后一行代码打开弹出窗口。

  1. 打开http://localhost:8080/geog585/markets.html测试地图。它应该看起来像下面的图片。如果没有,继续阅读一些故障排除提示。屏幕截图:费城农贸市场-第6课演练输出

../../_images/lesson6_walkthrough_output.png

图6.7完成演练后的最终网页

6.5.4. 故障排除

如果在演练结束时未获得预期结果,请验证以下内容:

  1. 确保已连接到Internet。在本课程中,我们总是参考内容交付网络(CDN)网站的传单,而不是在自己的服务器上托管。在我们的案例中,所有的 Leaflet 逻辑都是从互联网上提取出来的。

  2. 检查GeoServer是否已启动。请注意,在对代码进行调整时可能需要停止GeoServer,然后在进行编辑并希望预览工作时启动它。

  3. 确保通过http://localhost:8080/geog585/markets.html URL而不是本地文件在浏览器中打开页面,例如双击Windows文件资源管理器中的.html文件。

  4. 如上所述,检查是否已将Penn State Access帐户ID插入图块URL中。

  5. 检查是否已按上述说明在WMS URL中插入正确的工作区和web服务名称。

  6. 确保您的代码与下面的最终代码完全匹配,而不是前面两步中个性化的部分。

6.5.5. 演练的最终代码

下面是本演练从头到尾使用的代码。这将帮助您获得每个块应放置在何处的上下文。如果您对代码在不同API中的外观感到好奇,可以下载OpenLayers 3 example 在这里。

<!DOCTYPE html>
      <html>
        <head>
          <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
            <title>Farmers markets in Philadelphia</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"></script>
            <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.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.9526,-75.1652],13);

              // create tile layer and add it to map
              var tiles = L.tileLayer('http://personal.psu.edu/juw30/tiles/PhillyBasemap/{z}/{x}/{y}.png');
              tiles.addTo(map);

              // create wms layer
              var farmerMarkets = L.tileLayer.wms('http://localhost:8080/geoserver/geog585/wms', {
                layers: 'philadelphia:FarmersMarkets',
                format: 'image/png',
                transparent: true
              });

              farmerMarkets.addTo(map);

              // define event handler function for click events and register it

              function Identify(e)
              {
                // set parameters needed for GetFeatureInfo WMS request
                var sw = map.options.crs.project(map.getBounds().getSouthWest());
                var ne = map.options.crs.project(map.getBounds().getNorthEast());
                var BBOX = sw.x + "," + sw.y + "," + ne.x + "," + ne.y;
                var WIDTH = map.getSize().x;
                var HEIGHT = map.getSize().y;

                var X = Math.trunc(map.layerPointToContainerPoint(e.layerPoint).x);
                var Y = Math.trunc(map.layerPointToContainerPoint(e.layerPoint).y);

                // compose the URL for the request
                var URL = 'http://localhost:8080/geoserver/geog585/wms?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetFeatureInfo&LAYERS=philadelphia:FarmersMarkets&QUERY_LAYERS=philadelphia:FarmersMarkets&BBOX='+BBOX+'&FEATURE_COUNT=1&HEIGHT='+HEIGHT+'&WIDTH='+WIDTH+'&INFO_FORMAT=application%2Fjson&TILED=false&CRS=EPSG%3A3857&I='+X+'&J='+Y;

                //send GetFeatureInfo as asynchronous HTTP request using jQuery $.ajax

                $.ajax({
                   url: URL,
                   dataType: "json",
                   type: "GET",
                   success: function(data)
                   {
                     if(data.features.length !== 0) {  // at least one feature returned in response
                       var returnedFeature = data.features[0]; // first feature from response

                       // Set up popup for clicked feature and open it
                       var popup = new L.Popup({
                         maxWidth: 300
                       });

                       popup.setContent("<b>" + returnedFeature.properties.NAME + "</b><br />" + returnedFeature.properties.ADDRESS);
                       popup.setLatLng(e.latlng);
                       map.openPopup(popup);
                    }
                  }
                 });
               }

               map.addEventListener('click', Identify);

             }

          </script>
        </head>
        <body onload="init()">
          <h1 id="title">Farmers markets in Philadelphia</h1>
          <div id="mapid">
          </div>
          <div id="docs">
              <p>This page shows farmers markets in Philadelphia, Pennsylvania. Click a market to get more information.</p>
          </div>
        </body>
      </html>