Cesium 入门(二):Geocoder 搜索框参数详解与天地图搜索接入

Cesium 入门(二):Geocoder 搜索框参数详解与天地图搜索接入

上一篇我们跑通了 Vue3 + Vite + Cesium + 天地图底图。这一篇来深挖 Cesium 自带的搜索框(Geocoder)------从 Viewer 配置、Widget 实例、ViewModel 状态,到接入天地图地理编码服务,把搜索功能彻底吃透。

一、先建立整体认知:四层结构

Cesium 的搜索能力不是单一组件,而是分了四层:

arduino 复制代码
Viewer 构造参数 (geocoder: true/false/对象)
    ↓
Geocoder Widget(UI 层,负责输入框、下拉列表、DOM 事件)
    ↓
GeocoderViewModel(状态层,管理 searchText、autoComplete、flightDuration 等)
    ↓
GeocoderService(HTTP 层,负责真正的地理编码请求)

理解这个分层非常重要:options 只在初始化时被读取一次,用来创建 ViewModel;Widget 实例本身并不保存这些配置,真正的运行时状态都在 viewModel 。后续我们想动态改搜索行为,操作的是 myGeocoder.viewModel


二、Viewer 的 geocoder 参数:最简单的开关

new Cesium.Viewer() 时传入 geocoder: true,Cesium 就会在地图右上角渲染一个默认搜索框:

js 复制代码
viewer = new Cesium.Viewer('cesium-container', {
  // ... 其他配置
  geocoder: true, // 开启默认搜索框
})

如果不传值geocoder 会走 Cesium 内部的默认值,等效于创建了一个默认配置的 Geocoder Widget:

js 复制代码
// 不传时,Cesium 内部等价于
new Geocoder({
  container: 'cesium-viewer-geocoderContainer',
  scene: viewer.scene,
})

效果就是地图右上角出现搜索框:

此时我们在控制台打印:

js 复制代码
console.log(viewer.geocoder) // Geocoder Widget 实例,第二层
console.log(viewer.geocoder.viewModel) // GeocoderViewModel,第三层

可以看到 viewModel 上有 autoCompleteflightDurationdestinationFoundsearchText 等属性,这些都是运行时真正被读取的状态。

⚠️ 默认搜索框调用的是 Cesium ion 的地理编码服务,用的是内置默认 token,对中文地名支持很差,生产环境必须替换掉。


三、自定义搜索框:手动 new 一个 Geocoder

默认搜索框固定在右上角,样式也不受我们控制。如果我们想把它放到左上角、自定义输入框宽度、或者接入自己的搜索源,就要手动实例化 Cesium.Geocoder

3.1 关闭默认搜索框,准备挂载点

js 复制代码
viewer = new Cesium.Viewer('cesium-container', {
  // ... 其他配置
  geocoder: false, // 关闭默认搜索框
})

在页面上放一个用于挂载搜索框的 div

vue 复制代码
<template>
  <div id="cesium-container">
    <div id="my-geocoder"></div>
  </div>
</template>

3.2 手动实例化 Geocoder

js 复制代码
const myGeocoder = new Cesium.Geocoder({
  container: 'my-geocoder',          // 挂载到页面上的 div id
  scene: viewer.scene,               // 必须,搜索后飞行需要场景
  autoComplete: false,               // 初始化时关闭输入联想
  flightDuration: 3,                 // 搜索成功后的飞行动画时长,单位秒
  geocoderServices: [
    new Cesium.IonGeocoderService({ scene: viewer.scene }),
  ],
  destinationFound: function (vm, destination) {
    console.log('搜索成功,坐标', destination)
    viewer.camera.flyTo({ destination, duration: 3 })
  },
})

window.myGeocoder = myGeocoder // 方便在控制台调试

运行后,左上角就出现了完全由我们控制的搜索框:

3.3 这些 options 到底去哪了?

注意一个关键细节:options 只用来初始化 ViewModel 。Widget 实例本身并不会保存 autoCompleteflightDuration 这些值。你打印 myGeocoder 时,会发现这些属性在 viewModel 上:

js 复制代码
myGeocoder.viewModel.autoComplete   // false
myGeocoder.viewModel.flightDuration // 3

Widget 层主要负责 UI 交互 :监听输入框的 focus、blur、keydown,显示/隐藏下拉列表,调用 ViewModel 的命令;而真正的业务状态、配置、搜索结果都保存在 ViewModel 上 。这也是为什么后面想动态改配置,必须操作 viewModel 而不是 Widget 实例。


四、接入天地图搜索:实现 GeocoderService

默认的 IonGeocoderService 对中文支持不好,而且依赖 Cesium ion 服务。我们有天地图密钥,完全可以自己实现一个 GeocoderService,让搜索框走天地图地名搜索 API。

4.1 天地图 GeocoderService 实现

新建 src/services/tianditu-geocoder-service.js

js 复制代码
import * as Cesium from 'cesium'

/**
 * 天地图地名搜索服务,用于接入 Cesium 的 Geocoder 组件
 * 文档:http://lbs.tianditu.gov.cn/server/search.html
 *
 * Cesium 要求实现 geocode(query),
 * 返回 Promise<{displayName, destination}[]>,destination 为 Cartesian3 或 Rectangle
 *
 * 注意:天地图的 tk 必须是"浏览器端"类型密钥,服务端会校验 Referer
 */
export default class TiandituGeocoderService {
  constructor(token) {
    this._token = token
  }

  // Cesium GeocoderService 接口
  async geocode(query) {
    const postStr = {
      keyWord: query,                  // 天地图要求的参数名是 keyWord,不是 query
      mapBound: '-180,-90,180,90',     // 全球范围
      level: 12,
      queryType: 1,                    // 普通搜索
      start: 0,
      count: 10,
    }

    const resource = new Cesium.Resource({
      url: 'https://api.tianditu.gov.cn/v2/search', // 注意是 /v2/search,不是 /search
      queryParameters: {
        postStr: JSON.stringify(postStr),
        type: 'query',
        tk: this._token,
      },
    })

    try {
      const json = await resource.fetchJson()
      const pois = json?.pois ?? []

      return pois
        .map((poi) => {
          const [lon, lat] = (poi.lonlat ?? '').split(',').map(Number)
          const address = poi.address || ''

          return {
            displayName: address ? `${poi.name}(${address})` : poi.name,
            // 3000 米高度,保证飞行后能看到周边环境
            destination: Cesium.Cartesian3.fromDegrees(lon, lat, 3000),
          }
        })
        .filter((r) => r.displayName && !Number.isNaN(r.destination.x))
    } catch (e) {
      console.warn('天地图搜索失败:', e)
      return []
    }
  }
}

4.2 几个实测踩坑点

在接入过程中,我踩了三个坑,这里一次性列出来:

说明
端点 天地图地名搜索 V2.0 端点是 https://api.tianditu.gov.cn/v2/search,用旧版 /search 会直接 404
参数名 关键字参数必须是 keyWord,写成 query 会返回"缺少参数:keyWord"
Token 类型 天地图服务端会校验 Referer 和 User-Agent,必须使用浏览器端类型的 tk;curl 裸调会被拒绝,但在浏览器里 fetch 能正常通过

4.3 把天地图服务挂到自定义搜索框上

js 复制代码
import TiandituGeocoderService from './services/tianditu-geocoder-service.js'

const TIANDITU_TOKEN = '*****' // 替换成你的天地图浏览器端密钥

const myGeocoder = new Cesium.Geocoder({
  container: 'my-geocoder',
  scene: viewer.scene,
  autoComplete: true, // 打开输入联想,天地图也会返回候选结果
  flightDuration: 3,
  geocoderServices: [new TiandituGeocoderService(TIANDITU_TOKEN)],
  destinationFound: function (vm, destination) {
    console.log('搜索成功,坐标', destination)
    viewer.camera.flyTo({ destination, duration: 3 })
  },
})

输入"天安门",就能看到天地图返回的候选列表:

选择结果后,destinationFound 回调里打印出 Cartesian3 坐标:

控制台里也能看到天地图返回的原始 POI 数据结构:

最终相机会飞到目标位置,控制台还会打印"相机飞完了":


五、运行时操控:GeocoderViewModel 状态模型

搜索框初始化之后,我们常常需要在运行时动态控制它。比如:

  • 根据用户权限动态开启/关闭输入联想
  • 根据页面状态修改飞行时长
  • 从外部回填搜索文本
  • 监听搜索完成事件

这些都要通过 viewModel 来操作:

js 复制代码
const vm = myGeocoder.viewModel

// 1. 动态开启/关闭输入联想
vm.autoComplete = true
// 改成 false 后,下拉联想会立即消失

// 2. 动态修改飞行时长
vm.flightDuration = 5

// 3. 外部回填输入框文本
vm.searchText = '北京'

// 4. 查看当前是否正在搜索(只读响应式属性)
console.log(vm.isSearching) // false,开始搜索时变为 true

// 5. 查看联想结果列表
console.log(vm.searchSuggestions)

// 6. 监听相机飞行完成事件
vm.complete.addEventListener(() => {
  console.log('相机飞完了')
})

为什么要这么做?

因为 options 只在构造函数里被读取一次 ,用来创建 ViewModel。之后所有的交互------输入、联想、搜索、飞行------都由 ViewModel 来管理。Widget 只负责把 DOM 事件转发给 ViewModel,并把 ViewModel 的状态反映到界面上

所以记住这个口诀:

初始化看 options,运行时改 viewModel。


六、完整可运行代码

6.1 src/services/tianditu-geocoder-service.js

js 复制代码
import * as Cesium from 'cesium'

export default class TiandituGeocoderService {
  constructor(token) {
    this._token = token
  }

  async geocode(query) {
    const postStr = {
      keyWord: query,
      mapBound: '-180,-90,180,90',
      level: 12,
      queryType: 1,
      start: 0,
      count: 10,
    }

    const resource = new Cesium.Resource({
      url: 'https://api.tianditu.gov.cn/v2/search',
      queryParameters: {
        postStr: JSON.stringify(postStr),
        type: 'query',
        tk: this._token,
      },
    })

    try {
      const json = await resource.fetchJson()
      const pois = json?.pois ?? []

      return pois
        .map((poi) => {
          const [lon, lat] = (poi.lonlat ?? '').split(',').map(Number)
          const address = poi.address || ''
          return {
            displayName: address ? `${poi.name}(${address})` : poi.name,
            destination: Cesium.Cartesian3.fromDegrees(lon, lat, 3000),
          }
        })
        .filter((r) => r.displayName && !Number.isNaN(r.destination.x))
    } catch (e) {
      console.warn('天地图搜索失败:', e)
      return []
    }
  }
}

6.2 src/App.vue

vue 复制代码
<script setup>
import * as Cesium from 'cesium'
import { onBeforeUnmount, onMounted } from 'vue'
import TiandituGeocoderService from './services/tianditu-geocoder-service.js'

const TIANDITU_TOKEN = '*****' // 替换为你的天地图浏览器端密钥

let viewer = null
let myGeocoder = null

function createTiandituLayer(layerName) {
  return new Cesium.WebMapTileServiceImageryProvider({
    url: `https://t{s}.tianditu.gov.cn/${layerName}_w/wmts?service=wmts&request=GetTile&version=1.0.0&LAYER=${layerName}&tileMatrixSet=w&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${TIANDITU_TOKEN}`,
    layer: layerName,
    style: 'default',
    format: 'tiles',
    tileMatrixSetID: 'w',
    subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],
    maximumLevel: 18,
  })
}

