ITPub博客

首页 > 应用开发 > IT综合 > Java与Leaflet融合实践:构建湖南省道路WebGIS系统

Java与Leaflet融合实践:构建湖南省道路WebGIS系统

投稿 IT综合 作者:bhluo 时间:2025-11-24 17:03:06 0 删除 编辑

# Java与Leaflet融合实践:构建湖南省道路WebGIS系统


## WebGIS系统架构与技术选型


地理信息系统在现代信息化建设中扮演着重要角色,而WebGIS通过浏览器为用户提供便捷的地理数据访问和分析能力。湖南省作为中国中部重要省份,其道路网络错综复杂,构建一个能够展示和分析道路长度的WebGIS系统具有重要的实用价值。


本系统采用Java作为后端服务语言,结合Leaflet前端地图库,构建一个轻量级、高性能的湖南省道路长度可视化平台。Java提供稳定的数据服务和业务逻辑处理,Leaflet负责前端地图渲染和交互,两者通过RESTful API进行数据通信,形成完整的技术解决方案。


## 系统架构设计与数据准备


### 后端服务架构


```java

// 道路数据实体类

public class RoadSegment {

    private String roadId;

    private String roadName;

    private String roadType;

    private double length; // 单位:公里

    private List<Coordinate> coordinates;

    private String district; // 所属行政区划

    

    // 构造方法

    public RoadSegment(String roadId, String roadName, String roadType, 

                      double length, List<Coordinate> coordinates, String district) {

        this.roadId = roadId;

        this.roadName = roadName;

        this.roadType = roadType;

        this.length = length;

        this.coordinates = coordinates;

        this.district = district;

    }

    

    // getter 和 setter 方法

    public String getRoadId() { return roadId; }

    public void setRoadId(String roadId) { this.roadId = roadId; }

    

    public String getRoadName() { return roadName; }

    public void setRoadName(String roadName) { this.roadName = roadName; }

    

    public String getRoadType() { return roadType; }

    public void setRoadType(String roadType) { this.roadType = roadType; }

    

    public double getLength() { return length; }

    public void setLength(double length) { this.length = length; }

    

    public List<Coordinate> getCoordinates() { return coordinates; }

    public void setCoordinates(List<Coordinate> coordinates) { 

        this.coordinates = coordinates; 

    }

    

    public String getDistrict() <"1K.2597.HK">{ return district; }

    public void setDistrict(String district) { this.district = district; }

}


// 地理坐标类

public class Coordinate {

    private double longitude;

    private double latitude;

    

    public Coordinate(double longitude, double latitude) {

        this.longitude = longitude;

        this.latitude = latitude;

    }

    

    // getter 和 setter 方法

    public double getLongitude() { return longitude; }

    public void setLongitude(double longitude) { this.longitude = longitude; }

    

    public double getLatitude() { return latitude; }

    public void setLatitude(double latitude) { this.latitude = latitude; }

}

```


### Spring Boot后端服务


