Atlaslog
Atlaslog / Examples / OpenLayers / Topolis
OpenLayers · Runnable Example

Topolis

Runnable OpenLayers example: Topolis. Open the live demo, read the complete source, and copy the file into your project. No build step or API key.

Open standalone → Chinese notes All OpenLayers examples
/demos/openlayers-topolis.htmlLIVE
single HTML file · CDN dependencies onlyinterface may contain Chinese labels

Source

demos/openlayers-topolis.html
<!--
  示例:OpenLayers 拓扑编辑
  要点:节点、边、面三级拓扑结构,绘制时自动吸附端点并实时重算封闭面
  依赖:OpenLayers 10.3.1 · jsDelivr CDN
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta name="robots" content="noindex,follow">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>拓扑编辑</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@10.3.1/ol.css">
<style>
  html,body{height:100%;margin:0}
  body{display:flex;flex-direction:column;overflow:hidden;
       font:13px/1.6 -apple-system,"Segoe UI","PingFang SC",sans-serif}
  .bar{flex:0 0 auto;padding:8px 12px;background:#f6f7f9;border-bottom:1px solid #e3e6ea;
       display:flex;gap:10px;align-items:center;flex-wrap:wrap;font-size:12px;color:#58606e}
  .bar label{display:flex;gap:5px;align-items:center;cursor:pointer}
  .bar select,.bar input[type=range]{font-size:12px}
  .bar .val{font-family:ui-monospace,Consolas,monospace;color:#1f5fd0;min-width:44px}
  .bar .sep{width:1px;height:16px;background:#d8dce2}
  #map{flex:1 1 auto;min-height:0;background:#eef0f3}
</style>
</head>
<body>
<div class="bar">
  <span>在已有节点之间画线,闭合后自动生成面</span>
  <span class="sep"></span>
  <span id="info">节点 4 · 边 5 · 面 2</span>
</div>
<div id="map"></div>
<script src="https://cdn.jsdelivr.net/npm/ol@10.3.1/dist/ol.js"></script>
<script>
(function () {
  'use strict';

  if (typeof ol === 'undefined') {
    document.getElementById('map').textContent = 'OpenLayers 加载失败,请检查能否访问 cdn.jsdelivr.net';
    return;
  }


  var center = ol.proj.fromLonLat([116.391, 39.907]);
  var map = new ol.Map({
    target: 'map',
    layers: [new ol.layer.Tile({source: new ol.source.XYZ({url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', attributions: 'Tiles © Esri'}), opacity: 0.7})],
    view: new ol.View({center: center, zoom: 16})
  });

  var nodeSeq = 1;
  var edgeSeq = 1;
  var nodes = {};
  var edges = [];

  function addNode(lon, lat) {
    var id = nodeSeq++;
    nodes[id] = ol.proj.fromLonLat([lon, lat]);
    return id;
  }

  function addEdge(a, b) {
    edges.push({id: edgeSeq++, a: a, b: b});
  }

  var n1 = addNode(116.380, 39.903);
  var n2 = addNode(116.398, 39.903);
  var n3 = addNode(116.398, 39.913);
  var n4 = addNode(116.380, 39.913);
  addEdge(n1, n2);
  addEdge(n2, n3);
  addEdge(n3, n4);
  addEdge(n4, n1);
  addEdge(n1, n3);

  var nodesSource = new ol.source.Vector();
  var edgesSource = new ol.source.Vector();
  var facesSource = new ol.source.Vector();

  var nodesLayer = new ol.layer.Vector({
    source: nodesSource,
    zIndex: 3,
    style: function (feature) {
      return new ol.style.Style({
        image: new ol.style.Circle({
          radius: 7,
          fill: new ol.style.Fill({color: 'rgba(220, 40, 40, 0.25)'}),
          stroke: new ol.style.Stroke({color: '#c62828', width: 1})
        }),
        text: new ol.style.Text({
          text: String(feature.getId()),
          font: '600 11px sans-serif',
          offsetY: -16,
          fill: new ol.style.Fill({color: '#b71c1c'}),
          stroke: new ol.style.Stroke({color: '#fff', width: 3})
        })
      });
    }
  });

  var edgesLayer = new ol.layer.Vector({
    source: edgesSource,
    zIndex: 2,
    style: function (feature) {
      return new ol.style.Style({
        stroke: new ol.style.Stroke({color: '#1565c0', width: 2}),
        text: new ol.style.Text({
          text: String(feature.getId()),
          font: '600 11px sans-serif',
          fill: new ol.style.Fill({color: '#0d47a1'}),
          stroke: new ol.style.Stroke({color: '#fff', width: 2})
        })
      });
    }
  });

  var facesLayer = new ol.layer.Vector({
    source: facesSource,
    zIndex: 1,
    style: function (feature) {
      return new ol.style.Style({
        fill: new ol.style.Fill({color: 'rgba(56, 142, 60, 0.18)'}),
        stroke: new ol.style.Stroke({color: '#2e7d32', width: 1}),
        text: new ol.style.Text({
          text: '面 ' + feature.getId(),
          font: '600 12px sans-serif',
          fill: new ol.style.Fill({color: '#1b5e20'}),
          stroke: new ol.style.Stroke({color: '#fff', width: 3})
        })
      });
    }
  });

  map.addLayer(facesLayer);
  map.addLayer(edgesLayer);
  map.addLayer(nodesLayer);

  function edgeAngle(half) {
    var a = nodes[half.from];
    var b = nodes[half.to];
    return Math.atan2(b[1] - a[1], b[0] - a[0]);
  }

  function signedArea(ring) {
    var sum = 0;
    for (var i = 0; i < ring.length; ++i) {
      var p = nodes[ring[i]];
      var q = nodes[ring[(i + 1) % ring.length]];
      sum += p[0] * q[1] - q[0] * p[1];
    }
    return sum / 2;
  }

  function findFaces() {
    var half = [];
    var outgoing = {};
    edges.forEach(function (edge, index) {
      half.push({from: edge.a, to: edge.b, id: index * 2, twin: index * 2 + 1});
      half.push({from: edge.b, to: edge.a, id: index * 2 + 1, twin: index * 2});
    });
    half.forEach(function (h) {
      (outgoing[h.from] || (outgoing[h.from] = [])).push(h);
    });
    Object.keys(outgoing).forEach(function (key) {
      outgoing[key].sort(function (p, q) {
        return edgeAngle(p) - edgeAngle(q);
      });
    });

    var visited = {};
    var rings = [];
    half.forEach(function (start) {
      if (visited[start.id]) {
        return;
      }
      var ring = [];
      var current = start;
      var guard = 0;
      do {
        visited[current.id] = true;
        ring.push(current.from);
        var candidates = outgoing[current.to];
        var twinIndex = candidates.indexOf(
          candidates.filter(function (c) { return c.id === current.twin; })[0]
        );
        current = candidates[(twinIndex + candidates.length - 1) % candidates.length];
        ++guard;
      } while (current.id !== start.id && guard < 400);
      if (ring.length >= 3 && signedArea(ring) > 0) {
        rings.push(ring);
      }
    });
    return rings;
  }

  var faceSeq = 1;

  function refresh() {
    nodesSource.clear();
    edgesSource.clear();
    facesSource.clear();

    Object.keys(nodes).forEach(function (id) {
      var feature = new ol.Feature(new ol.geom.Point(nodes[id]));
      feature.setId(Number(id));
      nodesSource.addFeature(feature);
    });

    edges.forEach(function (edge) {
      var feature = new ol.Feature(
        new ol.geom.LineString([nodes[edge.a], nodes[edge.b]])
      );
      feature.setId(edge.id);
      edgesSource.addFeature(feature);
    });

    faceSeq = 1;
    findFaces().forEach(function (ring) {
      var ringCoords = ring.map(function (id) { return nodes[id]; });
      ringCoords.push(ringCoords[0]);
      var feature = new ol.Feature(new ol.geom.Polygon([ringCoords]));
      feature.setId(faceSeq++);
      facesSource.addFeature(feature);
    });

    document.getElementById('info').textContent =
      '节点 ' + Object.keys(nodes).length +
      ' · 边 ' + edges.length +
      ' · 面 ' + facesSource.getFeatures().length;
  }

  refresh();

  function nearestNode(coordinate) {
    var best = null;
    var bestDist = Infinity;
    var tolerance = map.getView().getResolution() * 12;
    Object.keys(nodes).forEach(function (id) {
      var dx = nodes[id][0] - coordinate[0];
      var dy = nodes[id][1] - coordinate[1];
      var dist = Math.sqrt(dx * dx + dy * dy);
      if (dist < bestDist) {
        bestDist = dist;
        best = Number(id);
      }
    });
    return bestDist <= tolerance ? best : null;
  }

  function newNode(coordinate) {
    var id = nodeSeq++;
    nodes[id] = coordinate;
    return id;
  }

  var drawSource = new ol.source.Vector();
  var drawLayer = new ol.layer.Vector({
    source: drawSource,
    style: new ol.style.Style({
      stroke: new ol.style.Stroke({color: 'rgba(255, 160, 0, 0.95)', width: 2})
    })
  });
  map.addLayer(drawLayer);

  var draw = new ol.interaction.Draw({
    type: 'LineString',
    source: drawSource
  });

  draw.on('drawend', function (evt) {
    var coords = evt.feature.getGeometry().getCoordinates();
    var start = nearestNode(coords[0]);
    var end = nearestNode(coords[coords.length - 1]);
    if (start === null) {
      start = newNode(coords[0]);
    }
    if (end === null) {
      end = newNode(coords[coords.length - 1]);
    }
    var duplicate = edges.some(function (edge) {
      return (edge.a === start && edge.b === end) ||
        (edge.a === end && edge.b === start);
    });
    if (start !== end && !duplicate) {
      addEdge(start, end);
    }
    drawSource.clear();
    refresh();
  });

  map.addInteraction(draw);
  map.addInteraction(new ol.interaction.Snap({source: nodesSource}));

})();
</script>
</body>
</html>
Language: the code and this page are in English; the full tutorial and API notes for this example are in Chinese. The demo itself is the authoritative version — it runs exactly as shown.

More OpenLayers examples

OpenLayers · EXAMPLETracing OpenLayers · EXAMPLETranslate Features OpenLayers · EXAMPLETurf