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

@phila/phila-ui-map-core

v1.0.0

Published

Core map utilities and styles for Phila UI library

Readme

@phila/phila-ui-map-core

Component Status

| Component | Status | | ---------------------- | ------------------------------------------------------------------ | | BasemapDropdown | Stable | | BasemapToggle | Stable | | CircleLayer | Stable | | DrawTool | Stable | | FillLayer | Stable | | GeolocationButton | Stable | | LineLayer | Stable | | Map | Stable | | MapButton | Stable | | MapFloatingPanel | Stable | | MapIconTextPin | Stable | | MapMarker | Stable | | MapNavigationControl | Stable | | MapPopup | Stable | | MapSearchControl | Stable | | MapTooltip | In Progress | | Pictometry | Stable | | PictometryLayerControl | Stable | | RasterLayer | Stable | | SymbolLayer | Stable |

Vue 3 components for building map applications in Philadelphia city government apps. Built on MapLibre GL JS. Includes the core Map component, layer and marker components, map controls (navigation, drawing, search), and utility functions for working with Philadelphia data sources (AIS geocoding, parcel data).

What's in the package

  • Components: Map, layer components (FillLayer, LineLayer, CircleLayer, SymbolLayer, RasterLayer), MapMarker, and controls (MapNavigationControl, MapSearchControl, DrawTool, BasemapToggle, GeolocationButton, etc.)

Importing the Map component: The export name Map conflicts with JavaScript's built-in Map constructor. Alias it when importing:

import { Map as PhilaMap } from "@phila/phila-ui-map-core";

CSS: The package bundles MapLibre GL CSS so you only need a single import:

import "@phila/phila-ui-map-core/dist/assets/phila-ui-map-core.css";
  • Component Types: Props and emit interfaces for each component, located in types.ts files within each component folder
  • Composables: useMapControl - for building custom controls that need map access
  • Utils: Plain async functions and their TypeScript interfaces for Philadelphia data (geocodeAddress, queryParcelAtPoint, fetchParcelGeometry, etc.)
  • Defaults: basemap and imagery configuration objects

Running the examples

Example apps are in src/examples/. To run them:

pnpm dev

Then open:

  • http://localhost:3000/ - Example 1: Just a map, no controls
  • http://localhost:3000/example2.html - Example 2: Map with address search
  • http://localhost:3000/example3.html - Example 3: Parcel clicking, geocoding, basemap switching, and controls

Vue DevTools is available at http://localhost:3000/__devtools__/ in a separate window. The DevTools will reflect whichever example you currently have open in another tab. Running the examples (instead of viewing Storybook) lets you inspect all components and their state in Vue DevTools.

Architecture Decisions

No shared state

These components do not manage shared state. If your app needs shared state (e.g., a selected address used by multiple components), use your own state management like Pinia. Components communicate via events, and your app decides what to do with the results.

Components emit events

Components like MapSearchControl emit events (@result, @error) rather than writing to global state. The parent component handles the results however it needs to.

Utils vs Composables

Reusable functions are exported as either utils or composables:

  • Utils (utils/): Plain async functions with no Vue dependencies. Examples: geocodeAddress(), queryParcelAtPoint(), fetchParcelGeometry()
  • Composables (composables/): Functions that use Vue reactivity (refs, inject, provide, lifecycle hooks). Example: useMapControl()

If a function just fetches data and returns it, it's a util. If it needs Vue's reactivity system, it's a composable.

Data types live with their utility functions

Each util file exports both functions and their associated TypeScript interfaces:

utils/ais.ts

  • Functions: geocodeAddress()
  • Interfaces: AisGeocodeResult, AisResponse

utils/parcels.ts

  • Functions: queryParcelAtPoint(), fetchParcelGeometry()
  • Interfaces: ParcelQueryResult, ParcelFeatureCollection

This keeps related code together - if you need to fetch parcel data, import from utils/parcels and you get both the functions and the type definitions.

Configurable defaults

Basemap and imagery sources have sensible defaults for Philadelphia apps, but you can use any tile source. The defaults/imagery.ts exports let you customize or extend the built-in options.

Progressive complexity

Simple maps can be created using just props:

<Map
  :center="[-75.16, 39.95]"
  :zoom="17"
  basemap-url="https://tiles.arcgis.com/..."
  labels-url="https://tiles.arcgis.com/..."
/>

For more complex apps with dynamic layers, custom controls, or multiple data layers, use slots:

