npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tec-mapkit

v3.0.0

Published

The TEC campus map as a licensed npm package. Ships the basemap, its districts and listings, trek trails, geofenced side-quests and directions over the campus road network — all offline, no tile server and no data to supply. Requires a licence key.

Readme

tec-mapkit

The TEC campus map, as an npm package.

Install it, add your key, and you have the map: the basemap, its districts, its business listings, and directions between them. There is no data to supply, no tile server and no configuration step.

tec-mapkit is a commercial package. There is no free tier and no degraded mode: without a valid key the map renders nothing and downloads no data. See Licensing.

Getting started

1. Install.

npm install tec-mapkit maplibre-gl

2. Check the setup before you write anything.

npx tec-mapkit doctor $MAPKIT_KEY
tec-mapkit doctor v3.0.0

Environment
  ✓ Node — 22.15.0
  ✓ WebCrypto — available

Dependencies
  ✓ maplibre-gl — 4.7.1
  ✓ react — 18.3.1 — tec-mapkit/react is usable

Licence key
  ✓ signature — valid — issued by tec-mapkit
  ✓ licensed to — Acme Ltd
  ✓ domains — acme.com
  ✓ expiry — 2027-01-30 (356 days)

Every one of those checks fails the same way in a browser — an empty rectangle — so it's worth ten seconds up front. The doctor exits non-zero on a real problem, which makes it a usable CI step. It makes no network calls: keys verify offline, so it works on a plane and behind a firewall. Keys are read from the argument, then MAPKIT_KEY, then VITE_MAPKIT_KEY.

3. Render it. The map fills its parent, so the parent needs a height.

import 'maplibre-gl/dist/maplibre-gl.css';
import { MapCanvas } from 'tec-mapkit/react';

<div style={{ height: '100vh' }}>
  <MapCanvas apiKey={process.env.MAPKIT_KEY} directions />
</div>

That's the whole integration. If something is wrong — no key, an expired one, a missing peer dependency, a container with no height — the map says so on screen rather than rendering blank.

What it is (and isn't)

This package renders one map. The data is the product: the campus geometry, the districts and listings curated through packages/admin, and the road network directions run over. The API is about how that map looks and behaves — camera, palette, clustering, directions, which layers are visible.

It deliberately does not accept your own basemap, zones, markers, road network or style spec. Passing one logs a warning and is ignored. If you need a map that renders arbitrary GeoJSON, this is the wrong package — that's a different product with different licensing and support questions attached.

<MapCanvas basemap="/mine.geojson" />   // warns, renders the campus map anyway

Licensing

A key is a signed token carrying your organisation, your licensed domains and an expiry. It's verified in the browser against a public key embedded in the package — there's no call home, so your map works offline, behind a firewall, and doesn't go down when we do. It also means no per-request billing and no tile bill.

<MapCanvas apiKey="mk_live_…" />
// Verify one yourself — to fail a build early, or show your own message.
import { verifyLicense } from 'tec-mapkit';
const { customer, domains, expires } = await verifyLicense(key);

Keys are safe to ship in your bundle, the same way a publishable map token is: editing one to extend its term or add a domain breaks its signature, so it stops verifying. localhost always passes the domain check, so developing against a production key works without a second key.

When something's wrong

Setup problems render an explanation in place of the map instead of leaving a blank rectangle — a missing key, an expired one, a key for another domain, a missing maplibre-gl, or a container with no height. Each also fires onError, so you can replace the built-in message with your own.

onError receives a LicenseError with a code:

| code | means | |---|---| | missing | No apiKey was passed | | malformed | Not a key — wrong prefix or truncated | | invalid | Signature failed — not issued by us, or edited | | expired | Past the key's expiry date; renew it | | domain | Valid key, but not licensed for this hostname | | no-crypto | WebCrypto unavailable — usually plain HTTP instead of HTTPS |

On what this does and doesn't enforce. The map data ships inside the package, so verification is offline and therefore a licence rather than a lock: someone determined can patch the check out or read node_modules. That's true of every offline-licensed library. Keys are signed and attributable precisely because the meaningful remedy for that is commercial rather than technical. Genuine metering would require serving the data from an API instead of shipping it.

Issuing keys (maintainers)

node scripts/gen-keypair.mjs                     # once, ever
node scripts/issue-key.mjs -c "Acme" -d acme.com --months 12

.license-key.json is the private signing key: gitignored, never published, and the entire security of the scheme — anyone holding it can mint unlimited keys. Back it up, and note that regenerating it invalidates every key ever issued.

Keys can't be revoked before their expiry (there's nothing to revoke against), so prefer yearly terms over perpetual ones.

