ArcGIS JS API 4.x 开发实战

ArcGIS JS API 4.x 开发实战:SceneView 交互与信息获取

引言

ArcGIS API for JavaScript 4.x 版本为Web GIS应用开发带来了强大的3D能力,其中 SceneView 是构建三维场景的核心组件。在实际应用中,我们经常需要实现用户与三维场景的交互,例如点击场景中的要素以获取其详细信息,或者在点击位置显示弹窗。本文将深入探讨如何在 ArcGIS JS API 4.x 的 SceneView 中实现点击交互、获取元素信息以及显示信息弹窗,并解决开发过程中可能遇到的常见问题。

1. SceneView 点击获取元素信息:hitTest 方法详解

SceneView 中,要获取用户点击位置的元素信息,核心方法是 view.hitTest(event)。这个方法会返回一个Promise,解析后会得到一个包含点击位置处所有图形(Graphic)和图层(Layer)信息的数组。

hitTest 工作原理

当用户在 SceneView 上点击时,hitTest 方法会执行以下操作:

  1. 光线投射:从点击的屏幕坐标发射一条虚拟光线进入三维场景。
  2. 要素识别:检测这条光线穿过的所有可见图形和图层。
  3. 结果返回 :返回一个 HitTestResult 对象的数组,每个对象包含一个 graphic 属性(如果点击到图形)和一个 layer 属性(如果点击到图层)。结果数组按照与视点的距离从近到远排序。

示例代码:点击获取图形信息

以下是一个基本的示例,展示了如何监听 SceneView 的点击事件,并使用 hitTest 获取点击位置的图形信息,然后将其输出到控制台。

javascript 复制代码
require([
  "esri/Map",
  "esri/views/SceneView",
  "esri/layers/GraphicsLayer",
  "esri/Graphic",
  "esri/geometry/Point",
  "esri/symbols/SimpleMarkerSymbol",
  "esri/geometry/SpatialReference",
  "dojo/domReady!"
], function(Map, SceneView, GraphicsLayer, Graphic, Point, SimpleMarkerSymbol, SpatialReference) {

  // 创建一个地图
  var map = new Map({
    basemap: "streets"
  });

  // 创建一个 SceneView
  var view = new SceneView({
    container: "viewDiv",
    map: map,
    scale: 50000000,
    center: [-101.17, 21.78] // 初始中心点
  });

  // 创建一个 GraphicsLayer 用于显示点击的图形
  var graphicsLayer = new GraphicsLayer();
  map.add(graphicsLayer);

  // 添加一个示例图形到 GraphicsLayer
  var samplePoint = new Point({
    x: -100,
    y: 40,
    spatialReference: { wkid: 4326 }
  });

  var sampleSymbol = new SimpleMarkerSymbol({
    color: [255, 0, 0], // 红色
    size: 10,
    outline: {
      color: [255, 255, 255],
      width: 1
    }
  });

  var sampleGraphic = new Graphic({
    geometry: samplePoint,
    symbol: sampleSymbol,
    attributes: { // 为图形添加属性
      name: "示例点",
      description: "这是一个用于测试点击的示例点"
    }
  });
  graphicsLayer.add(sampleGraphic);

  // 监听视图的点击事件
  view.on("click", function(event) {
    // 使用 hitTest 方法获取点击位置的图形和图层信息
    view.hitTest(event).then(function(response) {
      if (response.results.length > 0) {
        // 获取第一个点击结果
        var result = response.results[0];
        var graphic = result.graphic;
        var layer = result.layer;

        // 输出图形和图层信息到控制台
        console.log("点击到的图形:", graphic);
        console.log("所属图层:", layer);

        // 如果点击到图形,可以进一步处理其属性
        if (graphic && graphic.attributes) {
          console.log("图形属性:", graphic.attributes);
          // 可以在这里添加逻辑,例如在点击位置添加一个标记
          // var point = new Point({
          //   x: event.mapPoint.x,
          //   y: event.mapPoint.y,
          //   spatialReference: new SpatialReference({ wkid: 4326 })
          // });
          // var markerSymbol = new SimpleMarkerSymbol({
          //   color: [226, 119, 40],
          //   outline: {
          //     color: [255, 255, 255],
          //     width: 2
          //   }
          // });
          // var clickedGraphic = new Graphic({
          //   geometry: point,
          //   symbol: markerSymbol
          // });
          // graphicsLayer.add(clickedGraphic);
        }
      } else {
        console.log("未点击到任何图形或图层。");
      }
    });
  });
});

