Point Of Interest (POI)

The Point Of Interest (POI) example series will show how you can create a marker that is placed at a specific geolocation. The example is split into four different parts that depend on each other. You will have a complete and reusable marker at the end of the series which has a title, description, a selected and an idle state which animates smoothly from one to another.

This sample consists of four parts

  1. POI at Location
  2. POI with Label
  3. Multiple POIs
  4. Selecting POIs

POI at Location (1/4)

The first part of the series will present an image at a geolocation. To do so, we will use the AR.context.onLocationChanged() callback to get the current location. After the location has been retrieved, we will use it to place an AR.ImageDrawable there.

All JavaScript code is written in the poiatlocation.js file.

The example Image Recognition already explained how images are loaded and displayed in AR. This sample loads an AR.ImageResource when the World variable was defined. It will be reused for each marker that we will create afterwards.

The init function is used to set a custom AR.context.onLocationChanged callback function.

init: function initFn() {

    AR.context.onLocationChanged = World.onLocationChanged;
}

The custom World.onLocationChanged function first removes the custom callback function. This way the world won't receive any new location updates. After we have disabled further location updates, a marker will be created using the World.createMarkerAtLocation function.

onLocationChanged: function onLocationChangedFn(latitude, longitude, altitude, accuracy) {

    AR.context.onLocationChanged = null;

    World.createMarkerAtLocation(latitude + 0.01, longitude - 0.0005, altitude - 0.06);
}

The createMarkerAtLocation function creates a new marker object at the specified geolocation. To do this, an AR.GeoLocation, AR.ImageDrawable and AR.GeoObject will be used. The AR.GeoLocation object will be initialized with latitude, longitude and altitude . It defines the geolocation where the marker should be drawn. To actually draw an image, the AR.ImageDrawable is used with the AR.ImageResource, defined in the World variable. An AR.GeoObject connects one or more AR.GeoLocation with multiple AR.Drawables. The AR.Drawables can be defined for multiple targets. A target can be the camera, the radar or a direction indicator. Both the radar and direction indicators will be covered in more detail in later examples.

createMarkerAtLocation: function createMarkerAtLocationFn(latitude, longitude, altitude) {

    var markerLocation = new AR.GeoLocation(latitude, longitude, altitude);
    var markerDrawable = new AR.ImageDrawable(World.markerDrawable, 3);
    var markerObject = new AR.GeoObject(markerLocation, {
        drawables: {
            cam: markerDrawable
        }
    });
}

POI with Label (2/4)

The second part adds a title and description label to our marker object and covers more drawable related options.

All JavaScript changes are in poiwithlabel.js. Note that only the file is only renamed but its content is mostly identical to poiatlocation.js. All new code is labeled with a comment so that changes can easily be spotted.

There are two major points that need to be considered while drawing multiple AR.Drawables at the same location. It has to be defined which one is before or behind another drawable and if they need a location offset. For both scenarios, ARchitect has some functionality to adjust the drawable behavior.

To position the label in front of the background, the background drawable(AR.ImageDrawable2D) receives a zOrder of 0. Both labels have a zOrder of 1. This way, it is guaranteed that the labels will be drawn in front of the background drawable.

Assuming both labels will be drawn on the same geolocation connected with the same geo object, they will overlap. To adjust their position, you can adjust the offsetX and offsetY property of an AR.Drawable2D object. The unit which are used to set an offset are SDUs. See the ARchitect reference for more information about SDUs.

The following listings shows how both labels are initialized and positioned. Note that they are added to the screen similarly to an AR.ImageDrawable.

// New: The createMarkerAtLocation function now has two additional parameters, title and description text.
createMarkerAtLocation: function createMarkerAtLocationFn(latitude, longitude, altitude, title, description) {

    var markerLocation = new AR.GeoLocation(latitude, longitude, altitude);
    var markerDrawable = new AR.ImageDrawable(World.markerDrawable, 5, {

        // New: zOrder option
        zOrder: 0
    });

    // New: Title label with options that defines rendering order and offsets
    var titleLabel = new AR.Label(title, 1, {
        zOrder: 1,
        offsetX: -2,
        offsetY: 0.5,
        style: {
            fontStyle: AR.CONST.FONT_STYLE.BOLD
        }
    });

    // New: Description label (similar options as for the title label)
    var descriptionLable = new AR.Label(description, 1, {
        zOrder: 1,
        offsetX: -2,
        offsetY: -titleLabel.height * 0.5
    });

    var markerObject = new AR.GeoObject(markerLocation, {
        drawables: {

            // New: two more cam drawables: title and description label
            cam: [markerDrawable, titleLabel, descriptionLable]
        }
    });
}

Multiple POIs (3/4)

The third example is split into two parts. The first is all about refactoring existing code so that it can be reused in other ARchitect Worlds. The new structure is then used to create multiple markers at different locations. The second part deals with highlighting a marker drawable after the user taps on it.

This example consists of two JavaScript files. The ARchitect World entry point is multiplepois.js and the marker definition can be found in marker.js. Inside marker.js, a custom function named Marker is defined. The only parameter is an object containing poiData in JSON format. The function contains all the code from the createMarkerAtLocation function of the previous example. Additional poiData and selection state will be saved in the variables.

function Marker(poiData) {

    this.poiData = poiData;

    this.isSelected = false;

    // ...
    // all the `createMarkerAtLocation` code
    // ...

    return this;
}

In multiplepois.js the marker creation in onLocationChanges is now slightly different. Instead of creating all the ARchitect objects in a World function, the new Marker function is used. This way the code is much cleaner while creating multiple markers.

To use the new Marker function, a object representing the poi data in JSON needs to be created and supplied as a parameter. The benefit of using JSON is that it is very easy to add additional parameters for the marker creation. The following snippet describes a JSON data object.