<Map :center="[-75.16, 39.95]" :zoom="17" :no-default-tile-layers="true">
  <RasterLayer id="basemap-labels" :source="labelsSource" before-id="basemap-ceiling" />
  <RasterLayer id="basemap" :source="basemapSource" before-id="basemap-labels" />
  <FillLayer id="parcels" :source="parcelSource" :paint="fillPaint" />
  <MapSearchControl position="top-left" @result="handleResult" />
  <MapNavigationControl position="bottom-left" />
</Map>

Most apps only need the props approach.

Pictometry (Eagleview oblique imagery)

The Map component includes optional Pictometry/Eagleview integration for viewing oblique aerial imagery. Enable it with props on the Map component:

<Map :enable-pictometry="true" :pictometry-credentials="credentials" pictometry-button-position="top-right">
  <!-- other layers and controls -->
</Map>

| Map Prop | Type | Default | Description | | -------------------------- | ----------------------- | ------------- | ----------------------------------- | | enablePictometry | boolean | false | Show the Pictometry toggle button | | pictometryCredentials | PictometryCredentials | — | OAuth2 { clientId, clientSecret } | | pictometryButtonPosition | MapControlPosition | "top-right" | Button placement on the map |

When the user clicks the Pictometry button, the Map component handles everything: loading the Eagleview SDK from CDN, authenticating, and mounting the viewer in a panel. The viewer opens at the current map center.

Events

PictometryPanel emits these events (forwarded through Map):

| Event | Payload | Description | | ------------- | ----------------------------------- | ------------------------------------------ | | ready | — | Viewer initialized successfully | | view-change | PictometryView | User panned, zoomed, or rotated the viewer | | error | { message: string; type: string } | Initialization or runtime error |

PictometryView contains { lonLat: { lon, lat }, zoom?, pitch?, rotation? }.

The Map component converts view-change events to its own move event, translating lon/lat to lng/lat for consistency with MapLibre conventions.

Layer controls

The viewer panel includes built-in controls for toggling overlay layers:

  • Parcels — US Parcels layer (property boundaries)
  • Street Labels — Street name labels overlaid on imagery

These appear at the bottom-left of the Pictometry panel when the viewer is initialized. Initial visibility is controlled via PictometryPanel props (showParcels, showLabels).

For custom layer toggling, use the usePictometry composable directly:

const { setLayerVisibility, setParcelsVisible, setLabelsVisible } = usePictometry();

// Toggle any layer by name
setLayerVisibility("US Parcels", true);

// Convenience methods
setParcelsVisible(true);
setLabelsVisible(false);

To build a custom layer control, use PictometryLayerControl:

<PictometryLayerControl
  layer-name="US Parcels"
  label="Show Parcels"
  :initial-state="false"
  @toggle="({ layerName, visible }) => setLayerVisibility(layerName, visible)"
/>

Viewer lifecycle

The Pictometry panel uses a v-show pattern to preserve viewer state across open/close cycles. On first open, the component mounts and initializes the Eagleview SDK. On close, the panel is hidden but the viewer stays mounted. On reopen, the panel is revealed without reinitializing — the user's navigation (pan, zoom, rotation) is preserved.

The viewer location is only set on first open (using the map center at click time). Subsequent reopens preserve the user's last position in the viewer.

SDK loading

The Eagleview Embedded Explorer SDK is loaded dynamically from CDN. It creates window.ev.EmbeddedExplorer when ready. The loader handles race conditions and waits for the SDK to fully initialize (not just script onload). Components that use Pictometry are lazy-loaded via defineAsyncComponent, so the SDK is never fetched unless enablePictometry is true.

Layer ordering with basemapBeforeId

When building apps with many feature layers, you may want to ensure basemap and imagery layers always stay below your feature layers, even when the basemap source changes (e.g., switching between street map and imagery). The basemapBeforeId prop controls this.

The problem: When a basemap layer's source changes, MapLibre removes and re-adds the layer. Without a before-id, the layer gets added to the top of the layer stack, covering your feature layers.

The solution: Pass the ID of one of your feature layers to basemapBeforeId. The basemap will always be inserted before that layer:

<Map basemap-before-id="parcels">
  <FillLayer id="parcels" :source="parcelSource" :paint="fillPaint" />
  <LineLayer id="roads" :source="roadSource" :paint="linePaint" />
</Map>

This keeps basemaps below all your feature layers, even when switching between street map and imagery.

For apps with all-dynamic layers: If all your feature layers can be toggled on/off, the Map component's built-in basemap-ceiling layer handles this automatically. The basemap ceiling is an invisible layer that always exists in the layer stack, ensuring basemaps stay below feature layers even when all feature layers are toggled off. No basemapBeforeId is needed unless you want to override the default behavior.

Typed layer components