```java

// Spring Boot主应用

@SpringBootApplication

@EnableWebMvc<"2Y.2597.HK">

public class HunanRoadGisApplication {

    public static void main(String[] args) {

        SpringApplication.run(HunanRoadGisApplication.class, args);

    }

}


// 道路数据服务控制器

@RestController

@RequestMapping("/api/roads")

@CrossOrigin(origins = "*") // 允许跨域访问

public class RoadDataController {

    

    @Autowired

    private RoadDataService roadDataService;

    

    /**

     * 获取湖南省所有道路数据

     */

    @GetMapping("/all")

    public ResponseEntity<List<RoadSegment>> getAllRoads() {

        try {

            List<RoadSegment> roads = roadDataService.getAllRoads();

            return ResponseEntity.ok(roads);

        } catch (Exception e) {

            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();

        }

    }

    

    /**

     * 根据行政区划获取道路数据

     */

    @GetMapping("/district/{districtName}")

    public ResponseEntity<List<RoadSegment>> getRoadsByDistrict(

            @PathVariable String districtName) {

        try {

            List<RoadSegment> roads = roadDataService.getRoadsByDistrict(districtName);

            return ResponseEntity.ok(roads);

        } catch (Exception e) {

            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();

        }

    }

    

    /**

     * 根据道路类型获取数据

     */

    @GetMapping("/type/{roadType}")

    public ResponseEntity<List<RoadSegment>> getRoadsByType(

            @PathVariable String roadType) {

        try {

            List<RoadSegment> roads = roadDataService.getRoadsByType(roadType);

            return ResponseEntity.ok(roads);

        } catch (Exception e)<"3P.2597.HK"> {

            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();

        }

    }

    

    /**

     * 获取道路长度统计信息

     */

    @GetMapping("/statistics")

    public ResponseEntity<RoadStatistics> getRoadStatistics() {

        try {

            RoadStatistics statistics = roadDataService.getRoadStatistics();

            return ResponseEntity.ok(statistics);

        } catch (Exception e) {

            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();

        }

    }

}


// 道路数据服务实现

@Service

public class RoadDataService {

    

    /**

     * 模拟数据 - 实际项目中应从数据库或GIS数据文件读取

     */

    public List<RoadSegment> getAllRoads() {

        List<RoadSegment> roads = new ArrayList<>();

        

        // 模拟湖南省主要道路数据

        roads.add(new RoadSegment("G4-001", "京港澳高速湖南段", "高速公路", 

                532.5, generateCoordinates(28.2282, 112.9388, 26.1536, 113.0305, 50),

                "全省"));

        

        roads.add(new RoadSegment("G60-001", "沪昆高速湖南段", "高速公路", 

                415.3, generateCoordinates(27.6253, 111.4526, 27.2361, 113.0223, 40),

                "全省"));

        

        roads.add(new RoadSegment("S101-001", "长沙市区主干道", "省级道路", 

                89.7, generateCoordinates(28.2282, 112.9388, 28.112, 113.0815, 20),

                "长沙市"));

        

        // 添加更多模拟数据...

        return roads;

    }

    

    /**

     * 生成模拟的路径坐标

     */

    private List<Coordinate> generateCoordinates(double startLat, double startLon,

                                               double endLat, double endLon, 

                                               int pointCount) {

        List<Coordinate> coordinates = new ArrayList<>();

        for (int i = 0; i <"4G.2597.HK">< pointCount; i++) {

            double lat = startLat + (endLat - startLat) * i / pointCount;

            double lon = startLon + (endLon - startLon) * i / pointCount;

            // 添加一些随机偏移使路径更自然

            lat += (Math.random() - 0.5) * 0.1;

            lon += (Math.random() - 0.5) * 0.1;

            coordinates.add(new Coordinate(lon, lat));

        }

        return coordinates;

    }

    

    public List<RoadSegment> getRoadsByDistrict(String districtName) {

        return getAllRoads().stream()

                .filter(road -> districtName.equals(road.getDistrict()))

                .collect(Collectors.toList());

    }

    

    public List<RoadSegment> getRoadsByType(String roadType) {

        return getAllRoads().stream()

                .filter(road -> roadType.equals(road.getRoadType()))

                .collect(Collectors.toList());

    }

    

    public RoadStatistics getRoadStatistics() {

        List<RoadSegment> allRoads = getAllRoads();

        RoadStatistics stats = new RoadStatistics();

        

        // 计算各类道路总长度

        Map<String, Double> lengthByType = allRoads.stream()

                .collect(Collectors.groupingBy(

                    RoadSegment::getRoadType,

                    Collectors.summingDouble(RoadSegment::getLength)

                ));

        

        Map<String, Double> lengthByDistrict = allRoads.stream()

                .collect(Collectors.groupingBy(

                    RoadSegment::getDistrict,

                    Collectors.summingDouble(RoadSegment::getLength)

                ));

        

        stats.setTotalLength(allRoads.stream().mapToDouble(RoadSegment::getLength).sum());

        stats.setLengthByType(lengthByType);

        stats.setLengthByDistrict(lengthByDistrict);

        stats.setRoadCount(allRoads.size());

        

        return stats;

    }

}


// 统计信息实体类

public class RoadStatistics {

    private double totalLength;

    private Map<String, Double> lengthByType;

    private Map<String, Double> lengthByDistrict;

    private int roadCount;

    

    // getter 和 setter 方法

    public double getTotalLength() { return totalLength; }

    public void setTotalLength(double totalLength) { this.totalLength = totalLength; }

    

    public Map<String, Double> getLengthByType() { return lengthByType; }

    public void setLengthByType(Map<String, Double> lengthByType) { 

        this.lengthByType = lengthByType; 

    }

    

    public Map<String, Double> getLengthByDistrict() { return lengthByDistrict; }

    public void setLengthByDistrict(Map<String, Double> lengthByDistrict) { 

        this.lengthByDistrict <"5D.2597.HK">= lengthByDistrict; 

    }

    

    public int getRoadCount() { return roadCount; }

    public void setRoadCount(int roadCount) { this.roadCount = roadCount; }

}

```