注意事项:

  • hitTest 方法返回的结果是一个数组,包含所有在点击位置处的图形和图层信息。通常我们只关心第一个结果(最上层的图形)。
  • 如果点击位置没有图形,results 数组将为空。

2. SceneView 点击弹窗:PopupTemplate 的应用

在获取到图形信息后,通常的需求是在点击位置显示一个信息弹窗(Popup),展示图形的详细属性。ArcGIS JS API 提供了 PopupTemplate 类来方便地定义弹窗的内容和样式。

PopupTemplate 介绍

PopupTemplate 允许你定义弹窗的标题、内容以及如何显示图形的属性。它支持以下几种内容类型:

  • 字符串 :可以直接使用 {fieldName} 占位符来引用图形的属性。
  • 函数:提供一个函数来动态生成HTML内容,这在需要复杂逻辑或格式化时非常有用。
  • HTML:直接嵌入HTML字符串。

示例代码:点击显示弹窗

以下是一个完整的示例代码,展示了如何在 SceneView 中点击元素后弹出一个信息窗口。

javascript 复制代码
require([
  "esri/Map",
  "esri/views/SceneView",
  "esri/layers/GraphicsLayer",
  "esri/Graphic",
  "esri/geometry/Point",
  "esri/symbols/SimpleMarkerSymbol",
  "esri/PopupTemplate",
  "dojo/domReady!"
], function(Map, SceneView, GraphicsLayer, Graphic, Point, SimpleMarkerSymbol, PopupTemplate) {

  // 创建一个地图
  var map = new Map({
    basemap: "streets"
  });

  // 创建一个 SceneView
  var view = new SceneView({
    container: "viewDiv",
    map: map,
    scale: 50000000,
    center: [-101.17, 21.78], // 初始中心点
    zoom: 5
  });

  // 创建一个 GraphicsLayer 并添加到地图中
  var graphicsLayer = new GraphicsLayer();
  map.add(graphicsLayer);

  // 创建一个 PopupTemplate
  var popupTemplate = new PopupTemplate({
    title: "点位信息",
    content: "经度: {x}<br>纬度: {y}<br>名称: {name}<br>描述: {description}"
  });

  // 添加一个示例图形到 GraphicsLayer
  var point = new Point({
    x: -100,
    y: 40,
    spatialReference: { wkid: 4326 }
  });

  var markerSymbol = new SimpleMarkerSymbol({
    color: [226, 119, 40],
    outline: {
      color: [255, 255, 255],
      width: 2
    }
  });

  var graphic = new Graphic({
    geometry: point,
    symbol: markerSymbol,
    attributes: { // 为图形添加属性
      x: point.x,
      y: point.y,
      name: "测试点",
      description: "这是一个带有弹窗信息的测试点"
    },
    popupTemplate: popupTemplate // 绑定 PopupTemplate
  });

  graphicsLayer.add(graphic);

  // 监听 SceneView 的点击事件
  view.on("click", function(event) {
    // 使用 hitTest 方法获取点击位置的图形信息
    view.hitTest(event).then(function(response) {
      if (response.results.length > 0) {
        // 获取第一个点击结果
        var result = response.results[0];
        var graphic = result.graphic;

        // 如果图形有 PopupTemplate,则显示弹窗
        if (graphic && graphic.popupTemplate) {
          view.popup.open({
            features: [graphic], // 传入图形数组
            location: event.mapPoint // 弹窗显示位置
          });
        } else {
          console.log("点击到的图形没有绑定PopupTemplate。");
        }
      } else {
        console.log("未点击到任何图形。");
      }
    });
  });
});

弹窗内容自定义

如果你需要显示更复杂的内容,可以在 PopupTemplate 中使用自定义函数或 HTML 模板。例如,使用函数动态生成内容:

