Skill: jvy/openlayers (version 1.0.0)

--- name: openlayers description: Build, debug, and integrate OpenLayers web maps, including map/view setup, XYZ and OSM base layers, vector sources, GeoJSON overlays, Web Mercator and WGS84 handling, feature styling, click/pointer interaction, layer ordering, fit-to-extent flows, and frontend integration for vanilla HTML, Vite, React, and similar apps. Use when the user asks for OpenLayers code, `ol` usage, OpenLayers debugging, web GIS map implementation, or migration guidance from Leaflet/Mapbox. 中文触发：OpenLayers、ol 地图、前端 GIS、GeoJSON 地图、矢量图层、Web Mercator、地图交互、OpenLayers 调试。 metadata: openclaw: emoji: "🧭" homepage: "https://openlayers.org/" requires: bins: ["python3"] install: - id: "brew" kind: "brew" formula: "python" bins: ["python3"] label: "Install Python 3 (brew)" --- # OpenLayers Use this skill for practical OpenLayers web map work in browser apps. OpenLayers is the right choice when the user needs a stronger GIS-style 2D web map stack than Leaflet, especially for projection-aware workflows, tiled and vector layers, custom styling, feature interaction, and more explicit control over map/view/source/layer composition. ## Workflow 1. Confirm the runtime first: - plain HTML - Vite - React - Next.js or another SPA framework 2. Confirm the actual map job: - base map only - markers or selected features - GeoJSON overlay - WMS or WMTS layer - draw or modify interaction - bug fixing or migration 3. Start from the smallest visible map before adding custom sources, interactions, or large datasets. 4. Check container size, CSS, and projection assumptions before debugging layer logic. 5. Keep coordinate transforms explicit; OpenLayers often needs `fromLonLat(...)` or explicit `dataProjection` and `featureProjection`. ## Implementation Guardrails - Ensure the map container has a real height before initializing `Map`. - Keep `View` projection, incoming data projection, and display projection explicit. - Use `fromLonLat([lng, lat])` for geographic coordinates shown on the default Web Mercator map. - For GeoJSON, set both `dataProjection` and `featureProjection` when the source CRS is known. - Prefer stable layer and source instances instead of recreating the whole map on every state update. - Clean up the map target in SPA teardown paths. - Fit bounds from actual features or source extents when the dataset is dynamic. - Style features deterministically; do not bury business logic inside a style function unless the user needs data-driven styling. ## Common Failure Checks - Blank map: verify container height, CSS import, map target, and layer source URL. - Features in the wrong place: verify whether lon/lat was used without `fromLonLat(...)`, or whether GeoJSON projections were omitted. - Click picking not working: verify layer visibility, z-index ordering, and `forEachFeatureAtPixel(...)` usage. - Map looks offset after layout changes: call `map.updateSize()` after the container becomes visible or resizes. - Performance issues with many features: simplify data, cluster points, reduce style churn, or move expensive logic out of pointer handlers. - WMS or WMTS not rendering: verify service URL, layer names, matrix set, CRS, and cross-origin behavior. ## Practical Patterns ### Minimal map - Create one `Map`, one `View`, and one base `TileLayer`. - Use `OSM` or `XYZ` only after confirming the tile URL and attribution requirements. - Set center using `fromLonLat(...)` when starting from geographic coordinates. ### GeoJSON overlay - Use `VectorSource` with a `GeoJSON` format object. - Set `dataProjection` and `featureProjection` explicitly when precision matters. - Fit the view to `vectorSource.getExtent()` after features load. ### Interactive picking - Use `map.on("singleclick", ...)` for primary feature selection. - Use `forEachFeatureAtPixel(...)` to identify the top visible feature. - Keep popup or side-panel rendering outside the OpenLayers style layer when possible. ### SPA integration - Create the map once after the container mounts. - Reuse `Source` and `Layer` objects across updates. - On teardown, call `map.setTarget(undefined)` to release DOM bindings. ### Draw and edit - Add `Draw`, `Modify`, or `Select` only when the user explicitly needs editing. - Confirm output format and projection before persisting edited geometries. ## Task Boundaries - Use this skill for OpenLayers-specific implementation, debugging, and migration patterns. - For CRS and reprojection reasoning, use `project`, `wgs84`, or `cgcs2000` as appropriate. - For file-based GIS conversion or batch reprojection, use `qgis`. - For lightweight non-GIS-heavy web maps, `leaflet` may be simpler. - For Mapbox GL style expressions or hosted Mapbox platform APIs, use `mapbox`. - For globe and 3D scene rendering, use `cesium`. ## What To Return - A minimal working OpenLayers setup or a targeted fix. - The likely root cause when debugging projection, sizing, or layer visibility issues. - Clear notes on coordinate order, projection assumptions, and view fitting. - Framework-specific lifecycle handling when relevant. - A smaller reproducible example first if the original integration is noisy. ## Practical Commands ### Generate a minimal OpenLayers page ```bash python3 {baseDir}/scripts/openlayers_scaffold.py minimal --out ./openlayers-demo.html ``` ### Generate a GeoJSON-ready OpenLayers page ```bash python3 {baseDir}/scripts/openlayers_scaffold.py geojson --out ./openlayers-geojson.html --geojson ./data/sample.geojson ``` ## OpenClaw + ClawHub Notes - Keep examples generic, portable, and safe to paste into local projects. - Do not hardcode private tile services, private tokens, or machine-specific paths. - Prefer standards-based examples and explicit projection handling over hidden magic. - For clawhub.ai publication, keep examples reproducible and version/changelog updates semver-driven. ## Reference Docs In This Skill - Read `{baseDir}/references/patterns.md` when generating or fixing OpenLayers code. - Read `{baseDir}/references/layer-recipes.md` when the task involves XYZ, OSM, GeoJSON, WMS, WMTS, or feature interaction patterns.