## 前端Leaflet地图实现


### 基础地图配置


```html

<!DOCTYPE html>

<html>

<head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>湖南省道路长度WebGIS系统</title>

    

    <!-- Leaflet CSS -->

    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />

    

    <style>

        #map {

            height: 600px;

            width: 100%;

        }

        

        .info-panel {

            position: absolute;

            top: 10px;

            right: 10px;

            z-index: 1000;

            background: white;

            padding: 15px;

            border-radius: 5px;

            box-shadow: 0 2px 8px rgba(0,0,0,0.2);

            max-width: 300px;

        }

        

        .legend {

            line-height: 18px;

            color: #555;

        }

        

        .legend i {

            width: 18px;

            height: 18px;

            float: left;

            margin-right: 8px;

            opacity: 0.7;

        }

        

        .road-stat {

            margin: 5px 0;

            padding: 5px;

            background: #f8f9fa;

            border-radius: 3px;

        }

    </style>

</head>

<body>

    <div>

        <h3>湖南省道路信息系统</h3>

        <div id="roadStatistics">

            <div>加载中...</div>

        </div>

        <div>

            <h4>图例</h4>

            <div><i style="background: #ff0000"></i>高速公路</div>

            <div><i style="background: #00ff00"></i>省级道路</div>

            <div><i style="background: #0000ff"></i>县级道路</div>

        </div>

    </div>

    

    <div id="map"></div>

    

    <!-- Leaflet JS -->

    <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>

    <script src="app.js"></script>

</body>

</html>

```


### JavaScript交互逻辑