javascript 复制代码
var popupTemplate = new PopupTemplate({
  title: "点位信息",
  content: function(feature) {
    var point = feature.geometry;
    var attributes = feature.attributes;
    return `
      <div>
        <p><b>名称:</b> ${attributes.name}</p>
        <p><b>描述:</b> ${attributes.description}</p>
        <p><b>经度:</b> ${point.x.toFixed(4)}</p>
        <p><b>纬度:</b> ${point.y.toFixed(4)}</p>
      </div>
    `;
  }
});

3. view.hitTest 只获取到 id,没有其他信息?

在开发过程中,你可能会遇到 view.hitTest 返回的结果中 graphic 对象只有 id,而没有 attributes 或其他详细信息的情况。这通常是由于以下几个原因造成的:

原因一:图形没有设置属性数据

hitTest 方法返回的 graphic 对象的 attributes 属性,直接来源于你在创建 Graphic 实例时为其设置的 attributes 对象。如果 Graphic 没有设置 attributes 属性,那么 hitTest 返回的 graphic 自然也就没有这些信息。

解决方法 :确保在创建 Graphic 时,为其设置包含所有需要信息的 attributes 对象。

javascript 复制代码
const point = new Point({ /* ... */ });
const symbol = new SimpleMarkerSymbol({ /* ... */ });

const graphic = new Graphic({
  geometry: point,
  symbol: symbol,
  attributes: { // 确保这里有你需要的属性
    objectId: 1,
    name: "我的第一个点",
    type: "POI",
    value: 123
  }
});
graphicsLayer.add(graphic);

原因二:图层类型不支持返回详细属性

某些图层类型(例如 TileLayerImageryLayer)在 hitTest 时可能只返回图层本身的信息,而不会返回其内部要素的详细属性,因为这些图层通常是预渲染的栅格数据,不包含独立的矢量要素属性。

解决方法

  • 使用 FeatureLayerGraphicsLayer :如果你需要点击要素并获取其详细属性,应将数据作为 FeatureLayerGraphicsLayer 添加到地图中。FeatureLayer 专门用于显示和查询矢量要素,而 GraphicsLayer 用于显示客户端图形。
  • 进行额外查询 :如果数据源是 MapImageLayerWMSLayer 等,你可能需要在 hitTest 之后,根据点击的地理位置和图层信息,向服务器发起额外的查询请求(例如使用 QueryTask),以获取详细的要素属性。

原因三:数据加载或渲染问题

如果图形数据尚未完全加载或渲染完成,hitTest 可能无法正确识别到图形的完整信息。

解决方法

  • 确保数据已完全加载 :对于异步加载的图层(如 FeatureLayer),可以在图层加载完成后再进行 hitTest 操作。
  • 检查网络请求:使用浏览器开发者工具检查网络请求,确保所有相关的资源(如要素数据)都已成功加载。

示例:为 Graphic 添加属性

javascript 复制代码
require([
  "esri/Graphic",
  "esri/geometry/Point",
  "esri/symbols/SimpleMarkerSymbol"
], function(Graphic, Point, SimpleMarkerSymbol) {

  const pointGeometry = new Point({
    longitude: -118.2437,
    latitude: 34.0522
  });

  const pointSymbol = new SimpleMarkerSymbol({
    color: [255, 0, 0],
    size: 10
  });

  // 正确设置 attributes
  const myGraphic = new Graphic({
    geometry: pointGeometry,
    symbol: pointSymbol,
    attributes: {
      name: "洛杉矶",
      population: 3900000,
      description: "美国加利福尼亚州最大城市"
    },
    popupTemplate: {
      title: "{name}",
      content: [
        { type: "fields", fieldInfos: [
          { fieldName: "population", label: "人口" },
          { fieldName: "description", label: "描述" }
        ]}
      ]
    }
  });

  // 将 myGraphic 添加到 GraphicsLayer 或 FeatureLayer 中
  // ...
});

4. 解决 ArcGIS JS API 中的 CORS 问题

在 ArcGIS JS API 开发中,你可能会遇到与CORS相关的错误,例如 PointSymbol3D 无法加载资源,并报错 No 'Access-Control-Allow-Origin' header is present on the requested resource。这与上文提到的CORS问题本质相同,通常是由于API尝试加载的某些资源(如字体、纹理、模型、图标等)存储在不同域名下,且该服务器未正确配置CORS头。

