Bidirectional Data Sync
Bidirectional data synchronization is a core feature of AurOpenlayers. It allows you to maintain a "Single Source of Truth" by automatically linking your TypeScript business models with OpenLayers geometries.
When you move a point on the map, your model updates. When you update your model in code, the map reflects that change instantly.
How It Works
The synchronization is handled by two functions within the geometry property of a VectorLayerDescriptor:
fromModel: Defines how to create an OpenLayers Geometry from your business data.applyGeometryToModel: Defines how to update your business data when the geometry changes (e.g., after a drag or modify interaction).
Step 1: Define Your Model
First, ensure your data model contains the necessary coordinates. In this example, we use a simple MapPoint.
interface MapPoint {
id: string;
lat: number;
lng: number;
name: string;
}
Step 2: Configure the Layer Descriptor
In your layer configuration, implement the transformation logic. AurOpenlayers uses these functions to bridge the gap between your application state and the map engine.
import { fromLonLat, toLonLat } from 'ol/proj';
import { Point } from 'ol/geom';
const pointLayer: VectorLayerDescriptor<MapPoint, Point> = {
id: 'points-layer',
feature: {
id: (model) => model.id,
geometry: {
// 1. Model -> Map
fromModel: (model) => new Point(fromLonLat([model.lng, model.lat])),
// 2. Map -> Model
applyGeometryToModel: (prev, geom) => {
const [lng, lat] = toLonLat(geom.getCoordinates());
// Return a new object (immutability is recommended)
return { ...prev, lat, lng };
}
},
// ... styles and interactions
}
};
Step 3: Enable Interactions
For the map to trigger applyGeometryToModel, you must enable an interaction like translate (dragging) or modify.
// Inside your feature descriptor
interactions: {
translate: {
enabled: true,
state: 'DRAG' // Optional style state while dragging
}
}
Step 4: React to Changes in the Component
When a user moves a feature, AurOpenlayers calculates the new model and emits a change event. You can listen to these changes via the VectorLayerApi to update your backend or application state.
export class MyMapComponent {
onReady(ctx: MapContext) {
const layerApi = ctx.layers['points-layer'];
// Listen for changes initiated by the map (translate/modify)
layerApi.onModelsChanged?.((changes) => {
changes.forEach(({ prev, next, reason }) => {
console.log(`Point ${next.id} moved via ${reason}`);
this.saveToDatabase(next);
});
});
}
}
Handling Complex Geometries
The same logic applies to lines and polygons. For example, a route line consists of multiple points.
Synchronizing a LineString
geometry: {
fromModel: (route: RouteLine) =>
new LineString(route.points.map(p => fromLonLat([p.lng, p.lat]))),
applyGeometryToModel: (prev) => {
// Usually, lines are updated when their individual points move.
// If the line itself is modified, update the points array here.
return prev;
}
}
Key Benefits
- Immutability:
applyGeometryToModelreceives the previous model and returns a new one, fitting perfectly with Angular'sChangeDetectionStrategy.OnPush. - Decoupling: Your business logic doesn't need to know about
ol/Featureorol/geom/Point. It only interacts with plain objects. - Automatic Batching: Multiple changes occurring in the same frame (like a high-speed drag) are batched to ensure optimal performance.