```javascript

// app.js - Leaflet地图主逻辑

class HunanRoadMap {

    constructor() {

        this.map = null;

        this.roadLayers = {};

        this.initMap();

        this.loadRoadData();

        this.setupEventListeners();

    }

    

    // 初始化地图

    initMap() {

        // 湖南省中心坐标

        const hunanCenter = [27.6253, 111.4526];

        

        this.map = L.map('map').setView(hunanCenter, 8);

        

        // 添加底图

        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {

            attribution: '© OpenStreetMap contributors',

            maxZoom: 18

        }).addTo(this.map);

        

        // 添加比例尺

        L.control.scale().addTo(this.map);

    }

    

    // 加载道路数据

    async loadRoadData() {

        try {

            const response = await fetch('http://localhost:8080/api/roads/all');

            const roads = await response.json();

            this.renderRoads(roads);

            this.updateStatistics(roads);

        } catch (error) {

            console.error('加载道路数据失败:', error);

        }

    }

    

    // 渲染道路到地图

    renderRoads(roads) {

        // 清除现有道路图层

        Object.values(this.roadLayers).forEach(layer => {

            this.map.removeLayer(layer);

        });

        this.roadLayers = {};

        

        // 按道路类型设置不同颜色

        const roadColors = {

            '高速公路': '#ff0000',

            '省级道路': '#00ff00',

            '县级道路': '#0000ff',

            '乡村道路': '#ffff00'

        };

        

        roads.forEach(road =><"6M.2597.HK"> {

            const coordinates = road.coordinates.map(coord => [coord.latitude, coord.longitude]);

            

            // 创建折线

            const polyline = L.polyline(coordinates, {

                color: roadColors[road.roadType] || '#888888',

                weight: this.getRoadWeight(road.roadType),

                opacity: 0.8

            }).addTo(this.map);

            

            // 添加弹出信息

            polyline.bindPopup(`

                <div>

                    <h4>${road.roadName}</h4>

                    <p><strong>类型:</strong> ${road.roadType}</p>

                    <p><strong>长度:</strong> ${road.length} 公里</p>

                    <p><strong>区域:</strong> ${road.district}</p>

                </div>

            `);

            

            this.roadLayers[road.roadId] = polyline;

        });

    }

    

    // 根据道路类型获取线宽

    getRoadWeight(roadType) {

        const weights = {

            '高速公路': 6,

            '省级道路': 4,

            '县级道路': 3,

            '乡村道路': 2

        };

        return weights[roadType] || 2;

    }

    

    // 更新统计信息显示

    updateStatistics(roads) {

        const totalLength = roads.reduce((sum, road) => sum + road.length, 0);

        const roadCount = roads.length;

        

        // 按类型统计

        const typeStats = {};

        roads.forEach(road => {

            typeStats[road.roadType] = (typeStats[road.roadType] || 0) + road.length;

        });

        

        let statsHtml = `

            <div>总道路数: ${roadCount} 条</div>

            <div>总长度: ${totalLength.toFixed(2)} 公里</div>

        `;

        

        for (const [type, length] of Object.entries(typeStats)) {

            statsHtml += `<div>${type}: ${length.toFixed(2)} 公里</div>`;

        }

        

        document.getElementById('roadStatistics').innerHTML = statsHtml;

    }

    

    // 设置事件监听

    setupEventListeners() {

        // 地图点击事件

        this.map.on('click', (e) => {

            console.log('地图点击坐标:', e.latlng);

        });

        

        // 键盘快捷键

        document.addEventListener('keydown', (e) => {

            if (e.key === 'r') {

                this.map.setView([27.6253, 111.4526], 8); // 重置视图

            }

        });

    }

    

    // 筛选显示特定类型的道路

    filterRoadsByType(roadType) {

        Object.entries(this.roadLayers).forEach(([id, layer]) => {

            // 这里需要根据道路类型显示/隐藏图层

            // 实际实现需要存储道路类型信息

        });

    }

}


// 页面加载完成后初始化地图

document.addEventListener('DOMContentLoaded', () => {

    new HunanRoadMap();

});

```


## 高级功能实现


### 空间分析与测量工具


