第02章 - 环境搭建与快速开始

第02章 - 环境搭建与快速开始

2.1 开发环境准备

2.1.1 系统要求

开发 OpenLayers 应用需要以下基本环境:

操作系统

  • Windows 10/11
  • macOS 10.15+
  • Linux(Ubuntu 20.04+、CentOS 8+)

必备软件

软件 版本要求 说明
Node.js 18.0+ JavaScript 运行环境
npm/yarn/pnpm 最新版 包管理器
Git 2.30+ 版本控制
现代浏览器 Chrome 90+ / Firefox 90+ 开发调试

推荐 IDE

  • Visual Studio Code(推荐)
  • WebStorm
  • Sublime Text

2.1.2 Node.js 安装

Windows 安装

# 使用 nvm-windows 管理 Node.js 版本
# 下载地址:https://github.com/coreybutler/nvm-windows/releases

# 安装 Node.js LTS 版本
nvm install lts
nvm use lts

# 验证安装
node --version
npm --version

macOS 安装

# 使用 Homebrew 安装 nvm
brew install nvm

# 配置环境变量
echo 'export NVM_DIR="$HOME/.nvm"' >> ~/.zshrc
echo '[ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \. "/opt/homebrew/opt/nvm/nvm.sh"' >> ~/.zshrc

# 安装 Node.js
nvm install --lts
nvm use --lts

# 验证安装
node --version
npm --version

Linux 安装

# 安装 nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash

# 重新加载配置
source ~/.bashrc

# 安装 Node.js
nvm install --lts
nvm use --lts

# 验证安装
node --version
npm --version

2.1.3 VS Code 配置

推荐扩展

// .vscode/extensions.json
{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "ms-vscode.vscode-typescript-next",
    "bradlc.vscode-tailwindcss",
    "christian-kohler.path-intellisense",
    "formulahendry.auto-rename-tag",
    "streetsidesoftware.code-spell-checker"
  ]
}

工作区设置

// .vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.tabSize": 2,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "typescript.preferences.importModuleSpecifier": "relative",
  "emmet.includeLanguages": {
    "javascript": "javascriptreact"
  }
}

2.2 项目初始化

2.2.1 使用 Vite 创建项目

创建 Vanilla JavaScript 项目

# 创建项目目录
npm create vite@latest openlayers-demo -- --template vanilla

# 进入项目目录
cd openlayers-demo

# 安装 OpenLayers
npm install ol

# 安装开发依赖
npm install

# 启动开发服务器
npm run dev

项目结构

openlayers-demo/
├── node_modules/
├── public/
│   └── vite.svg
├── src/
│   ├── main.js
│   └── style.css
├── index.html
├── package.json
├── vite.config.js
└── README.md

2.2.2 使用 Vue 3 创建项目

# 创建 Vue 项目
npm create vite@latest openlayers-vue -- --template vue

# 进入项目目录
cd openlayers-vue

# 安装 OpenLayers
npm install ol

# 安装 vue3-openlayers(可选的 Vue 封装)
npm install vue3-openlayers

# 启动开发服务器
npm run dev

Vue 组件示例

<!-- src/components/MapView.vue -->
<template>
  <div ref="mapContainer" class="map-container"></div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import Map from 'ol/Map';
import View from 'ol/View';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import { fromLonLat } from 'ol/proj';
import 'ol/ol.css';

const mapContainer = ref(null);
let map = null;

onMounted(() => {
  map = new Map({
    target: mapContainer.value,
    layers: [
      new TileLayer({
        source: new OSM()
      })
    ],
    view: new View({
      center: fromLonLat([116.4074, 39.9042]),
      zoom: 10
    })
  });
});

onUnmounted(() => {
  if (map) {
    map.setTarget(null);
    map = null;
  }
});
</script>

<style scoped>
.map-container {
  width: 100%;
  height: 100vh;
}
</style>

2.2.3 使用 React 创建项目

# 创建 React 项目
npm create vite@latest openlayers-react -- --template react

# 进入项目目录
cd openlayers-react

# 安装 OpenLayers
npm install ol

# 启动开发服务器
npm run dev