var poiData = {
    "latitude": latitude + 0.01,
    "longitude": longitude - 0.01,
    "altitude": altitude,
    "title": "Marker 1",
    "description": "This is marker 1"
};

To create a new marker object, the new keyword needs to be used.

var marker_one = new Marker(poiData);

To create multiple markers, new Marker(poiData) can be called multiple times with different locations, titles and descriptions as defined in the poiData object.

The following describes how a marker object can be selected by changing the background drawable.A second AR.ImageDrawable is defined in marker.js.

To react on user interaction, an onClick property can be set for each AR.Drawable. The property is a function which will be called each time the user taps on the drawable. The following snippet shows the adapted AR.ImageDrawable creation.

this.markerDrawable_idle = new AR.ImageDrawable(World.markerDrawable_idle, 2.5, {
    zOrder: 0,
    opacity: 1.0,
    onClick: Marker.prototype.getOnClickTrigger(this)
});

The function called on each tap is returned from the following helper function defined in marker.js. The function returns a function which checks the selected-state and executes appropriate function. The clicked marker is passed as an argument.

Marker.prototype.getOnClickTrigger = function(marker) {

    return function() {

        if (marker.isSelected) {

            Marker.prototype.setDeselected(marker);

        } else {

            Marker.prototype.setSelected(marker);
        }

        return true;
    };
};

The setSelected and setDeselected functions are prototype Marker functions.

Both functions perform the same steps but only inverted. Because of this, only one function (setSelected) will be covered in detail. Three steps are done to select the marker. First, the state will be set appropriately. Second, the selected background drawable will be enabled and the standard background disabled. Third, the onClick function is only set for the background drawable of the selected marker.

Marker.prototype.setSelected = function(marker) {

    marker.isSelected = true;

    marker.markerDrawable_idle.enabled = false;
    marker.markerDrawable_selected.enabled = true;

    marker.markerDrawable_idle.onClick = null;
    marker.markerDrawable_selected.onClick = Marker.prototype.getOnClickTrigger(marker);
};

To be able to deselect a marker while the user taps on the empty screen, the World object has an array where each marker is added after its initialization.

World.markerList.push( new Marker(poiData) );

To detect clicks where no drawable was hit, you can set a custom function on AR.context.onScreenClick. In the custom function, each marker can be checked if it is selected. If so, the setDeselected function is called, supplying the marker object at the current index.

onScreenClick: function onScreenClickFn() {

    for (var i = World.markerList.length - 1; i >= 0; i--) {
        if (World.markerList[i].isSelected) {
            World.markerList[i].setDeselected(World.markerList[i]);
        }
    }
}

Selecting POIs (4/4)

The last part describes the concepts behind AR.PropertyAnimations and AR.AnimationGroups. It also explains how direction indicators can be used to visualize selected objects that are currently not visible in the viewfinder.

With AR.PropertyAnimations you're able to animate almost any property of ARchitect objects. This sample will animate the opacity of both background drawables so that one will fade out while the other one fades in. The scaling will alos be animated. Because the marker size changes over time, both labels need to be animated as well to stay at the same position relative to the background drawable. To synchronize all the animations, AR.AnimationGroups are used.

In marker.js there are two new variables declared in the Marker function. They will hold a reference to an AR.AnimationGroup that is used to either start the select or deselect process.

this.animationGroup_idle = null;
this.animationGroup_selected = null;

The next changes are done in the setSelected and setDeselected prototype functions in marker.js. Again only the changes in setSelected will be explained.

The animations will be created on demand, meaning, if the animation group is null, all the necessary animations will be created. Note that there are two types of AR.AnimationGroups. The first type is parallel, which means that all the animations are running at the same time. The other type is sequential. A sequential group will play one animation after another. This example uses a parallel AR.AnimationGroup.

if (marker.animationGroup_selected === null) {

    var hideIdleDrawableAnimation = new AR.PropertyAnimation(marker.markerDrawable_idle, "opacity", null, 0.0, kMarker_AnimationDuration_ChangeDrawable);
    var showSelectedDrawableAnimation = new AR.PropertyAnimation(marker.markerDrawable_selected, "opacity", null, 0.8, kMarker_AnimationDuration_ChangeDrawable);

    // ** all required animations are created **

    marker.animationGroup_selected = new AR.AnimationGroup(AR.CONST.ANIMATION_GROUP_TYPE.PARALLEL, [hideIdleDrawableAnimation, showSelectedDrawableAnimation, idleDrawableResizeAnimation, selectedDrawableResizeAnimation, titleLabelResizeAnimation, descriptionLabelResizeAnimation]);
}

After the AR.PropertyAnimations and AR.AnimationGroup are created, the AR.AnimationGroup can be started using the start function.

if (!marker.animationGroup_selected.isRunning()) {
    marker.animationGroup_selected.start();
}

To define a direction indicator you'll need to create an AR.ImageResource referencing the image that should be displayed. The next step is to create an AR.ImageDrawable using the AR.ImageResource. You can set options regarding the offset and anchor of the image so that it will be displayed correctly on the edge of the screen.

this.directionIndicatorDrawable = new AR.ImageDrawable(World.markerDrawable_directionIndicator, 0.5, {
    enabled: false
});

The last step is to define the AR.ImageDrawable as an indicator target on the marker geo object. ARchitect will show and hide the image drawable of the direction indicator . Note that all AR.Drawable subclasses can be used as direction indicator.

var markerObject = new AR.GeoObject(markerLocation, {
    drawables: {
        cam: [this.markerDrawable_idle, this.markerDrawable_selected, this.titleLabel, this.descriptionLabel],
        indicator: this.directionIndicatorDrawable
    }
});