path: scripts/openlayers_scaffold.py
content: #!/usr/bin/env python3
"""Generate minimal OpenLayers HTML scaffolds for local debugging."""

from __future__ import annotations

import argparse
import html
import json
from pathlib import Path


MINIMAL_TEMPLATE = """<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>OpenLayers Minimal Map</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@latest/ol.css" />
    <style>
      html, body {{
        margin: 0;
        height: 100%;
      }}
      #map {{
        width: 100%;
        height: 100%;
      }}
      .panel {{
        position: absolute;
        top: 12px;
        left: 12px;
        z-index: 10;
        padding: 10px 12px;
        background: rgba(255, 255, 255, 0.92);
        border-radius: 8px;
        font: 13px/1.4 sans-serif;
        box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
      }}
    </style>
  </head>
  <body>
    <div class="panel">OpenLayers minimal map</div>
    <div id="map"></div>
    <script type="module">
      import Map from "https://cdn.jsdelivr.net/npm/ol@latest/Map.js";
      import View from "https://cdn.jsdelivr.net/npm/ol@latest/View.js";
      import TileLayer from "https://cdn.jsdelivr.net/npm/ol@latest/layer/Tile.js";
      import OSM from "https://cdn.jsdelivr.net/npm/ol@latest/source/OSM.js";
      import {{ fromLonLat }} from "https://cdn.jsdelivr.net/npm/ol@latest/proj.js";

      const map = new Map({{
        target: "map",
        layers: [
          new TileLayer({{
            source: new OSM(),
          }}),
        ],
        view: new View({{
          center: fromLonLat([{lng}, {lat}]),
          zoom: {zoom},
        }}),
      }});

      window.map = map;
    </script>
  </body>
</html>
"""