There are separate components for each MapLibre layer type: FillLayer, LineLayer, CircleLayer, SymbolLayer, and RasterLayer. Each component has strongly-typed props matching the MapLibre specification for that layer type (e.g., FillLayerProps only accepts fill-specific paint properties). This provides better TypeScript support and clearer APIs than a single generic component.

Map instance injection

Child components that need access to the MapLibre map instance use Vue's provide/inject. The Map component provides the instance, and child components inject it. Examples:

  • useMapControl() - a composable that injects the map instance for building custom controls
  • Layer components (FillLayer, RasterLayer, etc.) - inject the map to call map.addLayer(), map.addSource(), etc.

Importantly, all components that use the injected map instance are designed to keep their state visible in Vue DevTools. For example, the DrawTool adds layers to the map for drawing states, but it does this by rendering layer components (which appear in the Vue component tree) rather than calling map.addLayer() directly (which would be invisible to Vue DevTools).

Common Patterns

Icon-based SymbolLayer (simple mode)

When displaying a single icon type, pass the icon and iconName props. The component handles loadImage, addImage, and cleanup automatically.

<SymbolLayer
  id="address-marker"
  :source="{
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: [
        {
          type: 'Feature',
          geometry: { type: 'Point', coordinates: [-75.1636, 39.9523] },
          properties: { name: 'City Hall' },
        },
      ],
    },
  }"
  icon="/images/marker_blue.png"
  icon-name="marker-blue"
  :layout="{
    'icon-anchor': 'bottom',
    'icon-size': 0.05,
    'icon-allow-overlap': true,
  }"
/>

When the icon prop is provided, SymbolLayer loads the image, registers it under iconName, injects icon-image into layout, and cleans up on unmount.

Icon-based SymbolLayer (data-driven)

When you need different icons based on feature properties, register images yourself and pass a match expression in layout. Do not use the icon prop in this case.

<script setup>
import { inject, watch } from "vue";

const map = inject("map");
const isLoaded = inject("isLoaded");

watch(isLoaded, async loaded => {
  if (!loaded) return;
  const img1 = await map.value.loadImage("/images/red-school.png");
  map.value.addImage("red-school", img1.data);
  const img2 = await map.value.loadImage("/images/blue-school.png");
  map.value.addImage("blue-school", img2.data);
});
</script>

<template>
  <SymbolLayer
    id="school-markers"
    :source="schoolsSource"
    :layout="{
      'icon-image': [
        'match',
        ['get', 'GRADE_LEVEL'],
        'ELEMENTARY SCHOOL',
        'red-school',
        'HIGH SCHOOL',
        'blue-school',
        'red-school',
      ],
      'icon-size': 0.05,
    }"
  />
</template>

Polygon with styled outline

Pass the same source object to both a FillLayer and a LineLayer. Each creates its own MapLibre source (named after its id), so there is no conflict. This is preferred over sharing a source by string ID because each component has an independent lifecycle with no unmount ordering issues.

<script setup>
const districtSource = {
  type: "geojson",
  data: "https://services.arcgis.com/.../query?where=1=1&outFields=*&f=geojson",
};
</script>

<template>
  <FillLayer id="districts-fill" :source="districtSource" :paint="{ 'fill-color': '#1976D2', 'fill-opacity': 0.2 }" />
  <LineLayer id="districts-outline" :source="districtSource" :paint="{ 'line-color': '#1976D2', 'line-width': 3 }" />
</template>

Reactive data updates

When the source prop changes, the component detects the update. For GeoJSON sources, it uses setData() for efficiency rather than removing and re-adding the layer.

<script setup>
import { ref } from "vue";

const source = ref({
  type: "geojson",
  data: { type: "FeatureCollection", features: [] },
});

async function loadData() {
  const response = await fetch("https://api.example.com/points");
  const geojson = await response.json();
  source.value = { type: "geojson", data: geojson };
}
</script>

<template>
  <CircleLayer id="points" :source="source" :paint="{ 'circle-radius': 6, 'circle-color': '#1976D2' }" />
</template>

WMS raster layers

WMS endpoints work through RasterLayer by using the {bbox-epsg-3857} placeholder in the tile URL. MapLibre substitutes the current tile's bounding box. Use tileSize: 1024 for better image quality from WMS.

<RasterLayer
  id="zoning"
  :source="{
    type: 'raster',
    tiles: [
      'https://citygeo-geoserver.databridge.phila.gov/geoserver/wms?service=WMS&version=1.1.1&request=GetMap&layers=zoning_basedistricts&styles=&bbox={bbox-epsg-3857}&width=1024&height=1024&srs=EPSG:3857&format=image/png&transparent=true',
    ],
    tileSize: 1024,
  }"
/>