Classes and inheritance

In Mapspace API classes are created using the OpenLayers 4 pattern that follows the Google Closure pattern.

Any class inherits from ol.Object. This is the parent class for all classes and provides any class with methods to define a new type of properties called observables and to trigger events.

Inheritance

Inheritance is provided through the use of ol.inherits. This is a static function provided by OpenLayers that adds inheritance to a class. For example in the code below we are creating a new class called myClass and we are inheriting it from ol.Object:

myClass = function(options) {
    ol.Object.call(this, options);
    //Code of constructor goes here
};
ol.inherits(myClass, ol.Object);

The ol.inherits method has a pair of parameters, the first the name of the derived class, the second the parent class. Once called all the methods and properties of the parent class will be available in any instance of the derived class, and methods and properties with the same name in the derived class will overwrite existing ones in the parent class. Inheritance can be added to any derived class creating an unlimited tree of descendants in a hierachy.

Instanciation

To create an instance we use common JavaScript new operator:

var myObject = new myClass();

To check if a given instance is of a given class type, we can use common Javascript instanceof comparer.

var myObject = new myClass();
console.log(myObject instanceof myClass); // Returns true
console.log(myObject instanceof ol.Object); // Returns true

Usually Mapspace API follows the convention to offer only one optional parameter options in the constructor of a class. This means that any class can be instantiated with no parameters at all.

var myObject = new myClass();

To achieve this usually all the classes in the API follow the same pattern as OpenLayers, creating a default options object when it comes undefined.

myClass = function(options) {
    //Creation of a default options object
    options = options || {};
    ol.Object.call(this, options);

    //Code of the rest of constructor goes here
};
ol.inherits(myClass, ol.Object);

If documentation do not says the contrary the options parameter is a simple object containing the diferent initialization options as values. This options parameters are always documented in the API as Typedefs.

This way the next three initializations are equivalent:

var myObject = new myClass();

var myObject = new myClass({});

var myObject = new myClass({
    zoomLevels: undefined
});

Initially any expected option is undefined unless specifically set. All parameters usually have a default value when it is not set. The rare occassions where a parameter is needed are defined in the documentation of the typedefs that defines the option parameter.

Destruction

To destroy an instance, ol.Object provides a dispose method.

var myObject = new myClass();

myObject.dispose();

The dispose method is derived by ol.Object from ol.Disposable and what it does is to ensure that disposal occurs only once and call the disposeInternal function where the actual disposing actions take place.

The dispose method is defined as follows:

ol.Disposable.prototype.dispose = function() {
  if (!this.disposed_) {
    this.disposed_ = true;
    this.disposeInternal();
  }
};

Now it is up to the derived classes to implement the disposeInternal functions where dispose references. In Javascript a reference is marked for removal to the Garbage Collector setting it to null value.

The following example of the myClass class uses an internal object to store some oblique metadata, an array of arrays of numbers, and a typedef called Mapspace.Extent (that is defined as a simple array of numbers) to store an extent. This example bring us a good brief of how to dispose different content.

myClass = function(options) {
    //Creation of a default options object
    options = options || {};
    ol.Object.call(this, options);

    /**
     * @type {number}
     */
    this.zoomLevels = options.zoomLevels;

    /**
     * @type {Mapspace.ObliqueMetadata}
     */
    this.metadata = options.metadata;

    /**
     * @type {Array.<number[]>}
     */
    this.resolutions = [0.0, 0.4, 0.8, 1.6];

    /**
     * @type {Mapspace.Extent}
     */
    this.extent = null;

    /**
     * @type {ol.Feature}
     */
    this.feature = new ol.Feature();
};
ol.inherits(myClass, ol.Object);

In JavaScript any variable can contain primitive values (numbers, strings, booleans) or reference values (objects, arrays, functions, dates). Primitive values are always a copy of the value when passed through parameters, but reference values are always a reference to the passed parameters. So for primitive values, there is no need to dispose any value. They are automatically disposed by the Garbage Collector once they go outside its scope. But for reference values we need to manually dispose them setting them to null. This way we free that position in memory. When a reference value has only only reference and is out of scope, then the Garbage Collector removes it from memory definitely.