onMounted(() => {
  viewer = new Cesium.Viewer('cesium-container', {
    baseLayer: new Cesium.ImageryLayer(createTiandituLayer('img')),
    animation: false,
    timeline: false,
    geocoder: false, // 关闭默认搜索框
    baseLayerPicker: false,
    homeButton: false,
    sceneModePicker: false,
    navigationHelpButton: false,
    fullscreenButton: false,
  })

  viewer.imageryLayers.addImageryProvider(createTiandituLayer('cia'))

  // 手动创建自定义搜索框
  myGeocoder = new Cesium.Geocoder({
    container: 'my-geocoder',
    scene: viewer.scene,
    autoComplete: true,
    flightDuration: 3,
    geocoderServices: [new TiandituGeocoderService(TIANDITU_TOKEN)],
    destinationFound: function (vm, destination) {
      console.log('搜索成功,坐标', destination)
      viewer.camera.flyTo({ destination, duration: 3 })
    },
  })

  // 动态控制示例(可删)
  const vm = myGeocoder.viewModel
  vm.complete.addEventListener(() => {
    console.log('相机飞完了')
  })

  viewer.camera.flyTo({
    destination: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 50000),
  })
})

onBeforeUnmount(() => {
  myGeocoder?.destroy()
  viewer?.destroy()
  viewer = null
})
</script>

<template>
  <div id="cesium-container">
    <div id="my-geocoder"></div>
  </div>
</template>

<style scoped>
#cesium-container {
  width: 100%;
  height: 100%;
  position: relative;
}

#my-geocoder {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 999;
}

:deep(.cesium-geocoder-input) {
  width: 240px;
}
</style>

七、常见踩坑速查

现象 原因及解决
默认搜索框搜中文地名没结果 默认走 Cesium ion 地理编码服务,对中文支持差;换成天地图等国产服务
new Cesium.IonGeocoderService() 报错 options.scene is required 该版本构造函数必须传 scene,写成 new Cesium.IonGeocoderService({ scene: viewer.scene })
天地图搜索报 404 端点要用 /v2/search,不是 /search
天地图返回"缺少参数:keyWord" 参数名是 keyWord,不是 query
curl 能调通但浏览器报权限错误 天地图服务端校验 Referer,浏览器端 tk 需要在浏览器环境使用;确保 token 类型是"浏览器端"
修改 myGeocoder.autoComplete 无效 状态在 viewModel 上,要改 myGeocoder.viewModel.autoComplete
路由切换后搜索框事件还在 记得在 onBeforeUnmount 里调 myGeocoder.destroy()

写在最后

这一篇我们从 Viewer 的 geocoder 参数出发,逐层拆到了 Widget、ViewModel、GeocoderService,并最终把天地图搜索接入了自定义搜索框。

核心收获就两条:

  1. UI 想自定义,手动 new Cesium.Geocoder(),自己挂 DOM;
  2. 运行时想控制,全部找 viewModel

本文是《Cesium 入门》系列第 2 篇,示例代码可直接复制运行。发布前请把 TIANDITU_TOKEN 替换为你自己的天地图浏览器端密钥。

相关推荐
灵境(虚幻知音)1 天前
Cesium动态轨迹性能瓶颈深度拆解:翼带与尾迹的底层优化实践
性能优化·cesium·3d引擎·afsim·翼带·尾迹·自定义着色器
fxshy3 天前
WebGIS 游戏化实践:基于 Cesium + Vue3 实现全球飞行模拟系统:从球体坐标、飞行动力学到地形碰撞实战
游戏·vue3·cesium·webgis·飞行模拟
用户83134859306984 天前
Cesium实现动态流动火烧云晚霞效果(可用slider调整火烧云浓度)
vue.js·webgl·cesium
CBX6 天前
Cesium 入门实战:GeoJSON 数据加载与地区边界可视化
cesium
毕安格 - BimAngle12 天前
国家电网 GIM 格式模型一键输出 3D Tiles (for Cesium) 和 glTF/glb 更新时间:2026-08-18
3d·gis·cesium·gltf·glb·3d tiles·gim
探索前端13 天前
3dtiles加载时被地形遮挡问题研究及处理思路
前端·3d·cesium
兔年鸿运Q小Q23 天前
cesium1.140以上版本加载地形
arcgis·cesium
用户831348593069824 天前
Vue3+Cesium实现阴天乌云+下雨天气特效
vue.js·webgl·cesium
REDcker1 个月前
Cesium三维WebGIS入门详解
前端·gis·web·cesium·webgis