Contents

| | | |---|---| | Basemap | The campus OSM extract — roads, landuse, water — clipped to ~11 × 11 km around campus and simplified. ~320 KB of GeoJSON | | Districts | Named campus areas with hover, zone:click and region:change. Authored via packages/admin | | Businesses | Clickable listings with category icons. Authored via packages/admin | | Trails | Trek routes with difficulty and measured length, drawn dashed so they never read as roads | | Side-quests | Geofenced points that unlock when someone physically stands inside their radius | | Directions | Dijkstra over the shipped road network, weighted by travel time per road class, drawn on the map | | Themes | vale (default), midnight, slate, daylight, or your own token bag |

The map fills its parent, so the parent needs a height — the most common cause of a blank map. The engine logs a warning naming the container size when it sees one.

Directions

directions renders the built-in panel: pick two of the map's own places, or drop a pin anywhere, and the route is drawn with distance and duration.

<MapCanvas directions onRouteChange={r => setEta(r?.durationSeconds)} />

Imperatively:

const engine = new MapEngine({ container });
await engine.mount();

engine.getLocations();  // [{ id, name, kind: 'business' | 'district', lng, lat }, …]
engine.showRoute([77.9689, 30.4078], [77.9700, 30.4150]);
// -> { distanceMeters, durationSeconds, path, snappedFrom, snappedTo }
engine.clearRoute();

showRoute() returns null when the two points aren't connected — clearing any previously drawn route, so the map never shows a stale path beside a failed lookup. route() is the same computation without drawing anything, and estimateTravel() gives a straight-line figure for a rough "how far" that doesn't need the network.

Routing uses the shipped basemap, which carries highway and oneway, so it needs no setup. Endpoints snap to the nearest road; a point implausibly far from any road (maxSnapMeters, default 500 m) yields null rather than a misleading path.

Trails

Trek routes ship alongside the listings, drawn dashed and coloured by difficulty. distanceMeters is measured from the geometry at build time, so it can never disagree with the line it describes.

engine.getTrails();        // [{ id, name, difficulty, distanceMeters, geometry }, …]
engine.showTrail('bidholi-ridge-loop');   // frames it
engine.setTrailsVisible(false);

Clicking one opens its card and fires trail:click.

Side-quests

A quest is a point, a radius and something to say when someone stands inside it. watchQuests starts a high-accuracy position watch and activates them as they're reached; quest:enter fires once on the way in and quest:leave once on the way out.

<MapCanvas apiKey={key} watchQuests onQuestEnter={q => celebrate(q.reward)} />
engine.startQuests();                  // watch the device position
engine.updatePosition([lng, lat]);     // or drive it from a position you already have
engine.getQuests();                    // each flagged with `active`
engine.resetQuests();

Entering uses the radius, but leaving needs 25% beyond it. Without that dead band a GPS fix wandering at the boundary — which is what real fixes do standing still — fires and unfires the quest repeatedly, which as a UI is a card flickering in and out.

For the same reason a radius under 20 m is rejected at build time: it's smaller than the positioning error that has to land inside it, so it would mostly never trigger.

updatePosition() being public is what makes this buildable and demoable at all — otherwise testing a geofence means physically walking to it.

Partner actions

A listing's category decides which call-to-action its card gets: scooter rentals book through Rentie, restaurants and cafés order through Owlit. Anything else gets no button, which is better than an irrelevant one.

Links resolve per-listing first (rentieUrl / owlitUrl on the record, set in the admin tool) and fall back to a per-partner template. Matching is substring-based and case-insensitive, because "Scooty", "Scooter Rental" and "Two-Wheeler Rentals" are all the same thing to whoever typed them.

// Override the mapping, or add partners of your own.
<MapCanvas apiKey={key} partners={[...DEFAULT_PARTNERS, myPartner]} />
import { partnerAction } from 'tec-mapkit';
partnerAction(listing);   // { id, label, href } | null

Options

Everything below is on both <MapCanvas> and new MapEngine({...}).

| | | |---|---| | palette | 'midnight' · 'slate' · 'daylight' · your own tokens | | center / zoom | Default the campus centre at zoom 14 | | minZoom / maxZoom | Default 12.5 / 18 — below 12.5 the viewport is wider than the data | | maxBounds | Defaults to the box the geometry was clipped to | | districts / businesses / trails / quests | Show or hide any bundled layer | | watchQuests | Watch the device position so quests activate. Off by default — it prompts for permission | | infoCard / partners | The card on selection, and its partner call-to-actions | | clustering | Off by default while listings show — clustering hides individual pins below clusterMaxZoom | | directions / routeColor | The directions panel and the colour of its line | | interactive / dragRotate / flyIn | Camera behaviour | | categoryColors / categorySymbols | Per-category pin colour and glyph |