So following the example, the way to implement disposal in the myClass is this:

myClass.prototype.disposeInternal = function() {
    this.metadata = null;
    this.resolutions.length = 0;
    this.resolutions = null;
    this.extent = null;
    this.feature.dispose();
};

The zoomLevels property don't need any disposing as it is defined as a primitive value. On the contrary, the metadata property is defined through a typedef and it is an object reference, and it could happen that this reference comes from another one outside the scope of the instance of the myClass, so we need to set it to null. In the case of the resolutions property, as it is an array of arrays, that is, a reference object that contains references, some authors recommend to remove values from the array first, and then dispose the array next. Finally, the extent property is defined as an array of numbers, so there is no need to empty the array but just set it to null. In the case of the feature property, as it is an instance of other class, we call the dispose method of it.

Properties and methods

To implement a property just define it in the this value as we have previously seen. This can be done in the constructor or inside any other method, but in Mapspace API we follow the OpenLayers convention that avoid creating properties outside the constructor:

myClass = function(options) {
    //Creation of a default options object
    options = options || {};
    ol.Object.call(this, options);

    /**
     * The metadata of the oblique image.
     * @private
     * @type {Mapspace.ObliqueMetadata}
     */
    this.metadata = options.metadata;
};
ol.inherits(myClass, ol.Object);

var myObject = new myClass();
if (myObject.metadata) {
    console.log(myImage.metadata.imagecols);
} else {
    myObject.metadata = {
        imagecols: 2048
    };
}

Be careful when accessing properties. Check what properties can store because usually they can store undefined or null values. Check if they have an actual value before using them. In Javascript all properties are public by default so there is no way to prevent from modifying those values, but it is strictly recommended when a property is set as private in the documentation to access it only through methods.

In the case of the previous example, the myClass class provides a public method for accessing the metadata property called getMetadata, so the previous code should be redone as follows:

myClass = function(options) {
    //Creation of a default options object
    options = options || {};
    ol.Object.call(this, options);

    /**
     * The metadata of the oblique image.
     * @private
     * @type {Mapspace.ObliqueMetadata}
     */
    this.metadata = options.metadata;
};
ol.inherits(myClass, ol.Object);

myClass.prototype.getMetadata = function() {
    return this.metadata;
};

var myObject = new myClass();
var metadata = myObject.getMetadata();
if (metadata) {
    console.log(metadata.imagecols);
} else {
    metadata = {
        imagecols: 2048
    };
}

Notice that the metadata variable stores a reference to the actual object stored in the metadata property of myObject variable, not a cloned value.

Propeties are values that are stored in instances, so each instance has its own values. To store a global value for all the instances in a class, use the prototype object as follows:

myClass = function(options) {
    //Creation of a default options object
    options = options || {};
    ol.Object.call(this, options);

    //Constructor
};
ol.inherits(myClass, ol.Object);

myClass.prototype.NAME = 'Oblique image';

var myObj1 = new myClass();
var myObj2 = new myClass();

console.log(myObj1.NAME);  // Outputs Oblique image
console.log(myObj2.NAME);  // Outputs Oblique image

myClass.prototype.NAME = 'Oblique';

console.log(myObj1.NAME);  // Outputs Oblique
console.log(myObj2.NAME);  // Outputs Oblique

To implement a method use the prototype object as previously shown:

myClass = function(options) {
    //Creation of a default options object
    options = options || {};
    ol.Object.call(this, options);

    /**
     * The metadata of the oblique image.
     * @private
     * @type {Mapspace.ObliqueMetadata}
     */
    this.metadata = options.metadata;
};
ol.inherits(myClass, ol.Object);

//Example of a getter
myClass.prototype.getMetadata = function() {
    return this.metadata;
};

//Example of a setter
myClass.prototype.setMetadata = function(metadata) {
    this.metadata = metadata;
};

Notice that in the previous setter we are passing a reference object, so any change made to that metadata object will be reproduced in the this.metadata object. They are one and the same. To avoid this, you can clone the object. For example:

//Example of a setter cloning the input
myClass.prototype.setMetadata = function(metadata) {
    this.metadata = Mapspace.serialization.clone(metadata);
};

