Entity system
A map in the JS API can be represented as a collection of different objects combined into a common tree-like structure. Different types of these objects interact with each other to form an entity system.
This section describes this system, its entity types, their features and purpose.
The entity system is an imperative part of the Mappable Maps JS API.
Base entities
Base entities are abstract classes, and tree components are created by inheriting from them.
There are the following entity types:
- MMapEntity: Base entity.
- MMapComplexEntity: Entity that can have its own subtree, but does not have a public interface to interact with it.
- MMapGroupEntity: Similar to MMapComplex Entity, but has a public interface to interact with the entity subtree.
- RootEntity : Root entity that cannot be a part of the subtree of another entity. Similarly to Group Entity, it has a public interface to manage its own subtree.
- MMapCollection: Collection of map entity objects.
- MMapContext: Creates a context that components can provide or read.
- MMapContainer: Passes the necessary parameters to
React
andVue
library components.
Examples of correspondences to the mappable module entity types:
- RootEntity – MMap;
- MMapEntity – MMapTileDataSource, MMapFeatureDataSource, MMapLayer, MMapListener, MMapFeature;
- MMapComplexEntity – MMapDefaultSchemeLayer, MMapDefaultFeaturesLayer;
- MMapGroupEntity – MMapControls, MMapControl, MMapMarker;
- MMapCollection – MMapControls, MMapControl, MMapMarker.
For more information about each entity class, see the sections:
Root Entity
This is the root entity of the tree, meaning it cannot be added to an external subtree as a child element.
Apart from other entity types, you do not need to write a custom implementation of the root entity. For that, the mappable module has the MMap
class that must be used to create a tree and is the root element for MMapEntity
, MMapComplexEntity
, and MMapGroupEntity
.
Warning
To determine custom entities, we recommend that you only use Inheritance from MMapEntity
, MMapComplexEntity
, and MMapGroupEntity
. Backward compatibility is not guaranteed when inheriting from other classes.
Default entity parameters
An entity can have optional parameters, but you can set default values for them. There are two ways to do this:
- If the default value is static, use the
static
field of thedefaultProps
class. - If the default value is calculated dynamically, use the
protected
_getDefaultProps
method.
For example, an entity has an optional name
parameter, but we want to set the default value to 'some-entity'
if it is not explicitly specified.
Example for a static default value of the name
parameter:
type MMapSomeEntityProps = {
name?: string;
};
const defaultProps = Object.freeze({name: 'some-entity'});
type DefaultProps = typeof defaultProps;
class MMapSomeEntity extends mappable.MMapEntity<MMapSomeEntityProps, DefaultProps> {
static defaultProps = defaultProps;
}
A similar example, but for a dynamically calculated default value:
type MMapSomeEntityProps = {
id?: string;
name?: string;
};
type DefaultProps = {name: string};
class MMapSomeEntity extends mappable.MMapGroupEntity<MMapSomeEntityProps, DefaultProps> {
private static _uid = 0; // entity counter
protected _getDefaultProps(props: MMapSomeEntityProps): DefaultProps {
const id = props.name !== undefined ? `id-${props.name}` : `id-${MMapSomeEntity._uid++}_auto`;
return {id};
}
}
Note
To make optional parameters with a default value not optional in TypeScript, you must specify the type of the default value in the generic class.
For that, we created the DefaultProps
type and passed it to the second generic.
Linking an entity to the DOM
Entities are not linked to the DOM tree, because they are all located in a separate virtual tree. To link an entity to a DOM element while preserving the order and nesting of entities, use the useDomContext
hook:
class MMapDOMEntity extends mappable.MMapGroupEntity<{text: string}> {
private _element?: HTMLHeadingElement;
private _container?: HTMLDivElement;
private _detachDom?: () => void;
protected _onAttach(): void {
// Create a DOM element that will be linked to the entity.
this._element = document.createElement('div');
this._element.textContent = this._props.text;
this._element.classList.add('element');
// Create a container for the DOM elements of child entities.
this._container = document.createElement('div');
this._container.classList.add('container');
// Insert the container inside the element.
this._element.appendChild(this._container);
// Create entity linking to the DOM.
this._detachDom = mappable.useDomContext(this, this._element, this._container);
}
protected _onDetach(): void {
// Detach the DOM from the entity and remove references to the elements.
this._detachDom?.();
this._detachDom = undefined;
this._element = undefined;
this._container = undefined;
}
}
// Initializing the root element.
map = new mappable.MMap(document.getElementById('app'), {location: LOCATION});
// Initialize the MMapDOMEntity class entities.
const DOMEntity = new MMapDOMEntity({text: 'DOM Entity'});
const childDOMEntity = new MMapDOMEntity({text: 'Child DOM Entity'});
// DOMEntity._element will be inserted into the map's DOM element.
map.addChild(DOMEntity);
// childDOMEntity._element will be added to DOMEntity._container.
DOMEntity.addChild(childDOMEntity);
.element {
position: absolute;
top: 0;
width: 100%;
}
.container {
position: absolute;
top: 24px;
width: 100%;
}
This hook is available only when inheriting from the MMapComplexEntity
and MMapGroupEntity
classes, since it is implied that an entity can have child elements. The hook takes three arguments:
entity
: Reference to the entity to which the DOM element is being linked to.element
: DOM element the entity is being linked to.container
: DOM element into which the DOM elements of child entities will be inserted (if an entity does not imply the addition of child elements,null
is passed).
The useDomContext
hook returns a function that removes the link to the DOM element.
Proxy container
Proxy containers can be determined in the classes derived from MMapComplexEntity
and MMapGroupEntity
via a constructor by passing options.container
as a second argument.
You should first study some of the internal properties and mechanisms of these classes without using a proxy container.
The class includes a _childContainer
property. By default, this is a reference to the current instance (this
). It was mentioned earlier that the children
property returns an array of child elements. But In fact, child elements of an entity are stored in a closed class property, and children
is just an accessor property to it.
In addition to the already known addChild
method, there is a protected _addDirectChild
method (similar to removeChild
and _removeDirectChild
), which adds a child entity directly to the internal subtree. This is how it differs from addChild
, which adds an entity inside _childContainer
.
So all child entities are available via the children
property, and you can remove them from the subtree. This can cause problems if there is a child element that an external user shouldn't be able to access:
import type {LngLat, MMapFeature} from '@mappable-world/mappable-types';
type MMapSomeProps = {
coordinates: LngLat;
};
class MMapWithoutContainerEntity extends mappable.MMapGroupEntity<MMapSomeProps> {
private _feature: MMapFeature;
constructor(props: MMapSomeProps) {
super(props); // MMapWithoutContainerEntity does not have a proxy container.
this._feature = new mappable.MMapFeature({
geometry: {
type: 'Point',
coordinates: this._props.coordinates
}
});
this.addChild(this._feature); // Add _feature to the internal subtree.
// this._addDirectChild(this._feature); — similar action.
}
private _removeFeature() {
this.removeChild(this._feature); // Remove _feature from the internal subtree.
// this._removeDirectChild(this._feature); — similar action.
}
}
const withoutContainerEntity = new MMapWithoutContainerEntity({coordinates: [0, 0]});
const [feature] = withoutContainerEntity.children; // Get access to withoutContainerEntity._feature.
withoutContainerEntity.removeChild(feature); // You can remove the closed entity from the subtree.
Now let's see how the mechanism for adding child entities changes when we use a proxy container.
import type {LngLat, MMapFeature} from '@mappable-world/mappable-types';
type MMapSomeProps = {
coordinates: LngLat;
};
class MMapWithContainerEntity extends mappable.MMapGroupEntity<MMapSomeProps> {
private _feature: MMapFeature;
constructor(props: MMapSomeProps) {
super(props, {container: true}); // MMapWithContainerEntity has a proxy container.
this._feature = new mappable.MMapFeature({
geometry: {
type: 'Point',
coordinates: this._props.coordinates
}
});
// this.addChild(this._feature); — adds _feature to the open subtree inside this._childContainer.
this._addDirectChild(this._feature); // Add _feature to the internal closed subtree.
}
private _removeFeature() {
// this.removeChild(this._feature); — removes _feature from the open subtree inside this._childContainer.
this._removeDirectChild(this._feature); // Remove _feature from the internal closed subtree.
}
}
const withContainerEntity = new MMapWithContainerEntity({coordinates: [0, 0]});
const publicFeature = mappable.MMapFeature({
geometry: {type: 'Point', coordinates: [1, 1]}
});
withContainerEntity.addChild(publicFeature);
// You can get only publicFeature, because withContainerEntity._feature is in the closed subtree.
const [feature] = withContainerEntity.children;
withoutContainerEntity.removeChild(feature); // You can remove publicFeature from the subtree.
First, _childContainer
now contains a proxy container — a child entity similar to a class instance. Second, children
now returns an internal subtree inside _childContainer
, not inside a class instance itself. This enables the entity to have two subtrees: open and closed.
An internal subtree of the class instance is closed, because it is not accessible and stores _childContainer
and entities added via _addDirectChild
(which you can remove only with _removeDirectChild
).
The internal subtree of _childContainer
is open, because it can be accessed via the public children
property and stores entities added via addChild
(which you can remove only with removeChild
).
Let's compare the differences between entities with and without a proxy container:
Without proxy container | With proxy container | |
---|---|---|
_childContainer |
Refers to the class instance (this ) |
Refers to the proxy container |
children |
Returns the internal subtree of the entity | Returns the internal subtree of the proxy container |
addChild |
Adds child elements to the internal subtree of the entity | Adds child elements to the internal subtree of the proxy container |
_addDirectChild |
Adds child elements to the internal subtree of the entity | Adds child elements to the internal subtree of the entity |
removeChild |
Removes child elements from the internal subtree of the entity | Removes child elements from the internal subtree of the proxy container |
_removeDirectChild |
Removes child elements from the internal subtree of the entity | Removes child elements from the internal subtree of the entity |