React 组件示例

// src/components/MapView.jsx
import { useEffect, useRef } from 'react';
import Map from 'ol/Map';
import View from 'ol/View';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import { fromLonLat } from 'ol/proj';
import 'ol/ol.css';

function MapView() {
  const mapRef = useRef(null);
  const mapInstanceRef = useRef(null);

  useEffect(() => {
    if (mapRef.current && !mapInstanceRef.current) {
      mapInstanceRef.current = new Map({
        target: mapRef.current,
        layers: [
          new TileLayer({
            source: new OSM()
          })
        ],
        view: new View({
          center: fromLonLat([116.4074, 39.9042]),
          zoom: 10
        })
      });
    }

    return () => {
      if (mapInstanceRef.current) {
        mapInstanceRef.current.setTarget(null);
        mapInstanceRef.current = null;
      }
    };
  }, []);

  return (
    <div 
      ref={mapRef} 
      style={{ width: '100%', height: '100vh' }}
    />
  );
}

export default MapView;

2.2.4 TypeScript 项目配置

# 创建 TypeScript 项目
npm create vite@latest openlayers-ts -- --template vanilla-ts

cd openlayers-ts

# 安装 OpenLayers
npm install ol

# OpenLayers 自带类型定义,无需额外安装
npm run dev

TypeScript 配置

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"]
}

TypeScript 示例

// src/main.ts
import 'ol/ol.css';
import Map from 'ol/Map';
import View from 'ol/View';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import { fromLonLat } from 'ol/proj';
import type { Coordinate } from 'ol/coordinate';

// 定义地图配置类型
interface MapConfig {
  center: Coordinate;
  zoom: number;
}

const config: MapConfig = {
  center: fromLonLat([116.4074, 39.9042]),
  zoom: 10
};

const map = new Map({
  target: 'map',
  layers: [
    new TileLayer({
      source: new OSM()
    })
  ],
  view: new View({
    center: config.center,
    zoom: config.zoom
  })
});

// 类型安全的事件处理
map.on('click', (event) => {
  const coordinate = event.coordinate;
  console.log('点击坐标:', coordinate);
});

2.3 常用底图配置

2.3.1 OpenStreetMap

import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';

// 默认 OSM
const osmLayer = new TileLayer({
  source: new OSM()
});

// 自定义 OSM 服务器
const customOSM = new TileLayer({
  source: new OSM({
    url: 'https://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png',
    maxZoom: 19
  })
});

2.3.2 天地图

import TileLayer from 'ol/layer/Tile';
import XYZ from 'ol/source/XYZ';

// 天地图 key(需要注册获取)
const tdtKey = 'YOUR_TIANDITU_KEY';

// 天地图矢量底图
const tdtVec = new TileLayer({
  source: new XYZ({
    url: `http://t{0-7}.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tdtKey}`,
    maxZoom: 18
  })
});

// 天地图矢量注记
const tdtCva = new TileLayer({
  source: new XYZ({
    url: `http://t{0-7}.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tdtKey}`,
    maxZoom: 18
  })
});

// 天地图影像底图
const tdtImg = new TileLayer({
  source: new XYZ({
    url: `http://t{0-7}.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tdtKey}`,
    maxZoom: 18
  })
});

// 天地图影像注记
const tdtCia = new TileLayer({
  source: new XYZ({
    url: `http://t{0-7}.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tdtKey}`,
    maxZoom: 18
  })
});

2.3.3 高德地图

import TileLayer from 'ol/layer/Tile';
import XYZ from 'ol/source/XYZ';

// 高德矢量底图
const gaodeVec = new TileLayer({
  source: new XYZ({
    url: 'https://webrd0{1-4}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}',
    maxZoom: 18
  })
});

// 高德影像底图
const gaodeImg = new TileLayer({
  source: new XYZ({
    url: 'https://webst0{1-4}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
    maxZoom: 18
  })
});

// 高德影像注记
const gaodeAnn = new TileLayer({
  source: new XYZ({
    url: 'https://webst0{1-4}.is.autonavi.com/appmaptile?style=8&x={x}&y={y}&z={z}',
    maxZoom: 18
  })
});