Calling parent methods

Sometimes we need to separate code placing generic code in a parent class and specific code in a derived class. To access the code of the parent class, just call the parent method. For example, in our myClass example, to access a parent method from a derived method (an overrided method), use call but using the prototype object, as follows:

myClass = function(options) {
    //Creation of a default options object
    options = options || {};
    ol.Object.call(this, options);

    //Constructor
};
ol.inherits(myClass, ol.Object);

myClass.prototype.setProperties = function(values, opt_silent) {
    if (values.metadata) {
        this.setMetadata(values.metadata);
    }
    ol.Object.prototype.setProperties.call(this, values, opt_silent);
};

var obj = new myClass();

obj.setProperties({
    h: 100,
    v: 200,
    metadata: {
        imagecols: 1000
    }
});

console.log(obj.get('h'));  // Outputs 100
console.log(obj.metadata.imagecols);  //Outputs 1000

Notice that for this example to work we need the ol.Object.call(this, options); instruction to be called inside the constructor. This add the observable variables support to our class. Otherwise we have a ol.Object, but not correctly initiated.

Observable properties

Observable properties are a special case of properties introduced by OpenLayers. This properties are stored in a separated section in each object, and when set to a new value they automatically trigger an event, so they are perfect to trace when things change inside an object.

We add observable properties to a class when we derive from ol.Object and we call the constructor of ol.Object from our class.

Observable properties are accessed through a get method and set through a set method. Alternatively, we can get all of them with a getProperties method, or asign a bunch of them through a setProperties method. Anytime an observable variable change its value it triggers an event notifying the change.

Let's see an example. If we want our previous myClass class to have a name property that triggers an event each time it is changed:

var obj = new myClass();

//We can create observable variables at any time dynamically
obj.set('name', 'Oblique 1');

//Each instance can store its own observable variables and with its own values
var obj2 = myClass();
obj2.set('name', 'Oblique 2');
obj2.set('type', 'High Definition'); // Only obj2 has type observable variable

console.log(obj.get('name')); // Outputs Oblique 1
console.log(obj.get('type')); // Outputs undefined
console.log(obj2.get('name')); //Outputs Oblique 2
console.log(obj2.get('type')); // Outputs High Definition

If you try to access an observable variable without a previous set, then it returns undefined.

To listen to events triggered in the change of an observable variable, you can use the on method and the name of the change event has always the pattern change:name_of_property. Check Events and listeners.

var obj = new myClass();

//Setting a listener to the change of the 'name' observable variable
obj.on('change:name', function(evt) {
    console.log('New name is ' + this.get('name'));
}, obj);

obj.set('name', 'Oblique 1'); // Outputs New name is Oblique 1
obj.set('name', 'Oblique 2'); // Outputs New name is Oblique 2

Triggering events

To trigger an event call the dispatchEvent method. This method accepts several parameters as input: an string with the name of the event, an ol.events.Event object, or a generic object with two properties (type and target) that defines the event.

For example, let's say we want to trigger an event called loaded. This code shows how to do it:

var obj = new myClass();

// Dispatch event using a string with the name of the event
obj.dispatchEvent('loaded');

// Dispatch event using an ol.events.Event object or any derived class
obj.dispatchEvent(new ol.events.Event('loaded'));

// Dispatch event using a generic object
obj.dispatchEvent({
    type: 'loaded',
    target: null
});

As you can see any instance of a class that inherits from ol.Object and has been correctly initiated inheritance can trigger any type of event even from outside the code of the class. Events are only names to which attach listeners. Check Events and listeners for a full explanation on events.

Global instances

Global instances are instances created inside the API in the global Mapspace namespace or other namespaces so they can be easily accessed. An example of this is the Mapspace.Globalization instance. They work more or less as singletons, although singletons has no sense in JavaScript. For example, this instance is defined as follows in the API so there is no need to create new instances, although possible:

/**
 * A kind-of singleton instance of Mapspace.language.Globalization.
 * @type {Mapspace.language.Globalization}
 * @api
 */
Mapspace.Globalization = new Mapspace.language.Globalization();