Custom Crs
Runnable Leaflet example: Custom Crs. Open the live demo, read the complete source, and copy the file into your project. No build step or API key.
single HTML file · CDN dependencies onlyinterface may contain Chinese labels
Source
<!--
示例:自定义投影
要点:L.CRS.Simple 派生平面坐标系、GridLayer 生成对应瓦片、平面坐标与经纬度两套读数的区别
依赖:Leaflet 1.9.4 · 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/leaflet@1.9.4/dist/leaflet.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:8px;align-items:center;flex-wrap:wrap;font-size:12px;color:#58606e}
.bar button{font:inherit;font-size:12px;padding:4px 10px;border-radius:4px;cursor:pointer;
border:1px solid #ccd1d9;background:#fff;color:#16181d}
.bar button:hover{background:#eef0f3}
.bar b{color:#16181d;font-weight:600}
#stage{flex:1 1 auto;min-height:0;position:relative}
#map{position:absolute;inset:0}
#ref{position:absolute;right:12px;bottom:12px;width:190px;height:132px;
border:2px solid #fff;box-shadow:0 6px 18px rgba(0,0,0,.25);z-index:500}
#ref-label{position:absolute;right:12px;bottom:150px;z-index:500;font-size:11px;
color:#58606e;background:rgba(255,255,255,.9);padding:2px 6px;border-radius:3px}
.site{width:18px;height:18px;border-radius:4px;background:#1f5fd0;
box-shadow:0 0 0 3px rgba(31,95,208,.22);transform:rotate(45deg)}
</style>
</head>
<body>
<div class="bar">
<button id="fit">适配全图</button>
<button id="toggle">隐藏网格</button>
<span id="out">平面坐标 —</span>
<b>CRS: X-PLANE(1 单位 = 1 米)</b>
</div>
<div id="stage">
<div id="map"></div>
<div id="ref"></div>
<div id="ref-label">对照:Web 墨卡托 + OSM</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
(function () {
'use strict';
/* 自定义 CRS:沿用 Simple 的经纬度投影与 y 轴翻转,改代号与缩放基数 */
var PlaneCRS = L.extend({}, L.CRS.Simple, {
code: 'X-PLANE',
/* 0 级时 1 像素 = 1 米,每升一级放大两倍 */
scale: function (zoom) { return Math.pow(2, zoom); },
zoom: function (scale) { return Math.log(scale) / Math.LN2; },
distance: function (a, b) {
var dx = a.lng - b.lng;
var dy = a.lat - b.lat;
return Math.sqrt(dx * dx + dy * dy);
}
});
var AREA = [[0, 0], [1200, 2000]]; /* [y, x] 米 */
var map = L.map('map', {
crs: PlaneCRS,
minZoom: -1,
maxZoom: 4,
zoomSnap: 1,
zoomControl: true,
attributionControl: false,
maxBounds: [[-200, -200], [1400, 2200]],
maxBoundsViscosity: 0.8
});
map.fitBounds(AREA);
/* 平面坐标下的瓦片只能自己生成,OSM 的 3857 瓦片在这里没有对应编号 */
var GridTiles = L.GridLayer.extend({
createTile: function (coords) {
var tile = L.DomUtil.create('canvas', 'leaflet-tile');
var size = this.getTileSize();
tile.width = size.x;
tile.height = size.y;
var ctx = tile.getContext('2d');
ctx.fillStyle = '#fbfcfd';
ctx.fillRect(0, 0, size.x, size.y);
ctx.strokeStyle = '#e5e9ee';
ctx.lineWidth = 1;
for (var i = 0; i <= size.x; i += 64) {
ctx.beginPath();
ctx.moveTo(i + 0.5, 0);
ctx.lineTo(i + 0.5, size.y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, i + 0.5);
ctx.lineTo(size.x, i + 0.5);
ctx.stroke();
}
var start = this._map.unproject(coords.scaleBy(size), coords.z);
ctx.fillStyle = '#9aa3b0';
ctx.font = '11px sans-serif';
ctx.fillText('x ' + Math.round(start.lng) + ' · y ' + Math.round(start.lat),
8, size.y - 8);
return tile;
}
});
var grid = new GridTiles({ pane: 'tilePane', minZoom: -2, maxZoom: 5 }).addTo(map);
L.rectangle([[220, 300], [620, 1080]], {
color: '#17694a',
weight: 2,
fillColor: '#22c55e',
fillOpacity: 0.15,
renderer: L.svg()
}).bindTooltip('地块 A · 400 × 780 米', { permanent: true, direction: 'center' }).addTo(map);
L.rectangle(AREA, {
color: '#c2410c',
weight: 1,
dashArray: '6 4',
fill: false
}).addTo(map);
L.marker([600, 1000], {
icon: L.divIcon({ className: '', html: '<div class="site"></div>', iconSize: [18, 18], iconAnchor: [9, 9] })
}).bindPopup('厂区中心<br>平面坐标 x=1000, y=600').addTo(map);
map.on('mousemove', function (e) {
document.getElementById('out').textContent =
'平面坐标 x=' + Math.round(e.latlng.lng) + ',y=' + Math.round(e.latlng.lat) +
' 像素 ' + Math.round(e.containerPoint.x) + ',' + Math.round(e.containerPoint.y);
});
map.on('mouseout', function () {
document.getElementById('out').textContent = '平面坐标 —';
});
document.getElementById('fit').addEventListener('click', function () {
map.fitBounds(AREA, { padding: [20, 20] });
});
var shown = true;
document.getElementById('toggle').addEventListener('click', function () {
shown = !shown;
if (shown) {
grid.addTo(map);
this.textContent = '隐藏网格';
} else {
map.removeLayer(grid);
this.textContent = '显示网格';
}
});
/* 右下角对照图:同一套 API,换成 OSM 的 3857 瓦片 */
var ref = L.map('ref', {
center: [39.907359, 116.391263],
zoom: 9,
zoomControl: false,
attributionControl: false,
dragging: false,
scrollWheelZoom: false,
doubleClickZoom: false,
keyboard: false
});
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', { maxZoom: 19 }).addTo(ref);
L.circleMarker([39.907359, 116.391263], { radius: 5, color: '#fff', weight: 1,
fillColor: '#c2410c', fillOpacity: 1 }).addTo(ref);
})();
</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.