Map Layer Registry¶
The Map Layer Registry allows external NetBox plugins to display their data on the Pathways interactive map. This enables cross-plugin visualization — for example, a fiber management plugin can show splice points and cable routes on the infrastructure map.
Overview¶
The registry is a Python-side singleton populated during Django's ready() phase. Registered layers are serialized to JSON configuration that the map's JavaScript consumes to fetch, render, and style external data.
sequenceDiagram
participant Plugin as External Plugin
participant Registry as MapLayerRegistry
participant View as MapView
participant JS as Map JavaScript
participant API as GeoJSON API
Plugin->>Registry: register_map_layer() in ready()
View->>Registry: registry.all()
View->>JS: JSON config (externalLayers)
JS->>API: Fetch GeoJSON per layer
API-->>JS: FeatureCollection
JS->>JS: Render on map
Geometry Modes¶
URL Mode¶
The plugin provides its own GeoJSON endpoint. Pathways fetches from the URL and renders the features.
from netbox_pathways.registry import register_map_layer, LayerStyle
register_map_layer(
name='fiber-cables',
label='Fiber Cables',
geometry_type='LineString',
source='url',
url='/api/plugins/netbox-fms/geo/fiber-cables/',
style=LayerStyle(color='#e91e63'),
)
Reference Mode¶
The plugin's model has a foreign key to a Pathways model (Structure or SiteGeometry). Pathways resolves the geometry automatically and serves GeoJSON at /api/plugins/pathways/geo/external/<layer_name>/.
from netbox_pathways.registry import (
register_map_layer,
LayerDetail,
LayerStyle,
)
register_map_layer(
name='splice-points',
label='Splice Points',
geometry_type='Point',
source='reference',
queryset=lambda request: SplicePoint.objects.filter(
structure__site__in=request.user.get_sites()
),
geometry_field='structure', # FK to netbox_pathways.Structure
feature_fields=['name', 'splice_type', 'fiber_count', 'status'],
style=LayerStyle(
color_field='splice_type',
color_map={
'fusion': '#4caf50',
'mechanical': '#ff9800',
'connectorized': '#2196f3',
},
default_color='#9e9e9e',
),
detail=LayerDetail(
url_template='/plugins/netbox-fms/splice-points/{id}/',
fields=['name', 'splice_type', 'fiber_count', 'status', 'structure'],
),
)
Supported FK Targets¶
Reference mode resolves geometry through foreign keys to these models:
| Model | Geometry Lookup | Geometry Type |
|---|---|---|
netbox_pathways.Structure |
geometry |
Point/Polygon |
netbox_pathways.SiteGeometry |
geometry |
Point/Polygon |
dcim.Site |
pathways_geometry__geometry |
Point/Polygon |
dcim.Location |
pathways_structure__geometry |
Point/Polygon |
The geometry_field parameter names the FK field on your model that points to one of these targets.
The dcim.Site entry uses a two-hop ORM lookup: it traverses the reverse OneToOne from Site to SiteGeometry, then accesses the geometry field. This means any model with a site FK can be registered as a map layer — as long as a SiteGeometry record exists for that site.
The dcim.Location entry resolves through the identity link on
Structure.location: a structure that physically is a Location (a handhole
or vault modelled as a dcim.Location so devices can be placed in it) carries
the geometry. A Location with no identity structure resolves to NULL, and the
feature is dropped from the layer -- declare a fallback (below) to degrade to
the site centroid instead.
Ordered Geometry Fallbacks¶
geometry_field accepts a tuple of FK field names, tried in order via SQL
COALESCE:
An object whose location has an identity structure renders at that
structure's geometry; one whose location is a plain room degrades to the site
centroid instead of vanishing. Both the GeoJSON endpoint and the /info
feature counts resolve the tuple identically.
Malformed tuples (empty, or containing non-string entries) fail at
registration time with ValueError. Entries are resolved against the layer's
queryset model at request time, since the queryset is a request-bound
callable. Older netbox-pathways releases accept a tuple at registration and
fail at request time; feature-detect with:
from netbox_pathways.registry import SUPPORTED_GEO_MODELS
if "dcim.Location" in SUPPORTED_GEO_MODELS:
geometry_field = ("location", "site")
else:
geometry_field = "site"
Registration API¶
register_map_layer(**kwargs)¶
Register a layer on the Pathways map.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
str |
Yes | Unique layer identifier |
label |
str |
Yes | Display label in layer control |
geometry_type |
str |
Yes | 'Point', 'LineString', or 'Polygon' |
source |
str |
Yes | 'url' or 'reference' |
url |
str |
URL mode | GeoJSON endpoint URL |
queryset |
Callable |
Reference mode | (request) -> QuerySet |
geometry_field |
str or tuple[str, ...] |
Reference mode | FK field name to a geo model, or an ordered fallback tuple resolved to the first non-NULL geometry |
feature_fields |
list[str] |
No | Fields to include in GeoJSON properties |
style |
LayerStyle |
No | Visual styling configuration |
detail |
LayerDetail |
No | Sidebar detail panel configuration |
popover_fields |
list[str] |
No | Fields shown on hover (default: ['name']) |
default_visible |
bool |
No | Show layer by default (default: False) |
group |
str |
No | Group label for layer control |
min_zoom |
int |
No | Minimum zoom to fetch data (default: 11) |
max_zoom |
int |
No | Maximum zoom to fetch data (default: None) |
max_features |
int |
No | Reference-mode only: in-viewport count above which the layer is hidden and its toggle dimmed (default: 500) |
sort_order |
int |
No | Layer ordering (default: 100) |
Reference-mode vs URL-mode density gating. Reference-mode layers participate in the
/infocount-based gating described in Layer density gating; they report their count alongside the native layers and respect bothmax_featuresand the global "structures clustered -> hide supports" rule. URL-mode layers cannot be counted server-side (the queryset lives in the external plugin), so they keep the legacymin_zoom/max_zoomgate only. If you need count-based gating for a URL-mode layer, expose a counterpart reference-mode registration in your plugin.
unregister_map_layer(name)¶
Remove a previously registered layer.
LayerStyle¶
| Parameter | Type | Default | Description |
|---|---|---|---|
color |
str |
'#3388ff' |
Single color for all features |
color_field |
str |
None |
Property field for categorical coloring |
color_map |
dict |
None |
{field_value: color} mapping |
default_color |
str |
'#999999' |
Fallback when value not in color_map |
icon |
str |
None |
MDI icon class (future use) |
dash |
str |
None |
SVG dash-array for lines |
weight |
int |
3 |
Line weight in pixels |
opacity |
float |
0.8 |
Feature opacity |
Static styling (single color for all features):
Categorical styling (color based on a property value):
LayerStyle(
color_field='status',
color_map={
'active': '#4caf50',
'planned': '#ff9800',
'decommissioned': '#f44336',
},
default_color='#9e9e9e',
)
LayerDetail¶
| Parameter | Type | Default | Description |
|---|---|---|---|
url_template |
str |
'' |
Object detail page URL with {id} placeholder (rendered as the sidebar's "View Details" link) |
detail_url |
str |
'' |
HTML fragment endpoint with {id} placeholder (sidebar injects HTML directly) |
fields |
list[str] |
[] |
Fields to show in detail panel (JSON enrichment mode) |
label_field |
str |
'name' |
Field used as the detail panel title |
The sidebar resolves detail content in this order:
detail_url— Fetch HTML fragment, inject directly (richest option)- Feature
urlproperty — If the GeoJSON feature carries aurlproperty pointing at a REST endpoint, fetch JSON and render a field table from thefieldslist - Neither — Render raw GeoJSON properties as key-value table
url_template is independent of that fallback: whenever it is set, the
detail panel shows a "View Details" button linking to the object's page.
HTML fragment mode lets plugins render domain-specific content (fiber tube diagrams, splice schematics, status indicators). The endpoint must return a self-contained HTML fragment using Tabler/NetBox CSS variables for theme compatibility.
Integration Example¶
In your plugin's PluginConfig:
# netbox_fms/__init__.py
from netbox.plugins import PluginConfig
class FMSConfig(PluginConfig):
name = 'netbox_fms'
...
def ready(self):
super().ready()
from netbox_pathways.registry import (
register_map_layer,
LayerDetail,
LayerStyle,
)
from netbox_fms.models import FiberCable, SplicePoint
register_map_layer(
name='fiber-cables',
label='Fiber Cables',
geometry_type='LineString',
source='reference',
queryset=lambda request: FiberCable.objects.all(),
geometry_field='pathway',
feature_fields=['name', 'cable_type', 'fiber_count', 'status'],
style=LayerStyle(
color_field='cable_type',
color_map={
'single_mode': '#2196f3',
'multi_mode': '#ff9800',
},
default_color='#9e9e9e',
weight=4,
),
detail=LayerDetail(
url_template='/plugins/netbox-fms/fiber-cables/{id}/',
detail_url='/api/plugins/netbox-fms/fiber-cables/{id}/card/',
fields=['name', 'cable_type', 'fiber_count', 'status'],
),
popover_fields=['name', 'cable_type'],
min_zoom=13,
sort_order=50,
)
register_map_layer(
name='splice-points',
label='Splice Points',
geometry_type='Point',
source='reference',
queryset=lambda request: SplicePoint.objects.all(),
geometry_field='structure',
feature_fields=['name', 'splice_type', 'fiber_count'],
style=LayerStyle(
color_field='splice_type',
color_map={
'fusion': '#4caf50',
'mechanical': '#ff9800',
},
default_color='#9e9e9e',
),
detail=LayerDetail(
url_template='/plugins/netbox-fms/splice-points/{id}/',
fields=['name', 'splice_type', 'fiber_count', 'status'],
),
sort_order=51,
)
Map Behavior¶
Rendering¶
- Points render as circle markers with the configured color
- Lines render as polylines with color, weight, dash, and opacity
- Polygons render as filled polygons with 20% fill opacity
Zoom Filtering¶
Layers only fetch data when the map zoom is between min_zoom and max_zoom. This prevents overloading the map at low zoom levels.
Layer density gating¶
In addition to zoom filtering, the map fetches a one-shot /api/plugins/pathways/geo/info/ on every pan/zoom and uses the returned counts to decide whether each layer should render plain, with client clustering, or be hidden entirely. For reference-mode external layers, this is driven by the max_features registration field (default 500): when the in-viewport count for that layer exceeds max_features, the toggle dims and shows a count chip instead of fetching the GeoJSON. Native structures also drive a global "supports off" rule: when the structures layer is clustered (either client-cluster or server-cluster, decided from the structures thresholds), every other layer (native pathways and reference-mode externals alike) is suppressed for that viewport. The defaults and override mechanism are documented in the user-facing map widget guide. URL-mode external layers are not gated this way -- they keep their min_zoom / max_zoom window only.
Layer Control¶
Registered layers appear as toggle buttons in the map's layer control. The label is used as the button text, and default_visible determines the initial state.
Sidebar Integration¶
Clicking an external feature opens the sidebar detail panel. The sidebar resolves content using a three-tier fallback:
detail.detail_url— Fetches an HTML fragment from the plugin and injects it directly. This enables rich, domain-specific content like fiber tube diagrams or splice schematics. The endpoint must return a self-contained HTML fragment (no<html>/<body>wrappers) styled with Tabler/NetBox CSS variables.- Feature
urlproperty — If the GeoJSON feature carries aurlproperty pointing at a REST endpoint, fetches JSON and renders a field table using thefieldslist. - Neither — Renders raw GeoJSON properties as a key-value table.
Independently, detail.url_template renders a "View Details" button
linking to the object's page.
HTML responses are cached per feature to avoid redundant fetches.
Hover Popover¶
Hovering shows a popover with fields specified in popover_fields (defaults to ['name']).
Validation¶
The registry validates registrations and raises ValueError for:
- Duplicate layer names
- Invalid
geometry_type(must be Point, LineString, or Polygon) - Invalid
source(must be url or reference) - Missing
urlfor URL mode - Missing
querysetorgeometry_fieldfor reference mode color_fieldnot present infeature_fields
Permissions¶
- URL mode: The JavaScript fetch includes the session CSRF cookie. The external endpoint must handle its own authentication.
- Reference mode: The
querysetcallable receives the Djangorequestobject, so you can filter by user permissions, site access, or any other criteria.