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();