常见原因

  • 外部资源引用:你在代码中直接引用了来自非同源服务器的图片、模型或其他资源。
  • ArcGIS服务资源:某些ArcGIS服务(例如自定义符号、高程服务等)可能需要跨域访问,如果其服务器未正确配置CORS,就会出现问题。

解决方法

解决 ArcGIS JS API 中的CORS问题与通用CORS解决方案类似:

  1. 检查资源路径 :确保 PointSymbol3D 或其他组件使用的资源(例如图标、模型等)是从当前域名或允许跨域访问的服务器加载的。如果资源是从外部服务器加载的,检查该服务器是否支持跨域请求。

  2. 配置服务器的 CORS 头 :如果你有权限管理资源所在的服务器,配置 Access-Control-Allow-Origin 头是最佳实践。具体配置方法请参考本文第一部分的"策略一:修改目标服务器的CORS配置"。

  3. 使用代理服务器:如果无法修改资源服务器的配置,可以通过自己的服务器代理请求。例如,在你的服务器上设置一个代理接口,将跨域请求转发到目标服务器,然后将结果返回给前端。这样,浏览器只会看到同源请求,避免了 CORS 问题。具体配置方法请参考本文第一部分的"策略二:使用代理服务器"。

  4. 将资源本地化:如果资源是静态的(例如图标、模型文件),可以将其下载并保存到你的服务器上,然后通过本地路径引用。例如:

    javascript 复制代码
    const pointSymbol = new PointSymbol3D({
      symbolLayers: [
        new IconSymbol3DLayer({
          resource: { href: "/path/to/local/icon.png" } // 引用本地资源
        })
      ]
    });
  5. 使用 ArcGIS 提供的资源 :如果 PointSymbol3D 使用的是 ArcGIS 提供的默认资源(例如内置图标或模型),确保你的 ArcGIS API for JavaScript 版本是最新的,并且资源路径正确。ArcGIS 官方提供的资源通常已正确配置CORS。

    javascript 复制代码
    const pointSymbol = new PointSymbol3D({
      symbolLayers: [
        new IconSymbol3DLayer({
          resource: { href: "https://static.arcgis.com/icons/25x25.png" } // 确保资源路径正确
        })
      ]
    });
  6. 禁用浏览器 CORS 检查(仅用于开发环境) :与通用CORS问题一样,在开发环境中可以临时禁用浏览器的CORS检查,但绝不能在生产环境中使用。具体方法请参考本文第一部分的"策略五:禁用浏览器CORS检查"。

总结

ArcGIS JS API 4.x 为开发者提供了强大的3D能力,但同时也带来了新的交互和数据管理挑战。通过熟练运用 hitTest 方法获取场景元素信息,结合 PopupTemplate 实现友好的信息展示,并理解和解决CORS等常见问题,开发者可以构建出功能丰富、用户体验优秀的三维Web GIS应用。在开发过程中,务必注意图形属性的完整性以及跨域资源的正确配置,以确保应用的稳定性和安全性。

相关推荐
然我11 分钟前
从 Callback 地狱到 Promise:手撕 JavaScript 异步编程核心
前端·javascript·html
LovelyAqaurius13 分钟前
Flex布局详细攻略
前端
雪中何以赠君别15 分钟前
【JS】箭头函数与普通函数的核心区别及设计意义
前端·ecmascript 6
sg_knight17 分钟前
Rollup vs Webpack 深度对比:前端构建工具终极指南
前端·javascript·webpack·node.js·vue·rollup·vite
NoneCoder20 分钟前
Webpack 剖析与策略
前端·面试·webpack
穗余21 分钟前
WEB3全栈开发——面试专业技能点P3JavaScript / TypeScript
前端·javascript·typescript
a别念m1 小时前
webpack基础与进阶
前端·webpack·node.js
芭拉拉小魔仙1 小时前
【Vue3/Typescript】从零开始搭建H5移动端项目
前端·vue.js·typescript·vant
axinawang1 小时前
通过RedisCacheManager自定义缓存序列化(适用通过注解缓存数据)
前端·spring·bootstrap
前端南玖2 小时前
Vue3响应式核心:ref vs reactive深度对比
前端·javascript·vue.js