当前位置: 首页 > news >正文

3D 房地产地图 Web 应用

我们将从零开始,一步步使用 Vite、React 和 ArcGIS Maps SDK for JavaScript 构建一个专业的 3D 房地产地图 Web 应用。

这个教程将引导您完成以下核心步骤:

  1. 项目初始化:使用 Vite 创建一个 React 项目。

  2. 环境配置:安装并配置 ArcGIS JS SDK。

  3. 创建 3D 地图容器:构建一个 React 组件来承载 3D SceneView

  4. 加载 3D 建筑数据:添加一个 FeatureLayer 来显示城市建筑,并使用其属性进行 3D 挤出。

  5. 实现商业板块高亮:使用 UniqueValueRenderer 根据建筑属性来区分和高亮显示特定地产。

  6. 添加交互功能:创建侧边栏 UI,通过按钮动态切换高亮效果,并为建筑添加信息弹窗。

在开始之前,请确保您的电脑已安装 Node.js (LTS 版本即可)。

第 1 步:获取 ArcGIS API Key

所有使用 ArcGIS JS SDK 的应用都需要一个 API Key。

  1. 访问 ArcGIS for Developers 网站。

  2. 登录或创建一个免费账户。

  3. 进入您的开发者仪表盘 (Dashboard)

  4. 点击 + New API Key 按钮创建一个新的 API Key。

  5. 复制并保存好这个 Key,我们稍后会在代码中使用它。

第 2 步:使用 Vite 创建 React 项目

打开您的终端 (Terminal 或 Command Prompt),然后执行以下命令。

  1. 创建 Vite 项目

    Bash
    npm create vite@latest real-estate-3d-map -- --template react
    

    这个命令会创建一个名为 real-estate-3d-map 的文件夹,并内置一个基础的 React 项目。

  2. 进入项目目录并安装依赖

    Bash
    cd real-estate-3d-map
    npm install
    
  3. 安装 ArcGIS JS SDK: 这是我们的核心地图库。

    Bash
    npm install @arcgis/core
    

第 3 步:项目结构与代码实现

现在,我们来编写应用的核心代码。请按照以下步骤替换或创建相应的文件。

3.1) 配置 ArcGIS 静态资源

为了让 SDK 正确加载其样式和其他资源,我们需要进行一些配置。

  1. 安装 vite-plugin-static-copy:这个 Vite 插件可以帮助我们把 ArcGIS 的资源文件复制到最终的构建输出目录中。

    Bash
    npm install --save-dev vite-plugin-static-copy
    
  2. 修改 vite.config.js: 将以下内容粘贴到项目根目录的 vite.config.js 文件中。

    JavaScript
    import { defineConfig } from 'vite';
    import react from '@vitejs/plugin-react';
    import { viteStaticCopy } from 'vite-plugin-static-copy';export default defineConfig({plugins: [react(),// viteStaticCopy 插件配置viteStaticCopy({targets: [{src: 'node_modules/@arcgis/core/assets',dest: 'assets'}]})],
    });
    
3.2) 添加全局样式

我们需要引入 ArcGIS 的主题样式,并为我们的应用添加一些布局样式。

  1. 清空 src/index.css 并替换为以下内容:

    CSS
    /* 全局样式重置 */
    html, body, #root {padding: 0;margin: 0;width: 100%;height: 100%;overflow: hidden; /* 防止滚动条出现 */font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen","Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",sans-serif;
    }
    
  2. 清空 src/App.css 并替换为以下内容,用于主应用布局:

    CSS
    /* 主应用容器 */
    .app-container {display: flex;height: 100vh;width: 100vw;
    }/* 地图容器 */
    .map-container {flex-grow: 1; /* 占据剩余所有空间 */height: 100%;
    }/* 侧边栏 */
    .sidebar {width: 300px;padding: 20px;background-color: #f8f9fa;box-shadow: 2px 0 5px rgba(0,0,0,0.1);z-index: 10;display: flex;flex-direction: column;gap: 15px; /* 元素间距 */
    }.sidebar h2 {margin-top: 0;font-size: 1.5em;border-bottom: 2px solid #007ac2;padding-bottom: 10px;
    }.sidebar-description {font-size: 0.9em;color: #555;
    }.highlight-button {padding: 10px 15px;font-size: 1em;background-color: #007ac2;color: white;border: none;border-radius: 4px;cursor: pointer;transition: background-color 0.2s;
    }.highlight-button:hover {background-color: #005a8e;
    }.highlight-button.active {background-color: #28a745; /* 激活状态为绿色 */
    }.highlight-button.active:hover {background-color: #218838;
    }
    