2.3.4 百度地图(需坐标转换)

import TileLayer from 'ol/layer/Tile';
import TileImage from 'ol/source/TileImage';
import TileGrid from 'ol/tilegrid/TileGrid';
import { get as getProjection } from 'ol/proj';

// 百度地图使用自定义投影,需要特殊处理
// 创建百度地图瓦片网格
const resolutions = [];
for (let i = 0; i < 19; i++) {
  resolutions[i] = Math.pow(2, 18 - i);
}

const baiduTileGrid = new TileGrid({
  origin: [0, 0],
  resolutions: resolutions
});

// 百度地图图层
const baiduLayer = new TileLayer({
  source: new TileImage({
    projection: getProjection('EPSG:3857'),
    tileGrid: baiduTileGrid,
    tileUrlFunction: function(tileCoord) {
      if (!tileCoord) return '';
      const z = tileCoord[0];
      const x = tileCoord[1];
      const y = -tileCoord[2] - 1;
      
      // 百度地图服务器随机选择
      const num = Math.abs((x + y) % 10);
      return `http://online${num}.map.bdimg.com/tile/?qt=tile&x=${x}&y=${y}&z=${z}&styles=pl&udt=20170601`;
    }
  })
});

2.3.5 必应地图

import TileLayer from 'ol/layer/Tile';
import BingMaps from 'ol/source/BingMaps';

// 必应地图(需要 API Key)
const bingKey = 'YOUR_BING_MAPS_KEY';

// 道路地图
const bingRoad = new TileLayer({
  source: new BingMaps({
    key: bingKey,
    imagerySet: 'Road'
  })
});

// 航空影像
const bingAerial = new TileLayer({
  source: new BingMaps({
    key: bingKey,
    imagerySet: 'Aerial'
  })
});

// 航空影像带标签
const bingAerialLabel = new TileLayer({
  source: new BingMaps({
    key: bingKey,
    imagerySet: 'AerialWithLabels'
  })
});

2.3.6 ArcGIS 在线服务

import TileLayer from 'ol/layer/Tile';
import XYZ from 'ol/source/XYZ';

// ArcGIS 世界街道地图
const arcgisStreet = new TileLayer({
  source: new XYZ({
    url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}',
    maxZoom: 19
  })
});

// ArcGIS 世界影像
const arcgisImagery = new TileLayer({
  source: new XYZ({
    url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
    maxZoom: 19
  })
});

// ArcGIS 世界地形
const arcgisTerrain = new TileLayer({
  source: new XYZ({
    url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Terrain_Base/MapServer/tile/{z}/{y}/{x}',
    maxZoom: 13
  })
});

2.3.7 底图切换实现

import Map from 'ol/Map';
import View from 'ol/View';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import XYZ from 'ol/source/XYZ';
import { fromLonLat } from 'ol/proj';

// 定义底图配置
const basemaps = {
  osm: new TileLayer({
    source: new OSM(),
    visible: true
  }),
  gaode: new TileLayer({
    source: new XYZ({
      url: 'https://webrd0{1-4}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}'
    }),
    visible: false
  }),
  arcgis: new TileLayer({
    source: new XYZ({
      url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'
    }),
    visible: false
  })
};

// 创建地图
const map = new Map({
  target: 'map',
  layers: Object.values(basemaps),
  view: new View({
    center: fromLonLat([116.4074, 39.9042]),
    zoom: 10
  })
});

// 切换底图函数
function switchBasemap(name) {
  Object.keys(basemaps).forEach(key => {
    basemaps[key].setVisible(key === name);
  });
}

// 使用示例
document.getElementById('osm-btn').addEventListener('click', () => switchBasemap('osm'));
document.getElementById('gaode-btn').addEventListener('click', () => switchBasemap('gaode'));
document.getElementById('arcgis-btn').addEventListener('click', () => switchBasemap('arcgis'));

2.4 项目配置优化