GEOJSON_TEMPLATE = """<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>OpenLayers GeoJSON Map</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@latest/ol.css" />
    <style>
      html, body {{
        margin: 0;
        height: 100%;
        font-family: sans-serif;
      }}
      #map {{
        width: 100%;
        height: 100%;
      }}
      .panel {{
        position: absolute;
        top: 12px;
        left: 12px;
        z-index: 10;
        max-width: 320px;
        padding: 10px 12px;
        background: rgba(255, 255, 255, 0.94);
        border-radius: 8px;
        box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
        font-size: 13px;
      }}
      .code {{
        margin-top: 6px;
        color: #444;
        word-break: break-all;
      }}
    </style>
  </head>
  <body>
    <div class="panel">
      <div>OpenLayers GeoJSON demo</div>
      <div class="code">GeoJSON: {geojson_label}</div>
    </div>
    <div id="map"></div>
    <script type="module">
      import Map from "https://cdn.jsdelivr.net/npm/ol@latest/Map.js";
      import View from "https://cdn.jsdelivr.net/npm/ol@latest/View.js";
      import TileLayer from "https://cdn.jsdelivr.net/npm/ol@latest/layer/Tile.js";
      import VectorLayer from "https://cdn.jsdelivr.net/npm/ol@latest/layer/Vector.js";
      import OSM from "https://cdn.jsdelivr.net/npm/ol@latest/source/OSM.js";
      import VectorSource from "https://cdn.jsdelivr.net/npm/ol@latest/source/Vector.js";
      import GeoJSON from "https://cdn.jsdelivr.net/npm/ol@latest/format/GeoJSON.js";
      import Fill from "https://cdn.jsdelivr.net/npm/ol@latest/style/Fill.js";
      import Stroke from "https://cdn.jsdelivr.net/npm/ol@latest/style/Stroke.js";
      import Style from "https://cdn.jsdelivr.net/npm/ol@latest/style/Style.js";
      import CircleStyle from "https://cdn.jsdelivr.net/npm/ol@latest/style/Circle.js";
      import {{ fromLonLat }} from "https://cdn.jsdelivr.net/npm/ol@latest/proj.js";

      const geojsonObject = {geojson_payload};
      const vectorSource = new VectorSource({{
        features: new GeoJSON().readFeatures(geojsonObject, {{
          dataProjection: "EPSG:4326",
          featureProjection: "EPSG:3857",
        }}),
      }});

      const vectorLayer = new VectorLayer({{
        source: vectorSource,
        style: new Style({{
          image: new CircleStyle({{
            radius: 6,
            fill: new Fill({{ color: "#d9480f" }}),
            stroke: new Stroke({{ color: "#ffffff", width: 2 }}),
          }}),
          stroke: new Stroke({{
            color: "#0b7285",
            width: 3,
          }}),
          fill: new Fill({{
            color: "rgba(11, 114, 133, 0.18)",
          }}),
        }}),
      }});

      const map = new Map({{
        target: "map",
        layers: [
          new TileLayer({{ source: new OSM() }}),
          vectorLayer,
        ],
        view: new View({{
          center: fromLonLat([{lng}, {lat}]),
          zoom: {zoom},
        }}),
      }});

      const extent = vectorSource.getExtent();
      if (extent.every(Number.isFinite)) {{
        map.getView().fit(extent, {{ padding: [40, 40, 40, 40], maxZoom: 15 }});
      }}

      map.on("singleclick", (event) => {{
        const feature = map.forEachFeatureAtPixel(event.pixel, (candidate) => candidate);
        if (!feature) return;
        console.log("Picked feature properties:", feature.getProperties());
      }});

      window.map = map;
    </script>
  </body>
</html>
"""


def write_text(path_text: str, content: str) -> None:
    path = Path(path_text)
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content, encoding="utf-8")


def cmd_minimal(args: argparse.Namespace) -> int:
    html_text = MINIMAL_TEMPLATE.format(lng=args.lng, lat=args.lat, zoom=args.zoom)
    write_text(args.out, html_text)
    print(json.dumps({"written": args.out, "template": "minimal"}, ensure_ascii=True, indent=2))
    return 0