```javascript

// 测量工具类

class MeasurementTools {

    constructor(map)<"UF.4961.HK"> {

        this.map = map;

        this.measureControl = null;

        this.isMeasuring = false;

        this.currentPolyline = null;

        this.points = [];

        this.initMeasureControl();

    }

    

    initMeasureControl() {

        // 创建测量控制按钮

        this.measureControl = L.Control.extend({

            options: {

                position: 'topleft'

            },

            

            onAdd: function(map) {

                const container = L.DomUtil.create('div', 'leaflet-bar leaflet-control');

                const button = L.DomUtil.create('a', 'leaflet-control-measure', container);

                button.innerHTML = '📏';

                button.title = '距离测量';

                button.href = '#';

                

                L.DomEvent.on(button, 'click', L.DomEvent.stop)

                    .on(button, 'click', () => {

                        this.toggleMeasurement();

                    }.bind(this));

                

                return container;

            }

        });

        

        new this.measureControl().addTo(this.map);

    }

    

    toggleMeasurement() {

        this.isMeasuring = !this.isMeasuring;

        

        if (this.isMeasuring) {

            this.startMeasurement();

        } else {

            this.stopMeasurement();

        }

    }

    

    startMeasurement() {

        this.map.on('click', this.onMapClick.bind(this));

        this.map.getContainer().style.cursor = 'crosshair';

    }

    

    stopMeasurement() {

        this.map.off('click', this.onMapClick.bind(this));

        this.map.getContainer().style.cursor = '';

        

        if (this.currentPolyline) {

            this.map.removeLayer(this.currentPolyline);

            this.currentPolyline = null;

        }

        

        this.points = [];

    }

    

    onMapClick(e) {

        this.points.push(e.latlng);

        

        if (this.currentPolyline) {

            this.map.removeLayer(this.currentPolyline);

        }

        

        this.currentPolyline = L.polyline(this.points, {

            color: 'blue',

            weight: 3,

            dashArray: '5, 10'

        }).addTo(this.map);

        

        // 显示距离信息

        if (this.points.length > 1) {

            const distance = this.calculateTotalDistance();

            this.showDistancePopup(e.latlng, distance);

        }

    }

    

    calculateTotalDistance() {

        let totalDistance = 0;

        for (let i = 1; i < this.points.length; i++) {

            totalDistance += this.map.distance(this.points[i-1], this.points[i]);

        }

        return totalDistance / 1000; // 转换为公里

    }

    

    showDistancePopup(latlng, distance) {

        L.popup()

            .setLatLng(latlng)

            .setContent(`测量距离: ${distance.toFixed(2)} 公里`)

            .openOn(this.map);

    }

}

```


### 数据导出功能


```java

// 数据导出服务

@RestController

@RequestMapping("/api/export")

public class ExportController<"GD.4961.HK"> {

    

    @Autowired

    private RoadDataService roadDataService;

    

    /**

     * 导出道路数据为GeoJSON格式

     */

    @GetMapping("/geojson")

    public ResponseEntity<String> exportAsGeoJSON() {

        try {

            List<RoadSegment> roads = roadDataService.getAllRoads();

            String geoJSON = convertToGeoJSON(roads);

            

            return ResponseEntity.ok()

                    .header("Content-Type", "application/geo+json")

                    .header("Content-Disposition", "attachment; filename=hunan_roads.geojson")

                    .body(geoJSON);

        } catch (Exception e) {

            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();

        }

    }

    

    /**

     * 将道路数据转换为GeoJSON格式

     */

    private String convertToGeoJSON(List<RoadSegment> roads) {

        StringBuilder geoJSON = new StringBuilder();

        geoJSON.append("{\"type\": \"FeatureCollection\", \"features\": [");

        

        for (int i = 0; i < roads.size(); i++) {

            RoadSegment road = roads.get(i);

            if (i > 0) {

                geoJSON.append(",");

            }

            

            geoJSON.append("{")

                   .append("\"type\": \"Feature\",")

                   .append("\"geometry\": {")

                   .append("\"type\": \"LineString\",")

                   .append("\"coordinates\": [");

            

            // 添加坐标点

            List<Coordinate> coords = road.getCoordinates();

            for (int j = 0; j < coords.size(); j++) {

                Coordinate coord = coords.get(j);

                if (j > 0) {

                    geoJSON.append(",");

                }

                geoJSON.append("[")

                       .append(coord.getLongitude())

                       .append(",")

                       .append(coord.getLatitude())

                       .append("]");

            }

            

            geoJSON.append("]},")

                   .append("\"properties\": {")

                   .append("\"id\": \"").append(road.getRoadId()).append("\",")

                   .append("\"name\": \"").append(road.getRoadName()).append("\",")

                   .append("\"type\": \"").append(road.getRoadType()).append("\",")

                   .append("\"length\": ").append(road.getLength()).append(",")

                   .append("\"district\": \"").append(road.getDistrict()).append("\"")

                   .append("}")

                   .append("}");

        }

        

        geoJSON.append("]}");

        return geoJSON.toString();

    }

    

    /**

     * 导出统计报告为CSV格式

     */

    @GetMapping("/statistics/csv")

    public ResponseEntity<String> exportStatisticsCSV() {

        try {

            RoadStatistics stats = roadDataService.getRoadStatistics();

            String csv = generateStatisticsCSV(stats);

            

            return ResponseEntity.ok()

                    .header("Content-Type", "text/csv")

                    .header("Content-Disposition", "attachment; filename=road_statistics.csv")

                    .body(csv);

        } catch (Exception e) {

            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();

        }

    }

    

    private String generateStatisticsCSV(RoadStatistics stats) {

        StringBuilder csv = new StringBuilder();

        csv.append("统计类型,数值\n");

        csv.append("总道路数,").append(stats.getRoadCount()).append("\n");

        csv.append("总长度,").append(stats.getTotalLength()).append("\n");

        

        csv.append("\n按道路类型统计:\n");

        csv.append("道路类型,长度(公里)\n");

        stats.getLengthByType().forEach((type, length) -> {

            csv.append(type).append(",").append(length).append("\n");

        });

        

        csv.append("\n按行政区统计:\n");

        csv.append("行政区,长度(公里)\n");

        stats.getLengthByDistrict().forEach((district, length) -> {

            csv.append(district).append<"TU.4961.HK">(",").append(length).append("\n");

        });

        

        return csv.toString();

    }

}

```


