Tutorial: Building a Route Editor
This tutorial demonstrates how to build an interactive Route Editor using AurOpenlayers. You will learn how to visualize a sequence of points connected by a line, implement drag-and-drop point relocation, and sync map changes back to your business logic.
Overview
A route editor typically consists of two layers:
- Route Line: A polyline representing the path.
- Route Points: Interactive markers that users can select or drag.
The power of AurOpenlayers lies in the bidirectional synchronization: moving a point on the map automatically updates your domain model.
Step 1: Define Your Data Models
First, define the structures for your route. Unlike standard OpenLayers, AurOpenlayers works directly with your classes or interfaces.
export interface RoutePoint {
id: string;
name: string;
lat: number;
lng: number;
orderIndex: number;
}
export interface RouteLine {
id: string;
points: RoutePoint[];
}
Step 2: Configure the Route Line Layer
The line layer is "passive"—it reacts to changes in the points. Its geometry is derived from the array of points in the RouteLine model.
const routeLineLayer: VectorLayerDescriptor<RouteLine, LineString, any> = {
id: 'route-line',
feature: {
id: (model) => model.id,
geometry: {
// Convert the array of points into an OpenLayers LineString
fromModel: (model) =>
new LineString(model.points.map((p) => fromLonLat([p.lng, p.lat]))),
// The line itself isn't edited directly, so return the previous model
applyGeometryToModel: (prev) => prev,
},
style: {
base: () => ({ color: '#38bdf8', width: 4 }),
render: (opts) => new Style({
stroke: new Stroke({ color: opts.color, width: opts.width })
}),
},
},
};
Step 3: Configure the Interactive Points Layer
This layer handles the heavy lifting: rendering the markers and managing the translate (drag) interaction.
Geometry Sync
The applyGeometryToModel function is critical. It converts the new OpenLayers coordinates back into your RoutePoint format when a user finishes dragging.
const pointsLayer: VectorLayerDescriptor<RoutePoint, Point, any> = {
id: 'points',
feature: {
id: (model) => model.id,
geometry: {
fromModel: (model) => new Point(fromLonLat([model.lng, model.lat])),
applyGeometryToModel: (prev, geom) => {
if (!(geom instanceof Point)) return prev;
const [lng, lat] = toLonLat(geom.getCoordinates());
return { ...prev, lng, lat }; // Returns a new immutable model
},
},
// ... styles and interactions
}
};
Interactions and States
Use the interactions property to enable dragging. You can also define visual states (like DRAG or SELECTED) to provide user feedback.
interactions: {
translate: {
cursor: 'grab',
state: 'DRAG', // Automatically applies the DRAG style when moving
onStart: () => { console.log('Dragging started'); return true; },
onEnd: () => { console.log('Dragging ended'); return true; },
},
select: {
state: 'SELECTED',
}
},
style: {
base: (model) => ({
color: '#2563eb',
radius: 12,
label: String(model.orderIndex),
}),
states: {
SELECTED: () => ({ color: '#f97316', radius: 14 }),
DRAG: () => ({ color: '#16a34a', radius: 14 }),
},
render: (opts) => new Style({
image: new CircleStyle({
radius: opts.radius,
fill: new Fill({ color: opts.color }),
stroke: new Stroke({ color: '#ffffff', width: 2 }),
}),
// ... text/label configuration
})
}
Step 4: Initializing the Map
In your Angular component, use MapHostConfig to assemble the schema and provide it to the <mff-map-host> component.
readonly mapConfig: MapHostConfig = {
schema: {
layers: [routeLineLayer, pointsLayer]
},
view: { centerLonLat: [27.56, 53.90], zoom: 11 },
osm: true
};
Step 5: Handling Model Updates
When a point is dragged, the map engine triggers the onModelsChanged hook. You should use this to update your application state and refresh the line layer so it follows the moved point.
onReady(ctx: MapContext): void {
const pointApi = ctx.layers['points'];
const lineApi = ctx.layers['route-line'];
// Initialize data
pointApi.setModels(this.myPoints);
lineApi.setModels([{ id: 'route-1', points: this.myPoints }]);
// Listen for drag updates
pointApi.onModelsChanged?.((changes) => {
// 1. Update local business logic state
this.myPoints = pointApi.getAllModels();
// 2. Refresh the line layer to reflect new coordinates
lineApi.setModels([{ id: 'route-1', points: this.myPoints }]);
});
}
Pro Tip: Restricting Dragging
If you want to allow editing only for a specific point (e.g., when a user clicks an "Edit" button in a list), use the pickTarget property in the translate interaction:
translate: {
// Only allow dragging if the point matches our editing ID
pickTarget: ({ candidates }) =>
candidates.find((c) => c.model.id === this.editingPointId),
}
This ensures that even if the user attempts to drag other markers, only the active one will respond.