Files
MapStudio/api-comparison.html
2026-11-08 13:36:18 +08:00

600 lines
26 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="地图、等高线、铁路与地形数据 API 成本对比和低预算方案建议。">
<title>地图 API 成本对比|Map Studio</title>
<style>
:root {
--ink: #17201b;
--muted: #647168;
--line: #dde4dd;
--accent: #205c46;
--accent-soft: #e6f0ea;
--warn: #8a5b18;
--warn-soft: #fff5df;
--danger: #8a3c2c;
--danger-soft: #fff0ec;
--paper: #ffffff;
--bg: #f3f6f2;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
color: var(--ink);
background:
radial-gradient(circle at 82% -20%, rgba(63, 117, 88, 0.12), transparent 32%),
var(--bg);
font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
line-height: 1.65;
}
a { color: var(--accent); }
.page { width: min(1160px, calc(100% - 34px)); margin: 0 auto; }
.hero { padding: 68px 0 42px; }
.back {
display: inline-flex;
align-items: center;
gap: 8px;
margin-bottom: 30px;
font-size: 13px;
font-weight: 700;
text-decoration: none;
}
.eyebrow {
color: var(--accent);
font-size: 11px;
font-weight: 850;
letter-spacing: .17em;
}
h1 {
max-width: 850px;
margin: 10px 0 18px;
font-size: clamp(36px, 6vw, 68px);
line-height: 1.03;
letter-spacing: -.055em;
}
.lede { max-width: 760px; margin: 0; color: var(--muted); font-size: 18px; }
.hero-meta { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 26px; }
.pill {
padding: 8px 12px;
border: 1px solid var(--line);
border-radius: 999px;
background: rgba(255,255,255,.72);
color: #536159;
font-size: 12px;
font-weight: 700;
}
.decision {
display: grid;
grid-template-columns: 1.15fr .85fr;
gap: 18px;
margin: 12px 0 34px;
}
.decision-card {
padding: 28px;
border: 1px solid var(--line);
border-radius: 23px;
background: var(--paper);
box-shadow: 0 18px 50px rgba(26, 43, 33, .08);
}
.decision-card.primary {
color: #eef8f2;
background: linear-gradient(145deg, #285c47, #173b2d);
border-color: #244d3c;
}
.decision-card h2 { margin: 7px 0 12px; font-size: 25px; line-height: 1.2; letter-spacing: -.025em; }
.decision-card p { margin: 0; color: var(--muted); }
.decision-card.primary p { color: #c2d8cb; }
.recommendation {
display: grid;
gap: 12px;
margin-top: 20px;
}
.recommendation > div {
display: grid;
grid-template-columns: 28px 1fr;
gap: 10px;
align-items: start;
}
.recommendation span {
width: 26px;
height: 26px;
display: grid;
place-items: center;
border-radius: 8px;
background: rgba(255,255,255,.13);
color: #fff;
font-size: 11px;
font-weight: 850;
}
.recommendation > div > div > strong { display: block; font-size: 13px; }
.recommendation > div > div > small { color: #bcd2c5; }
.section { margin: 52px 0; }
.section-head { display: flex; align-items: end; justify-content: space-between; gap: 22px; margin-bottom: 20px; }
.section-head h2 { margin: 5px 0 0; font-size: clamp(28px, 4vw, 42px); line-height: 1.1; letter-spacing: -.035em; }
.section-head p { max-width: 540px; margin: 0; color: var(--muted); }
.table-wrap {
overflow-x: auto;
border: 1px solid var(--line);
border-radius: 19px;
background: var(--paper);
box-shadow: 0 12px 35px rgba(26, 43, 33, .06);
}
table { width: 100%; min-width: 930px; border-collapse: collapse; }
th, td { padding: 17px 16px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }
th { color: #526158; background: #f7f9f6; font-size: 11px; letter-spacing: .06em; }
td { font-size: 13px; }
tr:last-child td { border-bottom: 0; }
td strong { display: block; margin-bottom: 5px; }
.tag {
display: inline-flex;
padding: 4px 8px;
margin: 2px 3px 2px 0;
border-radius: 7px;
color: var(--accent);
background: var(--accent-soft);
font-size: 10px;
font-weight: 800;
white-space: nowrap;
}
.tag.warn { color: var(--warn); background: var(--warn-soft); }
.tag.danger { color: var(--danger); background: var(--danger-soft); }
.best-row { background: #fbfdfb; }
.price { font-weight: 850; white-space: nowrap; }
.price small { display: block; color: var(--muted); font-weight: 500; white-space: normal; }
.calc {
display: grid;
grid-template-columns: 360px minmax(0, 1fr);
gap: 18px;
}
.card {
padding: 25px;
border: 1px solid var(--line);
border-radius: 20px;
background: var(--paper);
box-shadow: 0 12px 35px rgba(26, 43, 33, .06);
}
.card h3 { margin: 0 0 8px; font-size: 19px; }
.card > p { margin: 0 0 18px; color: var(--muted); font-size: 13px; }
label { display: block; margin: 18px 0 7px; font-size: 12px; font-weight: 800; }
input[type="number"] {
width: 100%;
padding: 12px 13px;
border: 1px solid var(--line);
border-radius: 11px;
outline: none;
}
input[type="number"]:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(32,92,70,.11); }
input[type="range"] { width: 100%; accent-color: var(--accent); }
.hint { margin-top: 7px; color: var(--muted); font-size: 11px; }
.results { display: grid; gap: 11px; }
.result {
display: grid;
grid-template-columns: minmax(150px, 1fr) 150px;
gap: 14px;
align-items: center;
padding: 16px;
border: 1px solid var(--line);
border-radius: 14px;
}
.result.recommended { border-color: #9dbbaa; background: var(--accent-soft); }
.result strong { display: block; font-size: 13px; }
.result small { display: block; margin-top: 3px; color: var(--muted); }
.result .amount { text-align: right; font-size: 20px; font-weight: 900; letter-spacing: -.02em; }
.result .amount small { display: block; font-size: 9px; font-weight: 650; letter-spacing: 0; }
.formula {
margin-top: 15px;
padding: 13px 15px;
border-radius: 11px;
color: #56645b;
background: #f5f7f4;
font-size: 11px;
}
.arch { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; }
.arch-card {
padding: 23px;
border: 1px solid var(--line);
border-radius: 18px;
background: var(--paper);
}
.arch-card .number {
width: 32px;
height: 32px;
display: grid;
place-items: center;
margin-bottom: 17px;
border-radius: 9px;
color: #fff;
background: var(--accent);
font-size: 12px;
font-weight: 900;
}
.arch-card h3 { margin: 0 0 10px; font-size: 17px; }
.arch-card p { margin: 0; color: var(--muted); font-size: 12px; }
.arch-card ul { margin: 13px 0 0; padding-left: 18px; color: #536159; font-size: 11px; }
.notice {
padding: 22px 24px;
border: 1px solid #e6d4a5;
border-radius: 17px;
background: var(--warn-soft);
}
.notice h3 { margin: 0 0 8px; font-size: 16px; }
.notice p { margin: 0; color: #72551e; font-size: 13px; }
.sources {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 11px 26px;
margin: 0;
padding: 0;
list-style: none;
}
.sources li {
padding: 12px 0;
border-bottom: 1px solid var(--line);
color: var(--muted);
font-size: 12px;
}
.sources a { font-weight: 800; text-decoration: none; }
footer {
margin-top: 72px;
padding: 30px 0 42px;
border-top: 1px solid var(--line);
color: var(--muted);
font-size: 11px;
}
@media (max-width: 850px) {
.decision, .calc, .arch { grid-template-columns: 1fr; }
.section-head { display: block; }
.section-head p { margin-top: 10px; }
}
@media (max-width: 580px) {
.hero { padding-top: 38px; }
.lede { font-size: 15px; }
.decision-card, .card { padding: 20px; }
.result { grid-template-columns: 1fr; }
.result .amount { text-align: left; }
.sources { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="page">
<header class="hero">
<a class="back" href="index.html">← 返回地图工作台</a>
<div class="eyebrow">COST & DATA COMPARISON</div>
<h1>地图 API 成本对比:<br>先用开放栈,按量再升级</h1>
<p class="lede">针对地图、等高线、铁路(国铁/地铁)、地形图和网格功能,对比开放瓦片、MapTiler、Mapbox、高德、天地图与自建方案。结论是:原型阶段采用开放栈,正式产品按访问量决策:约 8.7 万地图会话/月以上优先 MapTiler Flex;高流量或固定预算场景再转自建。</p>
<div class="hero-meta">
<span class="pill">核对日期:2026-11-03</span>
<span class="pill">币种:人民币 / 美元</span>
<span class="pill">价格通常未含税</span>
<span class="pill">开放瓦片需遵守 Fair-use</span>
</div>
</header>
<section class="decision">
<article class="decision-card primary">
<div class="eyebrow" style="color:#9dc2af">RECOMMENDATION</div>
<h2>原型:¥0 软件费<br>正式版:按月访问次数决策</h2>
<p>当前工作台已使用 OSM、AWS DEM 动态等高线/晕渲、OpenRailwayMap 与 Overpass。浏览器内生成经纬网格,不需要额外 API。</p>
<div class="recommendation">
<div><span>1</span><div><strong>≤ 50,000 地图会话/月</strong><small>开放栈仍为 ¥0;MapTiler Free 仅适合 ≤5k,Mapbox Map Loads 免费额度覆盖到 50k。</small></div></div>
<div><span>2</span><div><strong>50,000–87,000 地图会话/月</strong><small>按官网 Map Loads 阶梯,Mapbox 仍略便宜;若重视国内网络与稳定预算,可选 MapTiler Flex。</small></div></div>
<div><span>3</span><div><strong>&gt; 87,000 地图会话/月</strong><small>MapTiler Flex 约从 8.7 万会话起比 Mapbox 省;更高流量或需固定预算时,再自建矢量瓦片与 DEM 等高线。</small></div></div>
</div>
</article>
<article class="decision-card">
<div class="eyebrow">功能覆盖</div>
<h2>本原型功能均已落地</h2>
<p>地图底图、等高线、铁路基础设施、国铁/地铁矢量、地形晕渲、经纬网格,以及快速城市定位。</p>
<div style="margin-top:18px">
<span class="tag">多底图</span>
<span class="tag">等高线</span>
<span class="tag">国铁</span>
<span class="tag">地铁</span>
<span class="tag">地形晕渲</span>
<span class="tag">经纬网格</span>
</div>
</article>
</section>
<section class="section" id="compare">
<div class="section-head">
<div>
<div class="eyebrow">OPTION MATRIX</div>
<h2>方案与成本横向对比</h2>
</div>
<p>“软件费”只计算地图/数据服务费用;流量、CDN、云服务器、开发维护和境内合规成本需单独计算。</p>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>方案</th>
<th>价格模型</th>
<th>地形 / 等高线</th>
<th>国铁 / 地铁</th>
<th>优势</th>
<th>主要风险</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr class="best-row">
<td><strong>开放栈组合</strong>OSM + OpenTopoMap + Overpass + ORM</td>
<td class="price">¥0<small>软件与数据许可费;服务器/缓存另计</small></td>
<td><span class="tag">DEM 动态等高线</span><span class="tag">Esri 晕渲</span></td>
<td><span class="tag">Overpass OSM</span><span class="tag">OpenRailwayMap</span></td>
<td>无按次账单;可自由切换数据源;适合验证功能</td>
<td><span class="tag danger">无 SLA</span><span class="tag warn">瓦片 Fair-use</span><span class="tag warn">国内覆盖不均</span></td>
<td>原型、内部工具、低流量公开项目</td>
</tr>
<tr>
<td><strong>MapTiler</strong>Cloud / MapLibre</td>
<td class="price">Free $0<small>5k 会话/月;Flex $30/月含 25k 会话,超额 $2.50/1k 会话</small></td>
<td><span class="tag">Terrain RGB</span><span class="tag">Hillshade</span><span class="tag">可做等高线</span></td>
<td><span class="tag warn">需自制/外部矢量层</span></td>
<td>商业授权清晰;MapLibre 开源;约 8.7 万会话/月以上通常比 Mapbox 省</td>
<td>Free 不可超额续用;铁路专题不是内置成品</td>
<td>正式 Web 产品、中低流量、要稳定商用</td>
</tr>
<tr>
<td><strong>Mapbox</strong>GL JS</td>
<td class="price">前 50k 免费<small>50k–100k:$5/1k;100k–200k:$4/1k;更高档递减</small></td>
<td><span class="tag">Terrain / Hillshade</span><span class="tag">Contour Tiles</span></td>
<td><span class="tag warn">需自制/外部矢量层</span></td>
<td>地图样式、3D、文档和 SDK 成熟;50k 免费额度较高</td>
<td><span class="tag danger">流量后单价较高</span>;中国大陆网络与合规需评估</td>
<td>高交互地图、3D、海外用户为主</td>
</tr>
<tr>
<td><strong>高德开放平台</strong>JS API</td>
<td class="price">按产品/量级<small>注册 key + security key;商业授权按官方为准</small></td>
<td><span class="tag">3D 地形图</span><span class="tag warn">等高线需确认</span></td>
<td><span class="tag">道路/地铁数据较好</span><span class="tag warn">铁路专题需自行组合</span></td>
<td>中国大陆 POI、道路、公交与地图体验好;本土网络稳定</td>
<td>坐标系偏移;密钥安全;商业授权与技术服务许可需核对</td>
<td>中国大陆公开商业产品、生活服务地图</td>
</tr>
<tr>
<td><strong>天地图</strong>国家地理信息公共服务平台</td>
<td class="price">开发者服务按官方配额<small>免费起步;商业使用需按平台规则</small></td>
<td><span class="tag">地形产品</span><span class="tag warn">功能组合有限</span></td>
<td><span class="tag warn">铁路专题通常需外部数据</span></td>
<td>国内官方地理底图;中文标注;政府/科研项目常用</td>
<td>产品化交互与 API 生态较弱;配额和审图合规需确认</td>
<td>政务、科研、公开地理信息项目</td>
</tr>
<tr>
<td><strong>自建矢量瓦片</strong>Planetiler / Martin + DEM</td>
<td class="price">固定基础设施<small>约 $20–150/月起,视流量、存储、CDN 与运维</small></td>
<td><span class="tag">DEM 生成等高线</span><span class="tag">可完全定制</span></td>
<td><span class="tag">OSM/自有铁路数据</span><span class="tag">完全可控</span></td>
<td>高流量长期最省钱;无按会话账单;可缓存、离线、私有化</td>
<td>前期开发和运维成本;数据更新、CDN、合规仍需投入</td>
<td>高流量、固定预算、离线/私有化、长期产品</td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="section" id="calculator">
<div class="section-head">
<div>
<div class="eyebrow">COST CALCULATOR</div>
<h2>按月访问量快速估算</h2>
</div>
<p>“地图会话”按网页或 App 中地图初始化一次计。拖动或输入月度地图会话量,查看三家方案的近似软件费。</p>
</div>
<div class="calc">
<div class="card">
<h3>你的月度地图会话</h3>
<p>1 次会话内拖动、缩放通常不重复计费;MapTiler 与 Mapbox 的定义略有差别。</p>
<label for="sessionsInput">每月地图会话数</label>
<input id="sessionsInput" type="number" min="0" step="1000" value="50000">
<input id="sessionsRange" type="range" min="0" max="500000" step="1000" value="50000" aria-label="月度地图会话数滑块">
<div class="hint">当前设置:<strong id="sessionLabel">50,000</strong> 次/月</div>
<label for="infraInput">开放栈 / 自建基础设施预算(USD/月)</label>
<input id="infraInput" type="number" min="0" step="5" value="30">
<div class="hint">可包含小规格云服务器、对象存储、CDN 或托管瓦片服务;不含开发成本。</div>
<div class="formula">
估算假设:Mapbox 按官网阶梯价;MapTiler Flex 为 $30/月含 25k 会话,超额 $2.50/1k。税费、搜索、路线、静态图、存储和其他 API 不计入。
</div>
</div>
<div class="card">
<h3>近似月度软件/服务费</h3>
<p>结果仅用于技术选型预算,不替代正式报价。</p>
<div class="results">
<div class="result recommended">
<div><strong>开放栈 / 自建</strong><small>OSM + OpenTopoMap + Overpass + 自建或自选基础设施</small></div>
<div class="amount" id="openCost">$30<small>基础设施预算</small></div>
</div>
<div class="result">
<div><strong>MapTiler Flex</strong><small>超过 Free 额度后的稳定商用选项</small></div>
<div class="amount" id="maptilerCost">$30<small>基础价 + 超额</small></div>
</div>
<div class="result">
<div><strong>Mapbox GL JS</strong><small>Map Loads 阶梯计价</small></div>
<div class="amount" id="mapboxCost">$0<small>前 50k 免费</small></div>
</div>
</div>
<div class="formula" id="breakEven">
在当前访问量下,开放栈的差额主要来自基础设施、开发和运维;访问量越高,自建越有利。
</div>
</div>
</div>
</section>
<section class="section">
<div class="section-head">
<div>
<div class="eyebrow">IMPLEMENTATION PATH</div>
<h2>推荐落地路线</h2>
</div>
<p>从原型到正式产品的数据源可以分层替换,不需要一次性重写前端。</p>
</div>
<div class="arch">
<article class="arch-card">
<div class="number">01</div>
<h3>原型与内部工具</h3>
<p>直接使用当前工作台:开放底图 + OpenTopoMap 等高线 + OpenRailwayMap + Overpass。</p>
<ul>
<li>软件费:¥0</li>
<li>网格在浏览器内绘制</li>
<li>设置可替换的瓦片 URL</li>
</ul>
</article>
<article class="arch-card">
<div class="number">02</div>
<h3>低预算商业版</h3>
<p>保留 Leaflet / MapLibre 前端,将底图切换到 MapTiler;铁路继续用 OSM 矢量并缓存。</p>
<ul>
<li>优先 MapTiler Flex</li>
<li>自建轻量瓦片代理与缓存</li>
<li>等高线用 DEM 生成 PBF</li>
</ul>
</article>
<article class="arch-card">
<div class="number">03</div>
<h3>高流量 / 国内商业版</h3>
<p>中国公开服务优先高德或天地图底图;全球用户可自建 Planetiler/Martin + CDN。</p>
<ul>
<li>规避公共瓦片 Fair-use</li>
<li>固定预算、可预测扩容</li>
<li>按要求完成审图、授权与备案</li>
</ul>
</article>
</div>
</section>
<section class="section">
<div class="notice">
<h3>中国大陆上线前必须核对的合规项</h3>
<p>互联网地图服务、测绘成果展示、审图号、坐标系、数据出境和商业授权不能只看 API 是否能调通。若产品在中国大陆公开运营,请让法务/合规确认底图来源、审图要求、境外数据传输、用户定位隐私和供应商商业授权。OSM 数据可自由使用,但公共瓦片服务器并非无限制商业 CDN。</p>
</div>
</section>
<section class="section" id="sources">
<div class="section-head">
<div>
<div class="eyebrow">SOURCES</div>
<h2>价格与规则来源</h2>
</div>
<p>价格和配额会变化,采购前应再次打开官方页面核对。以下数字于 2026-11-03 从公开页面核对。</p>
</div>
<ul class="sources">
<li><a href="https://operations.osmfoundation.org/policies/tiles/">OSMF Tile Usage Policy</a> — OSM 公共瓦片的 Fair-use、缓存和署名要求。</li>
<li><a href="https://www.opentopomap.org/about">OpenTopoMap About</a> — SRTM 等高线地形图、开放数据来源。</li>
<li><a href="https://www.openrailwaymap.org/">OpenRailwayMap</a> — 基于 OpenStreetMap 的铁路基础设施图层。</li>
<li><a href="https://www.maptiler.com/cloud/pricing/">MapTiler Cloud Pricing</a> — Free 5k 会话;Flex $30/月含 25k;超额 $2.50/1k。</li>
<li><a href="https://www.mapbox.com/pricing/">Mapbox Pricing</a> — GL JS Map Loads 前 50k 免费,50k–100k $5/1k。</li>
<li><a href="https://lbs.amap.com/api/javascript-api-v2/guide/abc/prepare">高德 JS API 2.0</a> — key/security key、企业认证与安全接入要求。</li>
<li><a href="https://www.tianditu.gov.cn/">天地图</a> — 国家地理信息公共服务平台与官方地图服务。</li>
<li><a href="https://registry.opendata.aws/terrain-tiles/">AWS Terrain Tiles</a> — 公开 Terrarium DEM 瓦片,可作为低成本等高线数据源。</li>
</ul>
</section>
<footer>
Map Studio 成本对比材料 · 估算不包含税、CDN、开发、运维、审图与商业授权成本。公共数据和瓦片请遵守各项目许可证与使用政策。
</footer>
</div>
<script>
(function () {
"use strict";
const sessionsInput = document.getElementById("sessionsInput");
const sessionsRange = document.getElementById("sessionsRange");
const infraInput = document.getElementById("infraInput");
const sessionLabel = document.getElementById("sessionLabel");
const openCost = document.getElementById("openCost");
const maptilerCost = document.getElementById("maptilerCost");
const mapboxCost = document.getElementById("mapboxCost");
const breakEven = document.getElementById("breakEven");
const money = (value) => "$" + Math.round(value).toLocaleString("zh-CN");
const number = (value) => value.toLocaleString("zh-CN");
function mapboxEstimate(sessions) {
const tiers = [
{ start: 0, end: 50000, rate: 0 },
{ start: 50000, end: 100000, rate: 5 },
{ start: 100000, end: 200000, rate: 4 },
{ start: 200000, end: 1000000, rate: 3 },
{ start: 1000000, end: 5000000, rate: 2.5 }
];
let cost = 0;
for (const tier of tiers) {
const used = Math.max(0, Math.min(sessions, tier.end) - tier.start);
cost += used / 1000 * tier.rate;
}
return cost;
}
function maptilerEstimate(sessions) {
if (sessions <= 5000) return 0;
if (sessions <= 25000) return 30;
return 30 + Math.ceil((sessions - 25000) / 1000) * 2.5;
}
function update() {
let sessions = Math.max(0, Number(sessionsInput.value) || 0);
const infra = Math.max(0, Number(infraInput.value) || 0);
if (sessions > 5000000) {
sessionsInput.value = 5000000;
sessionsRange.value = 5000000;
sessions = 5000000;
}
sessionsRange.value = Math.min(500000, sessions);
sessionLabel.textContent = number(sessions);
openCost.innerHTML = money(infra) + "<small>基础设施预算</small>";
maptilerCost.innerHTML = money(maptilerEstimate(sessions)) + "<small>Free / Flex 近似</small>";
mapboxCost.innerHTML = money(mapboxEstimate(sessions)) + "<small>Map Loads 阶梯</small>";
if (sessions <= 5000) {
breakEven.textContent = "低流量阶段可先用 MapTiler Free 或开放瓦片验证;注意 MapTiler Free 超额后不会自动转按量,而是暂停至下月。";
} else if (sessions <= 25000) {
breakEven.textContent = "MapTiler Free 已不足;Mapbox 在 50k 免费额度内软件费为 $0,MapTiler Flex 的 $30/月更适合需要稳定预算的正式产品。";
} else if (sessions <= 100000) {
breakEven.textContent = "按当前阶梯价,Mapbox 在约 8.7 万会话以内仍较低;若已有服务器和缓存能力,开放栈/自建可进一步降低边际成本。";
} else {
breakEven.textContent = "约 8.7 万会话以上 MapTiler Flex 开始比 Mapbox 省;高流量下两者按量费用都会快速增长,建议进一步评估自建矢量瓦片、DEM 等高线与 CDN 缓存。";
}
}
sessionsInput.addEventListener("input", update);
sessionsRange.addEventListener("input", () => {
sessionsInput.value = sessionsRange.value;
update();
});
infraInput.addEventListener("input", update);
update();
})();
</script>
</body>
</html>