2.4.1 Vite 配置

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
  // 开发服务器配置
  server: {
    port: 3000,
    open: true,
    cors: true,
    proxy: {
      // 代理 GeoServer 请求
      '/geoserver': {
        target: 'http://localhost:8080',
        changeOrigin: true
      }
    }
  },
  
  // 构建配置
  build: {
    outDir: 'dist',
    sourcemap: true,
    minify: 'terser',
    rollupOptions: {
      output: {
        manualChunks: {
          // 将 OpenLayers 单独打包
          ol: ['ol']
        }
      }
    }
  },
  
  // 优化配置
  optimizeDeps: {
    include: ['ol']
  }
});

2.4.2 ESLint 配置

// eslint.config.js
import js from '@eslint/js';
import globals from 'globals';

export default [
  js.configs.recommended,
  {
    languageOptions: {
      globals: {
        ...globals.browser,
        ...globals.es2021
      },
      ecmaVersion: 'latest',
      sourceType: 'module'
    },
    rules: {
      'no-unused-vars': 'warn',
      'no-console': 'off',
      'semi': ['error', 'always'],
      'quotes': ['error', 'single']
    }
  }
];

2.4.3 Prettier 配置

// .prettierrc
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100,
  "bracketSpacing": true,
  "arrowParens": "avoid"
}

2.4.4 环境变量配置

# .env.development
VITE_MAP_CENTER_LON=116.4074
VITE_MAP_CENTER_LAT=39.9042
VITE_MAP_ZOOM=10
VITE_GEOSERVER_URL=http://localhost:8080/geoserver
VITE_TIANDITU_KEY=your_tianditu_key

# .env.production
VITE_MAP_CENTER_LON=116.4074
VITE_MAP_CENTER_LAT=39.9042
VITE_MAP_ZOOM=10
VITE_GEOSERVER_URL=https://geoserver.example.com/geoserver
VITE_TIANDITU_KEY=your_tianditu_key
// 使用环境变量
import { fromLonLat } from 'ol/proj';

const center = fromLonLat([
  parseFloat(import.meta.env.VITE_MAP_CENTER_LON),
  parseFloat(import.meta.env.VITE_MAP_CENTER_LAT)
]);
const zoom = parseInt(import.meta.env.VITE_MAP_ZOOM);
const geoserverUrl = import.meta.env.VITE_GEOSERVER_URL;

2.5 调试技巧

2.5.1 浏览器开发者工具

// 将地图实例挂载到全局,方便调试
window.map = map;
window.ol = ol;

// 在控制台中可以执行
// map.getView().getCenter()
// map.getLayers().getArray()
// map.getView().setZoom(15)

2.5.2 添加调试图层

import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import { Style, Stroke, Circle, Fill } from 'ol/style';

// 创建调试图层
const debugLayer = new VectorLayer({
  source: new VectorSource(),
  style: new Style({
    stroke: new Stroke({
      color: 'red',
      width: 2
    }),
    image: new Circle({
      radius: 5,
      fill: new Fill({ color: 'red' })
    })
  }),
  zIndex: 999
});

map.addLayer(debugLayer);

// 点击时添加调试点
map.on('click', (event) => {
  const feature = new Feature({
    geometry: new Point(event.coordinate)
  });
  debugLayer.getSource().addFeature(feature);
  console.log('点击坐标:', event.coordinate);
});

2.5.3 地图状态监控

// 监控地图状态变化
map.on('moveend', () => {
  const view = map.getView();
  console.log('地图状态:', {
    center: view.getCenter(),
    zoom: view.getZoom(),
    resolution: view.getResolution(),
    extent: view.calculateExtent(map.getSize())
  });
});

// 监控图层加载
map.getLayers().forEach(layer => {
  if (layer.getSource && layer.getSource().on) {
    layer.getSource().on('tileloadstart', () => {
      console.log('瓦片开始加载');
    });
    layer.getSource().on('tileloadend', () => {
      console.log('瓦片加载完成');
    });
    layer.getSource().on('tileloaderror', (event) => {
      console.error('瓦片加载失败:', event);
    });
  }
});

2.5.4 性能监控

// 帧率监控
let frameCount = 0;
let lastTime = performance.now();