3.3) 创建核心组件

我们将应用分为 App (主应用)、Sidebar (侧边栏) 和 MapContainer (地图容器)。

  1. 创建 src/components 文件夹

    Bash
    mkdir src/components
    
    1. 创建 src/components/MapContainer.jsx: 这是最重要的组件,负责初始化和渲染 3D 地图。我们将使用纽约市的建筑数据作为示例。

      JavaScript
      import React, { useRef, useEffect } from 'react';
      import Map from '@arcgis/core/Map';
      import SceneView from '@arcgis/core/views/SceneView';
      import FeatureLayer from '@arcgis/core/layers/FeatureLayer';
      import Basemap from '@arcgis/core/Basemap';
      import TileLayer from '@arcgis/core/layers/TileLayer';
      import config from '@arcgis/core/config';// 渲染器模块
      import UniqueValueRenderer from "@arcgis/core/renderers/UniqueValueRenderer";
      import {PolygonSymbol3D,ExtrudeSymbol3DLayer
      } from "@arcgis/core/symbols";// 弹窗模块
      import PopupTemplate from "@arcgis/core/PopupTemplate";const MapContainer = ({ isHighlighted }) => {const mapDiv = useRef(null);// 将 view 和 layer 存储在 ref 中,以便在 effect 之间共享而不触发重渲染const viewRef = useRef(null);const layerRef = useRef(null);// 1. 初始化地图和视图useEffect(() => {if (mapDiv.current) {// --- 配置 API Key ---// 重要:在这里替换为您自己的 API Keyconfig.apiKey = "YOUR_API_KEY";// 创建底图const basemap = new Basemap({baseLayers: [new TileLayer({url: "https://tiles.arcgis.com/tiles/nSZVuSZjHpEZZbRo/arcgis/rest/services/Canvas_Dark_GCS/MapServer"})]});// 创建地图实例const map = new Map({basemap: basemap,ground: "world-elevation" // 关键:开启全球高程,实现真实地形});// 创建 3D 视图const view = new SceneView({container: mapDiv.current,map: map,camera: {position: {x: -74.005, // 经度y: 40.71,   // 纬度z: 1500     // 高度 (米)},heading: 0,tilt: 65 // 倾斜角度,以显示 3D 效果}});viewRef.current = view;// --- 定义建筑物的弹窗 ---const popupTemplate = new PopupTemplate({title: "建筑信息",content: `<b>类型:</b> {BIN}<br><b>高度:</b> {HEIGHTROOF:NumberFormat} 米<br><b>建造年份:</b> {CNSTRCT_YR}`});// --- 创建要渲染的建筑图层 ---const buildingsLayer = new FeatureLayer({// 数据源:纽约市建筑 footprinturl: "https://services.arcgis.com/V6ZHFr6zdgNZuVG0/arcgis/rest/services/NYC_Building_Footprints/FeatureServer/0",popupTemplate: popupTemplate,// 性能优化:只请求需要的字段outFields: ["BIN", "HEIGHTROOF", "CNSTRCT_YR"],});layerRef.current = buildingsLayer;map.add(buildingsLayer);// 清理函数:当组件卸载时销毁视图return () => {if (view) {view.destroy();}};}}, []); // 空依赖数组确保此 effect 只运行一次// 2. 处理高亮状态变化的 effectuseEffect(() => {if (!layerRef.current) return;// 根据 isHighlighted 状态动态切换渲染器if (isHighlighted) {// --- 高亮状态的渲染器 ---// 使用 UniqueValueRenderer 来区分我们的地产和其他地产const highlightRenderer = new UniqueValueRenderer({// 字段用于区分:这里我们用“建造年份”来模拟// 假设 2010 年后建成的都是我们的“商业板块”field: "CNSTRCT_YR",defaultSymbol: { // 其他建筑的默认符号 (灰色半透明)type: "polygon-3d",symbolLayers: [new ExtrudeSymbol3DLayer({size: 1, // 挤出高度由 visualVariables 控制material: {color: [180, 180, 180, 0.5]}})]},// 定义需要高亮的特定值uniqueValueInfos: [{value: 2014, // 假如这是我们的一个商业板块symbol: { // 高亮符号 (亮蓝色)type: "polygon-3d",symbolLayers: [new ExtrudeSymbol3DLayer({size: 1,material: {color: "#00C5FF"},edges: { // 添加轮廓线以突出type: "solid",color: [50, 50, 50, 0.7],size: 1.5}})]},label: "Our Property Block A"}, {value: 2015, // 这是另一个商业板块symbol: { // 高亮符号 (亮橙色)type: "polygon-3d",symbolLayers: [new ExtrudeSymbol3DLayer({size: 1,material: {color: "#FFA500"}})]},label: "Our Property Block B"}]});// 将建筑高度映射到挤出效果highlightRenderer.visualVariables = [{type: "size",field: "HEIGHTROOF",valueUnit: "meters"}];layerRef.current.renderer = highlightRenderer;} else {// --- 默认状态的渲染器 (所有建筑同一样式) ---const defaultRenderer = {type: "simple",symbol: {type: "polygon-3d",symbolLayers: [{type: "extrude",size: 1, // 高度由 visualVariables 决定material: { color: [220, 220, 220, 0.9] },edges: {type: "solid",color: [50, 50, 50, 0.5],size: 1}}]},visualVariables: [{type: "size",field: "HEIGHTROOF", // 使用屋顶高度字段进行挤出valueUnit: "meters" // 单位是米}]};layerRef.current.renderer = defaultRenderer;}}, [isHighlighted]); // 当 isHighlighted 变化时触发此 effectreturn <div className="map-container" ref={mapDiv}></div>;
      };export default MapContainer;
      
  2. 创建 src/components/Sidebar.jsx: 这个组件提供用户交互界面。

    JavaScript
    import React, { useRef, useEffect } from "react";
    import Map from "@arcgis/core/Map";
    import SceneView from "@arcgis/core/views/SceneView";
    import SceneLayer from "@arcgis/core/layers/SceneLayer";
    import config from "@arcgis/core/config";
    import UniqueValueRenderer from "@arcgis/core/renderers/UniqueValueRenderer"; // 用于按字段值区分渲染
    import SimpleRenderer from "@arcgis/core/renderers/SimpleRenderer";
    import MeshSymbol3D from "@arcgis/core/symbols/MeshSymbol3D";
    import FillSymbol3DLayer from "@arcgis/core/symbols/FillSymbol3DLayer";
    import PopupTemplate from "@arcgis/core/PopupTemplate";
    import { watch } from "@arcgis/core/core/reactiveUtils";// 配置API Key
    config.apiKey ="your api key";const MapContainer = ({ isHighlighted }) => {const mapDiv = useRef(null);const viewRef = useRef(null);const layerRef = useRef(null);const mapRef = useRef(null);const watchHandles = useRef([]);// 初始化地图和视图useEffect(() => {if (!mapDiv.current) return;const map = new Map({basemap: "arcgis-topographic",ground: "world-elevation",});mapRef.current = map;const view = new SceneView({container: mapDiv.current,map: map,camera: {position: { x: -74.005, y: 40.71, z: 1500 }, // 纽约区域heading: 0,tilt: 65,},constraints: { altitude: { min: 500, max: 5000 } },});viewRef.current = view;view.when(() => {console.log("✅ 3D 视图初始化成功");// 监听底图状态const basemapWatch = watch(() => map.basemap?.loadStatus,(status) => {console.log("📊 底图加载状态:", status);if (status === "failed") {console.error("❌ 切换到备用底图 'osm'");map.basemap = "osm";}});watchHandles.current.push(basemapWatch);// 纽约3D建筑图层const buildingsLayer = new SceneLayer({url: "https://tiles.arcgis.com/tiles/P3ePLMYs2RVChkJx/arcgis/rest/services/Buildings_NewYork_17/SceneServer",popupTemplate: new PopupTemplate({title: "Building Information",content: `<b>Name:</b> {NAME}<br><b>Construction Year:</b> {CNSTRCT_YR}<br><b>Height:</b> {HEIGHTROOF:NumberFormat} ft`,}),});layerRef.current = buildingsLayer;// 监听图层加载状态const layerWatch = watch(() => buildingsLayer.loadStatus,(status) => {console.log("🏗️ 建筑图层状态:", status);if (status === "failed") {console.error("❌ 图层加载失败:",buildingsLayer.loadError?.message);} else if (status === "loaded") {console.log("✅ 建筑图层加载成功,应用初始渲染器");// 图层加载完成后立即应用默认渲染器applyDefaultRenderer(buildingsLayer);}});watchHandles.current.push(layerWatch);map.add(buildingsLayer);}).catch((error) => {console.error("❌ 视图初始化失败:", error);});// 组件卸载清理return () => {watchHandles.current.forEach((handle) => handle.remove());if (viewRef.current) viewRef.current.destroy();};}, []);// 处理高亮状态变化useEffect(() => {const layer = layerRef.current;if (!layer || layer.loadStatus !== "loaded") return;if (isHighlighted) {console.log("🎨 应用高亮渲染器(2014和2015年建筑)");applyHighlightRenderer(layer);} else {console.log("🎨 恢复默认渲染器");applyDefaultRenderer(layer);}}, [isHighlighted]);// 默认渲染器:所有建筑显示为灰色const applyDefaultRenderer = (layer) => {layer.renderer = new SimpleRenderer({symbol: new MeshSymbol3D({symbolLayers: [new FillSymbol3DLayer({material: { color: [220, 220, 220, 0.9] }, // 默认灰色edges: {type: "solid",color: [50, 50, 50, 0.5],size: 1,},}),],}),});};// 高亮渲染器:2014年蓝色,2015年橙色,其他灰色const applyHighlightRenderer = (layer) => {layer.renderer = new UniqueValueRenderer({field: "CNSTRCT_YR", // 按建造年份字段区分defaultSymbol: new MeshSymbol3D({// 非目标年份建筑:灰色symbolLayers: [new FillSymbol3DLayer({material: { color: [180, 180, 180, 0.6] },}),],}),// 目标年份的特殊样式uniqueValueInfos: [{value: 2014, // 2014年建筑symbol: new MeshSymbol3D({symbolLayers: [new FillSymbol3DLayer({material: { color: "#00C5FF" }, // 亮蓝色edges: {type: "solid",color: [0, 0, 0, 0.8],size: 1.5, // 加粗边框突出显示},}),],}),label: "2014 Commercial Block", // 图例标签},{value: 2015, // 2015年建筑symbol: new MeshSymbol3D({symbolLayers: [new FillSymbol3DLayer({material: { color: "#FFA500" }, // 橙色edges: {type: "solid",color: [0, 0, 0, 0.8],size: 1.5,},}),],}),label: "2015 Commercial Block",},],});};return (<divref={mapDiv}style={{width: "100vw",height: "100vh",margin: 0,padding: 0,overflow: "hidden",}}></div>);
    };export default MapContainer;
    
  3. 修改 src/App.jsx: 这是主应用组件,负责整合 SidebarMapContainer,并管理高亮状态。

    JavaScript

    import React, { useState } from 'react';
    import MapContainer from './components/MapContainer';
    import Sidebar from './components/Sidebar';
    import './App.css';
    import '@arcgis/core/assets/esri/themes/dark/main.css';function App() {// 使用 React State 来管理高亮状态const [isHighlighted, setIsHighlighted] = useState(false);// 按钮点击事件处理函数const handleToggleHighlight = () => {setIsHighlighted(prevState => !prevState);};return (<div className="app-container"><SidebarisHighlighted={isHighlighted}onToggleHighlight={handleToggleHighlight}/><MapContainer isHighlighted={isHighlighted} /></div>);
    }export default App;
    
  4. 修改 src/main.jsx: 确保 ArcGIS JS SDK 的 CSS 在应用加载时被正确引入。我们将它移到 App.jsx 中以确保顺序正确,所以 main.jsx 保持 Vite 的默认设置即可。

    JavaScript

    import React from 'react'
    import ReactDOM from 'react-dom/client'
    import App from './App.jsx'
    import './index.css'ReactDOM.createRoot(document.getElementById('root')).render(<React.StrictMode><App /></React.StrictMode>,
    )
    

