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 and Vue 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 the defaultProps 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:

  1. entity: Reference to the entity to which the DOM element is being linked to.
  2. element: DOM element the entity is being linked to.
  3. 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
Previous