Events

ready · move · marker:click · cluster:click · zone:click · zone:enter · region:change · route:change · trail:click · quest:enter · quest:leave · locate · error

Available as on* props and via engine.on(), which returns an unsubscribe function.

Methods

| method | purpose | |---|---| | mount() / destroy() | Lifecycle; mount() resolves once the style is loaded | | getLocations() | Every routable place on the map | | getTrails() / showTrail() / setTrailsVisible() | Trails | | getQuests() / startQuests() / stopQuests() / resetQuests() | Quests | | updatePosition(point) | Test a position against every geofence | | showRoute() / clearRoute() / getRoute() | Directions | | route() / estimateTravel() | Distance and time without drawing | | setActiveMarker(id) | Highlight one listing | | setZonesVisible(bool) | Toggle the district overlay | | flyTo / easeTo / fitBounds / zoomIn / zoomOut / recenter | Camera | | getView() / getRegion() | Current camera, and the district under it | | startLocate() / stopLocate() | Device position with a pulsing dot |

Without React

import { MapEngine } from 'tec-mapkit';
import maplibregl from 'maplibre-gl';

const engine = new MapEngine({
  container: document.getElementById('map'),
  apiKey: 'mk_live_…',
  maplibregl,
});
await engine.mount();   // rejects with a LicenseError if the key doesn't check out
engine.on('marker:click', m => console.log(m.id, m.meta));

MapLibre is a peer dependency and the engine finds it on its own: the maplibregl option first, then window.maplibregl (how a CDN <script> tag exposes it), then the package itself. Import maplibre-gl/dist/maplibre-gl.css once in your app; if you don't, the engine injects a minimal fallback so the controls still land in the corners instead of stacking underneath the map.

Server rendering is safe — MapLibre is only pulled in by a dynamic import() inside a browser-only effect. Mark the component 'use client' in the Next.js App Router.

Reading the data

tec-mapkit/campus exposes what ships, for listing places or testing a point against the covered area. You can read it; you can't feed it back in, and you don't need to — the map loads it itself.

import { campusBasemap, campusDistricts, campusBusinesses, CAMPUS_BOUNDS, CAMPUS_VIEW }
  from 'tec-mapkit/campus';

It's a separate lazy chunk behind a dynamic import(), so it isn't fetched until a map actually mounts.

Editing the shipped data

data/districts.json, data/businesses.json, data/trails.json and data/quests.json are the source of truth. Districts and businesses are edited through a local-only admin app — draw a district by clicking points, place a business by clicking where it sits; it writes straight to disk and is never built or deployed. Trails and quests are hand- edited JSON for now, and validated on the way into the bundle.

npm run admin                  # author districts and businesses
node scripts/gen-overlays.mjs  # bake data/*.json into the shipped bundle

The basemap itself is regenerated from an OSM extract:

node scripts/gen-basemap.mjs [path/to/map.geojson]

Both steps are deliberately manual — nothing rebuilds the bundle automatically, so an editing session only ships when you say it does.

CLIP_RADIUS in gen-basemap.mjs is the coverage/size dial, and CAMPUS_VIEW's maxBounds is derived from it, so the camera can never reach a region with no data. Widening it grows the bundle roughly with its square. Note that simplification there preserves shared vertices on purpose: plain Douglas-Peucker deletes the vertex where a side street meets a main road, which leaves the map looking perfect while routing silently returns null — see the comment in that file before changing it.

Attribution

The basemap is a real OpenStreetMap extract, so it carries the standard "Map data © OpenStreetMap contributors" credit by default via MapLibre's AttributionControl. This isn't cosmetic: OSM data is licensed under ODbL, which requires attribution when the data is redistributed. It's on by default because a missing credit is a compliance gap, not a style preference.

<MapCanvas attribution="© Your Company · Map data © OpenStreetMap contributors" />

License

Commercial and proprietary — see LICENSE. The Software is licensed, not sold: one licence per product, for the term of your key, on the domains recorded in it. Redistributing the package, or bypassing the key check, is not permitted.

The bundled basemap is derived from OpenStreetMap and remains under ODbL, which this licence does not supersede. The "Map data © OpenStreetMap contributors" attribution the map displays is a licence obligation on that data, not a style choice — don't remove it. Fees cover the software and the curation around the data, not the OpenStreetMap data itself.