第 4 步:运行您的 3D 地图应用

  1. 重要:替换 API Key 打开 src/components/MapContainer.jsx 文件,找到下面这行代码,并将 "YOUR_API_KEY" 替换为您在第一步中获取的真实 API Key。

    JavaScript

    config.apiKey = "YOUR_API_KEY";
    
  2. 启动开发服务器 在您的终端中,确保您仍在 real-estate-3d-map 目录下,然后运行:

    Bash

    npm run dev
    
  3. 查看结果 Vite 会启动一个本地开发服务器。在浏览器中打开它提供的 URL (通常是 http://localhost:5173)。

    您现在应该能看到一个带有纽约市 3D 建筑的交互式地图。

    • 默认视图:所有建筑都以灰色 3D 形式挤出。

    • 点击建筑:会弹出一个信息框,显示其属性。

    • 点击“高亮商业板块”按钮:地图上的建筑会重新渲染,建造于 2014 年和 2015 年的建筑将分别以亮蓝色和亮橙色高亮显示,而其他建筑则变为半透明灰色。再次点击按钮可恢复原状。

方案总结与扩展

这个从0到1的实现完全遵循了您提出的设计方案:

  • 架构:使用了 React 和 Vite,通过 @arcgis/core 包集成地图,并通过 React Hooks (useRef, useEffect, useState) 管理地图生命周期和应用状态。

  • 核心组件:成功创建了 MapContainerSidebar

  • 3D 渲染与高亮:加载了 SceneView,通过 FeatureLayerrenderer 属性和 visualVariables 实现了建筑物的 3D 挤出。Highlighter 逻辑通过在 useEffect 中动态切换 UniqueValueRenderer 来实现。

  • 交互性:实现了点击弹出信息 (PopupTemplate) 和通过侧边栏按钮控制地图状态的功能。

下一步扩展建议

  • 数据导入:您可以添加文件上传功能(如使用 papaparse@loaders.gl/shapefile),让用户上传自己的 GeoJSON 或 Shapefile,并将其转换为 GraphicsLayerFeatureLayer 添加到地图上。

  • 高级状态管理:如果应用变得更复杂(例如,有多个图层、多种过滤器),可以引入 Redux Toolkit 或 Zustand 来管理全局状态。

  • UI 优化:引入 Esri 的 Calcite Design System for React,以获得一套专业且与 GIS 应用风格统一的 UI 组件。

  • 性能优化:对于超大规模的数据集,可以研究 FeatureLayer 的量化(quantization)参数或考虑使用自定义的 WebGL 渲染(如 @deck.gl/arcgis)。

http://www.xdnf.cn/news/1450261.html

相关文章:

  • 从0到1搭建某铝箔智慧工厂网络:5G与WiFi 6助力智能制造
  • 渐变背景色和渐变字体颜色的实现方法
  • GPT-5冷酷操盘,游戏狼人杀一战封神!七大LLM狂飙演技,人类玩家看完沉默
  • 学习日记-SpringMVC-day49-9.4
  • 卫星通信+地面网络融合 Sivers半导体毫米波技术打通智慧交通最后一公里
  • DevOps平台选型指南:破解研发效率瓶颈,适配金融/政务/国产化场景的5大关键指标
  • E-E-A-T与现代SEO:赢得搜索引擎信任的完整策略
  • 高效办公新选择:艾克斯音频转文本工具——免费本地化AI识别神器
  • 第15章 Jenkins最佳实践
  • GitHub每日最火火火项目(9.4)
  • 在树莓派集群上部署 Distributed Llama (Qwen 3 14B) 详细指南
  • “乾坤大挪移”:耐达讯自动化RS485转Profinet解锁HMI新乾坤
  • 当Python遇见高德:基于PyQt与JS API构建桌面三维地形图应用实战
  • leetcode算法刷题的第二十六天
  • 软考中级习题与解答——第二章_程序语言与语言处理程序(2)
  • 用Logseq与cpolar:构建开源笔记的分布式协作系统
  • openEuler2403安装部署Kafka
  • 【图像处理基石】图像在频域处理和增强时,如何避免频谱混叠?
  • 机电装置:从基础原理到前沿应用的全方位解析
  • 大模型RAG项目实战:阿里巴巴GTE向量模型
  • 【算法--链表题5】24.两两交换链表中的节点--通俗讲解
  • 【Unity开发】热更新学习——AssetBundle
  • 华清远见25072班I/O学习day4
  • 从端口耗尽危机到性能提升:一次数据库连接问题的深度剖析与解决
  • Ansible 核心功能:循环、过滤器、判断与错误处理全解析
  • Java学习笔记一(数据类型,运算符,流程控制)
  • GitLens:VS Code下高效解决代码追溯的Git管理实用插件
  • 【串口过滤工具】串口调试助手LTSerialTool v3.12.0发布
  • Redis 发布订阅模式详解:实现高效实时消息通信
  • TDengine TIMEDIFF() 函数用户使用手册