map.on('postrender', () => {
  frameCount++;
  const currentTime = performance.now();
  
  if (currentTime - lastTime >= 1000) {
    console.log(`FPS: ${frameCount}`);
    frameCount = 0;
    lastTime = currentTime;
  }
});

// 渲染时间监控
map.on('precompose', () => {
  console.time('render');
});

map.on('postcompose', () => {
  console.timeEnd('render');
});

2.6 完整示例项目

2.6.1 项目结构

openlayers-quickstart/
├── public/
│   └── favicon.ico
├── src/
│   ├── components/
│   │   ├── MapView.js
│   │   └── LayerSwitcher.js
│   ├── config/
│   │   └── basemaps.js
│   ├── utils/
│   │   └── mapUtils.js
│   ├── styles/
│   │   └── map.css
│   ├── main.js
│   └── style.css
├── index.html
├── package.json
├── vite.config.js
└── README.md

2.6.2 配置文件

// package.json
{
  "name": "openlayers-quickstart",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "ol": "^9.2.4"
  },
  "devDependencies": {
    "vite": "^5.2.0"
  }
}

2.6.3 核心代码

// src/config/basemaps.js
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import XYZ from 'ol/source/XYZ';

export const basemapConfigs = {
  osm: {
    name: 'OpenStreetMap',
    layer: new TileLayer({
      source: new OSM(),
      properties: { name: 'osm' }
    })
  },
  gaode: {
    name: '高德地图',
    layer: new TileLayer({
      source: new XYZ({
        url: 'https://webrd0{1-4}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}'
      }),
      properties: { name: 'gaode' }
    })
  },
  satellite: {
    name: '卫星影像',
    layer: new TileLayer({
      source: new XYZ({
        url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'
      }),
      properties: { name: 'satellite' }
    })
  }
};

export function getBasemapLayers() {
  return Object.values(basemapConfigs).map(config => config.layer);
}
// src/utils/mapUtils.js
import { fromLonLat, toLonLat } from 'ol/proj';

// 坐标转换工具
export function lonLatToWebMercator(lon, lat) {
  return fromLonLat([lon, lat]);
}

export function webMercatorToLonLat(x, y) {
  return toLonLat([x, y]);
}

// 格式化坐标显示
export function formatCoordinate(coordinate, precision = 6) {
  const lonLat = toLonLat(coordinate);
  return `经度: ${lonLat[0].toFixed(precision)}, 纬度: ${lonLat[1].toFixed(precision)}`;
}

// 计算两点距离(米)
export function calculateDistance(coord1, coord2) {
  const [lon1, lat1] = toLonLat(coord1);
  const [lon2, lat2] = toLonLat(coord2);
  
  const R = 6371000; // 地球半径(米)
  const dLat = (lat2 - lat1) * Math.PI / 180;
  const dLon = (lon2 - lon1) * Math.PI / 180;
  
  const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
            Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
            Math.sin(dLon / 2) * Math.sin(dLon / 2);
  
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  
  return R * c;
}
// src/components/MapView.js
import Map from 'ol/Map';
import View from 'ol/View';
import { defaults as defaultControls, ScaleLine, MousePosition } from 'ol/control';
import { createStringXY } from 'ol/coordinate';
import { fromLonLat } from 'ol/proj';
import { getBasemapLayers } from '../config/basemaps.js';

export function createMap(targetId, options = {}) {
  const {
    center = [116.4074, 39.9042],
    zoom = 10,
    minZoom = 2,
    maxZoom = 18
  } = options;

  // 创建控件
  const controls = defaultControls().extend([
    new ScaleLine({
      units: 'metric'
    }),
    new MousePosition({
      coordinateFormat: createStringXY(6),
      projection: 'EPSG:4326',
      className: 'mouse-position'
    })
  ]);

  // 创建地图
  const map = new Map({
    target: targetId,
    layers: getBasemapLayers(),
    view: new View({
      center: fromLonLat(center),
      zoom: zoom,
      minZoom: minZoom,
      maxZoom: maxZoom
    }),
    controls: controls
  });

  // 默认只显示第一个底图
  const layers = map.getLayers().getArray();
  layers.forEach((layer, index) => {
    layer.setVisible(index === 0);
  });

  return map;
}

