KITE Local: privacy-first KML/KMZ/GeoJSON viewer that keeps your folder tree. One HTML file, nothing uploaded. Try it: https://ifsnop.github.io/kite/
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗Nothing to install, no account, nothing uploaded: open the link and drop a KML or KMZ onto the left panel. No file of your own at hand? Download the sample KMZ and drop that.
KML Interactive Tree Explorer
A privacy-first, browser-based viewer and organizer for KML, KMZ, GeoJSON, shapefile and GPX files, Cloud Optimized GeoTIFF orthophotos and ArcGIS map services. It preserves nested KML folder structures as an interactive layer tree, runs from one self-contained HTML file, and keeps imported data inside the user's browser.
The demo is the application itself, not a hosted service: the page is served from GitHub Pages and everything then runs in the browser. Files dropped onto it are not uploaded anywhere — the same guarantee as opening the downloaded file locally.
In short: if a KML is used as a conventional collection of folders, placemarks, lines and polygons in Google Earth, KITE Local is designed to open it directly while retaining its folder hierarchy. Advanced Google Earth features such as 3D models, tours, screen overlays and network links are outside the current scope; georeferenced ground overlays are supported.
The documents live in docs/ and are written in Spanish:
Google has announced that new downloads of Google Earth Pro for desktop will end on 25 June 2027. Existing installations are expected to continue working, but the desktop client will no longer be the long-term distribution path. Google is directing users towards its web and mobile products.
That transition leaves a practical gap for users who mainly need to open local KML/KMZ files, retain their organization, compare layers, adjust simple styles, add locations and make quick measurements without installing a full GIS package or uploading data to an application server.
KITE Local focuses deliberately on that workflow:
Document and Folder structures remain a browsable tree instead of being flattened into geometry layers or reduced to metadata..kite.json package; the current view, as a PNG image
or a link; a route profile, as a PNG image or a CSV file (comma and decimal point in English, semicolon and decimal comma in Spanish).It is an independent AGPL-3.0 project and is not affiliated with or endorsed by Google, Google Earth, QGIS, OpenStreetMap, Esri, Leaflet, Iconify or Instituto Geográfico Nacional de España.
Many KML files are not merely collections of geometries: their nested folders carry operational meaning. KITE Local recreates this hierarchy in the navigation panel and allows each file, folder, subtree and individual layer to be enabled or disabled.
General-purpose GIS importers commonly translate KML folders into separate geometry layers or store the original path as an attribute. KITE Local instead keeps the structure visible and directly usable.
Imported files are parsed locally. Their geometries and workspace remain in the browser and are stored locally through IndexedDB. No application account or backend upload is required.
External requests are still made for libraries, map tiles, WMS imagery and optional place searches; see Internet access and privacy.
The complete application—interface, styles and program logic—is contained in one HTML file. Download it and open it in a modern browser. No build, installation or server is needed to use it. Developing it is another matter: the sources live in src/ and are concatenated into that file by build.js (see the roadmap), but nobody running the viewer ever needs that step.
KITE Local does not impose fixed limits on KML file size, imported features or vertices. Actual limits depend on browser capabilities, available RAM, local storage quota and rendering performance.
Document and Folder structures<name>.shp together with its .dbf, .prj and .cpg,
or the .zip that holds them (one folder per layer). Coordinates are
reprojected from the .prj with proj4 (ETRS89 UTM, the usual case in
Spain; ED50 with the standard peninsular parameters, flagged in the
summary). Without a .prj, only coordinates already in degrees are
accepted. Text encoding comes from the .cpg, or is detected (UTF-8,
else Windows-1252). Z values are not imported.tif, 8-bit RGB or grey) and
single-band elevation models (16/32-bit integer or float, drawn with a
colour ramp and relief shading, altitude under the cursor): dropped
onto the panel, validated, then copied as a Blob into the browser's own
IndexedDB store so they survive a reload. Drawn by a custom
L.GridLayer that reads only the window and overview level each tile
needs (geotiff.js fromBlob), reprojected per tile through a 16×16
proj4 grid (WGS84 UTM any zone north or south, ETRS89 UTM 28–38 (EPSG:258xx and 3040–3050), ED50 UTM
28–31, REGCAN95, WGS84, ETRS89, Web Mercator). Not included in .kite.json exports or the system
clipboard; a deleted one is purged from storage on the next start.kite.json)MapServer): see ArcGIS map servicesExtendedData (<Data> and <SchemaData>) are kept as layer attributesdescription
(sanitized against an allow-list) or a table of the GeoJSON
properties.kite.json
package (the 💾 button on its row).kite.json folders onto
the panel, or drop them straight into a folderSeveral can be enabled at once, each with its own opacity, and their stacking order can be changed:
A base layer whose service stops answering is flagged in the panel, and the flag clears by itself when the tiles come back.
ExtendedData). Sort by column (numbers as numbers, empty cells last),
filter, jump to a layer and export what is shown as CSV (commas and a decimal point in English, semicolons and a decimal comma in Spanish), with the standard
defence against spreadsheet formulas hidden in a foreign file. A folder of
5 000 layers keeps about twenty rows in the DOMMapServer into the 🔗 dialog: the
service becomes one node of the tree with a row per server layer, drawn by
the server itself as image tiles (so its symbols and labels are intact),
and the checkboxes choose which layers it is asked fortoken in the address is
dropped), and a service that arrives inside someone else's .kite.json
arrives switched off, with a notice naming the server#map=… part of the
address, and opening such a link restores themabsolute, relativeToGround, clampToGround; sea-floor modes are left out with one warning per file) is kept, shown and editable in the polygon's list of points; altitude without a mode, and GeoJSON altitude, count as above sea level. Above-ground heights are added to the terrain the profile has just downloaded, with no extra request. Obstacles and the tower heights and vertices are saved with the profile; «Actualizar obstáculos» collects the visible polygons againThe fastest way is the live demo — nothing to download, and the files still never leave the browser.
To keep a local copy:
kitelocal.html in a modern desktop browser — double-clicking the file is enough..kite.json files onto the left navigation panel.git clone <repository-url>
cd <repository-directory>
No package manager, web server or build process is required to use the viewer. The complete application is distributed as one HTML file.
Two copies of that file are published, and they are the same application:
kitelocal.html — the readable build, with the source and its comments intact. This is the one to download, read or debug.kitelocal.min.html — a minified derivative, and what the demo link serves: roughly 55 % smaller over the wire (gzip) than the readable build.When it is served over https (the demo link, or your own server), a third file, sw.js, lets the page reload without a network connection: a small service worker that keeps the app and its pinned, integrity-checked libraries, so a tab the browser discarded comes back with your saved tree. Map tiles, place search and elevation services still need a network; the app says so instead of failing obscurely. Opened by double click (file://) no worker is registered and nothing changes. sw.js is generated from src/sw.js like the other two.
The complete tree is serialized to GeoJSON and saved in the browser using IndexedDB. Imported source files are not required after a successful import. The application also requests persistent browser storage when the browser supports it.
Browser storage is convenient, but it is not a backup. It may be cleared by the user, browser policy or storage pressure. Export important folders to .kite.json and keep copies outside the browser profile.
To start over, Properties → Restore deletes everything the application has saved in the browser (the tree, the copied orthophotos and every setting). A button alone does nothing: it opens a second confirmation, and the page reloads when it is done. The interface language is kept.
Imported KML/KMZ/GeoJSON content is parsed locally and is not uploaded to an application server. The imported geometries and workspace remain in the user's browser.
The current build is not fully offline and makes external requests:
unpkg.com, and JSZip from
cdnjs.cloudflare.com, when the page opens;unpkg.com) and proj4js
(cdnjs.cloudflare.com) are loaded only the first time they are needed
(PNG export, GeoTIFF/COG reading, reprojecting a shapefile .prj), with
the same pinned version and integrity hash as before;Material Design marker icons are embedded in the file and cost no request at all. They used to be fetched one by one from api.iconify.design, which meant 79 simultaneous requests every time the icon picker was opened; past the service's rate limit the icons silently went blank. They are now baked in at build time.
These providers receive ordinary request metadata, and text entered in the place search is sent to Nominatim. The imported KML geometry itself is not sent by the application to these services.
For controlled or offline deployments, vendor the JavaScript, CSS, image and icon dependencies locally, disable or replace the geocoder, and configure approved tile/WMS services.
Opening a URL downloads exactly the address you paste, with no credentials attached, and the content is then processed locally like any dropped file. Nothing is sent anywhere: the download is the only request, and it goes to the server you named.
These tools solve different problems:
KITE Local covers the simpler but common task of opening, organizing, comparing and measuring structured KML content quickly while keeping it under the user's control.
The viewer handles the core structures used by many conventional KML files:
Document and Folder hierarchyPlacemarkPointLineStringPolygon, including inner ringsMultiGeometryvisibility and folder/document openStyle for basic line and polygon appearanceStyleMap normal-style referencesGroundOverlay georeferenced images, including those bundled in a KMZ
(axis-aligned LatLonBox only: a rotation is read and reported, but
not applied)KML is a broad specification. The following are not fully supported or are intentionally simplified:
StyleMapImported marker icons are replaced by the viewer's default marker unless the user selects another icon.
The viewer recognizes a subset of Mapbox simplestyle properties:
strokestroke-widthfillfill-opacityOther properties remain part of the GeoJSON data but may not affect rendering.
A recent desktop browser is recommended. The application relies on IndexedDB, Fetch, File and Blob APIs, async/await, pointer events, CSS custom properties and :scope selectors.
The interface is primarily designed for mouse and keyboard use. On a phone it shows a notice recommending a computer and offers a map-only layout; touch support and accessibility otherwise remain areas for improvement.
DOMParser and JSON.parse are synchronous and can briefly block the interface on large inputs (reading the XML of a 309 MB KML is one task of about 4 seconds)..zip) is checked before anything is unpacked: at most 2000 entries, a 200× compression ratio and an uncompressed size of 500 MB by default, which you can change in Properties → Preferences.<extrude> is not read, so such a polygon is always a block from the terrain up). Two placemarks at the same latitude and longitude but different altitudes are still treated as duplicates.StyleMap normal-style references. (Namespaces are not a limitation — elements are matched by local name, so prefixed, default-namespaced and namespace-less KML all work.).kite.json is application-specific and currently requires a matching tree schema version.connect-src accepts any https origin, which is the one directive that had to be opened: the address a URL import downloads from is chosen by the user, so enumerating origins is impossible by definition. Nothing else was relaxed — default-src 'none' stands, and script-src, style-src and img-src keep their closed lists, so a remote origin can supply data but never code or styles. http: is deliberately excluded.
Treat imported files as untrusted input. The viewer uses text insertion and explicit HTML escaping for marker labels, sanitizes KML description HTML against an allow-list, ships a restrictive Content-Security-Policy (default-src 'none' plus the specific origins it needs), and pins its CDN dependencies with Subresource Integrity. Marker icons are embedded rather than fetched, so no remote SVG is executed.
Do not use public tile, geocoding or CDN services for sensitive work without an appropriate security and privacy review.
src/index.html,
src/styles.css, src/sw.js and 40 JavaScript files, concatenated into the
shipped file by build.js. The single-file product is the output, not the
source.npm test.role="tree" with roving focus, dialogs trap and restore focus, and
the panel is driven entirely from the keyboard..kite.json, and import of GPX.The full list, with the ideas that have been proposed (exporting to KMZ and GPX, importing CSV points, a visibility map, MGRS coordinates, marker clustering, time-aware KML, GeoPackage and QGIS projects…), is kept in one place: pending work and proposals. The known gaps, in short:
DOMParser nor Leaflet, so the work would have to be
split awkwardly across the boundary..kite.json migrations. Storage is versioned, and data
from a different version is discarded rather than migrated. The version is
only raised when a change requires it, and the user is told when a saved
workspace is dropped.debug branch)There is a second branch, debug, which is main plus a main-thread watchdog. The released product on main does not carry it, and is not meant to: it is diagnostic code, it wraps several dozen functions to count and time them, and it has no business running in something people use. The two branches are otherwise the same, and this README is kept identical on both so the instructions are wherever you happen to be looking.
The watchdog exists because a reported freeze could not be reproduced under test automation: the page was dead to the user while, from the inside, nothing seemed to be running. The instrument is what eventually named the cause — a native HTML5 drag session, which is a nested event loop in the browser, so the page receives no timers, no frames and no input while it lasts, without executing a line of its own code.
Keep it for the next time something hangs or drags its feet.
How to use it
git checkout debug && git pull, then open kitelocal.html from that branch. It is already built — no npm run build needed.Vigilante activo (N funciones).__kiteDiag() in the browser console before reloading, and keep the output.How to read it
| Field | What it answers |
|---|---|
bloqueos | The main thread stopped. Each entry says for how long, which functions ran during the gap, which were still in flight, and the heap before and after — if that number drops sharply, the pause was garbage collection. |
fotogramas | Gaps between animation frames. Frames stalling while bloqueos stays empty means painting is stuck, not scripting: look at the browser, not at the code. |
pulsaciones | The last presses, each with what was really under the pointer and which layers were open. tapado: true is a click eaten by something on top. A pointerdown with no matching click is a press that turned into a drag — that was the signature of the freeze. |
llamadas | Cumulative call counts, useful for spotting something called a thousand times more than it should be. |
memoriaSinRecolectar | usedJSHeapSize, which is not collected memory: a large number here is not proof of a leak. Measured once at 240 MB where the live heap, after forcing collection, was 15 MB. |
Two things it taught us, worth remembering before trusting a reading
applyVisibility → setLayerVisible → rootGroup) nor any of Leaflet's, so it reported an idle app during a 30-second block.To bring the branch up to date after main moves: git checkout debug && git merge main, then npm run build and node tests/run-all.js.
Issues and pull requests are welcome. Include the browser/version, a minimal non-sensitive sample, the expected result, and the actual result or console error.
Do not attach operational, confidential or personally identifiable geospatial data to a public issue.
One-time setup after cloning, alongside npm install: link the pre-commit hook that keeps BUILD and the generated artifacts in sync automatically.
ln -s ../../scripts/hooks/pre-commit .git/hooks/pre-commit
chmod +x scripts/hooks/pre-commit
.git/hooks/ isn't tracked by git, so this symlink has to be created once per clone — the script itself lives at scripts/hooks/pre-commit, a normal tracked file, reviewable like anything else. Once linked, any commit that touches src/ or package.json bumps BUILD (src/js/10-map.js) to the current timestamp, runs npm run build, and stages the regenerated kitelocal.html/kitelocal.min.html before the commit is created — a commit that only touches docs/, tests/ or this file is left untouched. If the hook's own build or syntax check fails, the commit is aborted and the working tree is restored to exactly how it was, so the actual error (whatever broke src/) is the only thing left to look at. Without the symlink, nothing breaks — BUILD just stays whatever it was until someone bumps it (or the hook gets linked), and npm run check/CI still catch a stale artifact the same way they always have.
Translations. The interface is available in Spanish (the source language) and English, and picks the browser's language by default (Properties → Preferences → Language overrides it). The Spanish text in the code is the translation key (tr("Cancelar")), so there is no separate catalogue to keep in step; each language is one dictionary file, src/js/04-i18n-<code>.js. To add one:
node i18n-tool.js new fr "Français" # creates src/js/04-i18n-fr.js with every key empty
# add "04-i18n-fr.js", to the JS manifest in build.js, before "04-i18n-init.js"
npm run i18n # how much of each language is translated
node i18n-tool.js sync fr # after the code changes: keep what is translated, add new keys
An empty value falls back to English, then to Spanish, so a partial translation is fine; English must stay complete (npm test checks it, along with placeholders and that no Spanish literal is left outside tr()). Right-to-left languages are not supported yet. The details are in .claude/rules/i18n.md.
If what you hit is a freeze or a slowdown rather than a wrong result, the debug branch carries an instrumented build that answers questions a console error cannot — see Diagnosing a freeze or a slowdown. Its output pastes straight into an issue.
Feature work and fixes land on branches, each through its own pull request into main. Cutting a release once every intended PR is merged:
package.json's "version" and update the version shown in the user manual and the security study under docs/, then commit — this is the release's final commit. The pre-commit hook (see "Contributing") notices package.json changing, bumps BUILD and runs npm run build on its own, so there's no separate manual build step here anymore.git tag vX.Y.Z && git push origin vX.Y.Z. The tag must match package.json's version precisely (with a leading v)..github/workflows/release.yml takes it from there: it verifies the tag matches package.json, that kitelocal.html/kitelocal.min.html/sw.js are up to date with src/, and runs the full test suite (npm test, with the browser suites required) — only then does it publish the GitHub Release itself, with auto-generated release notes from the merged PRs and both artifacts attached as downloadable assets for that exact version.If any of those checks fail, the workflow fails and no Release is published — the test run happens inside this same job specifically so a red suite stops it before the publish step ever runs, rather than racing independently against the separate tests.yml workflow that the same tag push also triggers. Fix what's missing (the version bump, npm run build, or the failing test), delete the bad tag (git push --delete origin vX.Y.Z), and tag the corrected commit.
package.json's "version" is the single source of truth: build.js bakes it into the app's attribution line (v<version> (<build>) | GitHub | Leaflet, bottom-right of the map) at build time, the same way it already inlines the page's CSS and JS.
This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0).
See the repository's LICENSE file for the complete license text. Redistribution and modification are subject to the AGPL-3.0 terms.
The map rotation code (src/js/08-map-rotation.js) is derived from leaflet-rotate 0.2.8 by Raruto and contributors (IvanSanchez, Fnicollet, Hyperknot), released under the GPL-3.0. It has been incorporated, cut down and modified, and is distributed as part of KITE Local under the AGPL-3.0, as section 13 of the GPLv3 allows; its authorship notice is kept in the file.
This application currently uses or accesses:
.shp and .dbf into GeoJSON.prj, and COG orthophotos per tileTheir licenses, attribution requirements, acceptable-use policies and service limits apply independently.
Google Earth and Google Earth Pro are trademarks of Google LLC.
nametitle