def cmd_geojson(args: argparse.Namespace) -> int:
    geojson_path = Path(args.geojson)
    geojson_obj = json.loads(geojson_path.read_text(encoding="utf-8"))
    html_text = GEOJSON_TEMPLATE.format(
        geojson_label=html.escape(args.geojson),
        geojson_payload=json.dumps(geojson_obj, ensure_ascii=True, indent=2),
        lng=args.lng,
        lat=args.lat,
        zoom=args.zoom,
    )
    write_text(args.out, html_text)
    print(
        json.dumps(
            {"written": args.out, "template": "geojson", "geojson": args.geojson},
            ensure_ascii=True,
            indent=2,
        )
    )
    return 0


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="OpenLayers HTML scaffold generator.")
    sub = parser.add_subparsers(dest="command", required=True)

    minimal = sub.add_parser("minimal", help="Write a minimal OpenLayers page.")
    minimal.add_argument("--out", required=True)
    minimal.add_argument("--lng", type=float, default=116.397)
    minimal.add_argument("--lat", type=float, default=39.908)
    minimal.add_argument("--zoom", type=int, default=11)
    minimal.set_defaults(func=cmd_minimal)

    geojson = sub.add_parser("geojson", help="Write an OpenLayers page with inline GeoJSON.")
    geojson.add_argument("--out", required=True)
    geojson.add_argument("--geojson", required=True)
    geojson.add_argument("--lng", type=float, default=116.397)
    geojson.add_argument("--lat", type=float, default=39.908)
    geojson.add_argument("--zoom", type=int, default=11)
    geojson.set_defaults(func=cmd_geojson)

    return parser


def main() -> int:
    parser = build_parser()
    args = parser.parse_args()
    return args.func(args)


if __name__ == "__main__":
    raise SystemExit(main())


path: references/layer-recipes.md
content: # Layer Recipes

Use this reference for concrete layer wiring patterns.

## OSM Base Layer

- Use `TileLayer` with `OSM` for the quickest public demo.
- Keep attribution visible.

## XYZ Tiles

- Use `TileLayer` with `XYZ`.
- Verify URL template, attribution, max zoom, retina behavior, and cross-origin needs.

## GeoJSON Overlay

- Use `VectorSource` plus `GeoJSON`.
- Explicitly set `dataProjection` and `featureProjection` when the source CRS is known.
- Fit to `vectorSource.getExtent()` after load.

## WMS

- Use `TileWMS` or `ImageWMS` depending on the use case.
- Confirm service URL, params, layer names, styles, transparent mode, and server CRS support.

## WMTS

- Build or derive the correct tile grid and matrix set.
- Matrix set mismatch is a common failure source.

## Interaction Patterns

- Picking: `map.forEachFeatureAtPixel(...)`
- Hover feedback: `pointermove` with throttled highlight updates
- Editing: `Draw`, `Modify`, `Snap`, and `Select` only when the output workflow is clear



path: references/patterns.md
content: # OpenLayers Patterns

Use this reference when the task needs more detail than the main skill body.

## Runtime Checklist

- Confirm whether the app is plain HTML, Vite, React, or another SPA.
- Confirm the installed OpenLayers package or CDN strategy.
- Confirm whether the displayed map view is default Web Mercator (`EPSG:3857`) or another projection.

## Projection Rules

- OpenLayers view coordinates are often in the view projection, not raw lon/lat.
- For display on the default web map, transform geographic coordinates with `fromLonLat([lng, lat])`.
- For GeoJSON reads, pass `dataProjection` for the source data and `featureProjection` for the map view when they differ.
- Do not assume every incoming dataset is WGS84 without checking context.

## Debugging Sequence

1. Verify the container has size.
2. Verify the map target exists before map initialization.
3. Verify the base layer loads.
4. Verify the view center and zoom are sane.
5. Verify overlay feature extent and projection handling.
6. Verify interactions and event handlers only after the map is visibly correct.

## Migration Notes

- From Leaflet to OpenLayers:
  - expect more explicit projection handling
  - prefer `Map`, `View`, `Layer`, `Source` composition over plugin-heavy shortcuts
  - replace implicit `lat,lng` habits with explicit `lng,lat` plus transform utilities
- From Mapbox GL to OpenLayers:
  - think in OpenLayers layers and sources rather than style-spec layers
  - hosted vector-tile styling workflows may need a different rendering setup
  - feature interaction is often implemented directly against map events and vector layers



path: agents/openai.yaml
content: interface:
  display_name: "OpenLayers Maps"
  short_description: "Build and debug projection-aware web maps with OpenLayers."
  default_prompt: "Use $openlayers to build, debug, or explain an OpenLayers map, including layers, sources, GeoJSON overlays, projection handling, feature picking, and frontend integration."

policy:
  allow_implicit_invocation: true



Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)