export function switchBasemap(map, basemapName) {
  const layers = map.getLayers().getArray();
  layers.forEach(layer => {
    const name = layer.get('name');
    if (name) {
      layer.setVisible(name === basemapName);
    }
  });
}
// src/components/LayerSwitcher.js
import { basemapConfigs } from '../config/basemaps.js';
import { switchBasemap } from './MapView.js';

export function createLayerSwitcher(map, containerId) {
  const container = document.getElementById(containerId);
  
  if (!container) {
    console.error('Layer switcher container not found');
    return;
  }

  // 创建底图切换按钮
  Object.entries(basemapConfigs).forEach(([key, config]) => {
    const button = document.createElement('button');
    button.textContent = config.name;
    button.className = 'basemap-btn';
    button.dataset.basemap = key;
    
    button.addEventListener('click', () => {
      switchBasemap(map, key);
      
      // 更新按钮状态
      container.querySelectorAll('.basemap-btn').forEach(btn => {
        btn.classList.remove('active');
      });
      button.classList.add('active');
    });
    
    container.appendChild(button);
  });

  // 默认选中第一个
  container.querySelector('.basemap-btn')?.classList.add('active');
}
// src/main.js
import 'ol/ol.css';
import './styles/map.css';
import { createMap } from './components/MapView.js';
import { createLayerSwitcher } from './components/LayerSwitcher.js';
import { formatCoordinate } from './utils/mapUtils.js';

// 创建地图
const map = createMap('map', {
  center: [116.4074, 39.9042],
  zoom: 10
});

// 创建底图切换器
createLayerSwitcher(map, 'layer-switcher');

// 添加点击事件
map.on('click', (event) => {
  console.log('点击位置:', formatCoordinate(event.coordinate));
});

// 将地图实例挂载到全局(调试用)
window.map = map;

console.log('地图初始化完成');
/* src/styles/map.css */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

#map {
  width: 100%;
  height: 100vh;
}

#layer-switcher {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 1000;
  background: white;
  padding: 10px;
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}

.basemap-btn {
  display: block;
  width: 100%;
  padding: 8px 16px;
  margin-bottom: 5px;
  border: 1px solid #ddd;
  border-radius: 4px;
  background: white;
  cursor: pointer;
  transition: all 0.3s;
}

.basemap-btn:last-child {
  margin-bottom: 0;
}

.basemap-btn:hover {
  background: #f5f5f5;
}

.basemap-btn.active {
  background: #1890ff;
  color: white;
  border-color: #1890ff;
}

.mouse-position {
  position: absolute;
  bottom: 30px;
  left: 10px;
  background: rgba(255, 255, 255, 0.9);
  padding: 5px 10px;
  border-radius: 4px;
  font-size: 12px;
}

.ol-scale-line {
  left: auto;
  right: 10px;
  bottom: 10px;
}
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>OpenLayers 快速入门</title>
</head>
<body>
  <div id="map"></div>
  <div id="layer-switcher"></div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>

2.7 本章小结

本章详细介绍了 OpenLayers 开发环境的搭建:

  1. 开发环境准备:Node.js、VS Code 配置
  2. 项目初始化:Vite、Vue、React、TypeScript 项目创建
  3. 常用底图配置:OSM、天地图、高德、必应、ArcGIS
  4. 项目配置优化:Vite、ESLint、Prettier、环境变量
  5. 调试技巧:浏览器工具、调试图层、状态监控
  6. 完整示例:项目结构和核心代码

关键要点

  • 推荐使用 Vite 作为构建工具
  • OpenLayers 自带 TypeScript 类型定义
  • 国内地图服务需要申请 API Key
  • 将地图实例挂载到全局便于调试
  • 合理使用环境变量管理配置

下一步

在下一章中,我们将深入学习 OpenLayers 的核心概念与架构设计,包括:

  • 核心对象模型
  • 事件系统
  • 可观察模式
  • 模块化设计

← 上一章:OpenLayers概述与入门 | 返回目录 | 下一章:核心概念与架构设计 →

posted @ 2026-01-08 13:40  我才是银古  阅读(9)  评论(0)    收藏  举报