## 系统部署与优化


### 配置文件


```properties

# application.properties

server.port=8080

spring.web.resources.static-locations=classpath:/static/


# CORS配置

spring.mvc.cors.allowed-origins=*

spring.mvc.cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS

spring.mvc.cors.allowed-headers=*


# 缓存配置

spring.cache.type=simple

spring.cache.cache-names=roadData,statistics

```


### 性能优化配置


```java

// 缓存配置类

@Configuration

@EnableCaching

public class CacheConfig {

    

    @Bean

    public CacheManager cacheManager() {

        ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();

        cacheManager.setCacheNames(Arrays.asList("roadData", "statistics"));

        return cacheManager;

    }

}


// 缓存服务

@Service

public class CachedRoadDataService {

    

    @Autowired

    private RoadDataService roadDataService;

    

    @Cacheable(value = "roadData", key = "'all'")

    public List<RoadSegment> getAllRoadsCached() {

        return roadDataService.getAllRoads();

    }

    

    @Cacheable(value = "statistics", key = "'roadStats'")

    public RoadStatistics getRoadStatisticsCached() {

        return roadDataService.getRoadStatistics();

    }

    

    @CacheEvict(value = {"roadData", "statistics"}, allEntries = true)

    public void clearCache() {

        // 缓存清除方法

    }

}

```


## 总结


通过Java后端与Leaflet前端的有机结合,我们成功构建了一个功能完整的湖南省道路长度WebGIS系统。该系统具备以下特点:


**技术架构合理**:采用前后端分离架构,Java提供稳定的数据服务,Leaflet负责高效的地图渲染

**功能完善**:包含道路展示、长度统计、空间测量、数据导出等核心GIS功能

**用户体验良好**:响应式设计,支持多种交互操作,信息展示直观清晰

**扩展性强**:模块化设计便于后续功能扩展和数据更新


该系统不仅为湖南省道路管理提供了可视化工具,也为类似的地理信息系统开发提供了可复用的技术方案。通过进一步的优化和功能扩展,可以应用于更复杂的地理空间分析和决策支持场景。


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/19545/viewspace-3103219/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2015-11-01

  • 博文量
    895
  • 访问量
    933686