Documentation

Geo AR (Points of Interest)

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.

POI at Location

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 can be found in the file poiatlocation.js.

The example Image Recognition already explained how images are loaded and displayed in the augmented reality view. 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 last line of the poiatlocation.js shows how to set a custom AR.context.onLocationChanged callback.

AR.context.onLocationChanged = World.onLocationChanged;
view source code on GitHub

The custom function World.onLocationChanged checks with the flag World.initiallyLoadedData if the function was already called. There is also a possibility to set AR.context.onLocationChanged to null. In this case the function will not be called anymore and no further location updates will be received.

On the first call of World.onLocationChanged an object that contains geo information will be created which will be later used to create a marker using the World.loadPoisFromJsonData function.

locationChanged: function locationChangedFn(lat, lon, alt, acc) {
    // request data if not already present
    if (!World.initiallyLoadedData) {
        var poiData = {
            "id": 1,
            "longitude": (lon + (Math.random() / 5 - 0.1)),
            "latitude": (lat + (Math.random() / 5 - 0.1)),
            "altitude": 100.0
        };
        World.loadPoisFromJsonData(poiData);
        World.initiallyLoadedData = true;
    }
}
view source code on GitHub

The loadPoisFromJsonData function creates an AR.ImageResource which is later used as an image for the marker.

// start loading marker assets
World.markerDrawable_idle = new AR.ImageResource("assets/marker_idle.png");
view source code on GitHub

For creating the marker a new object AR.GeoObject will be created at the specified geolocation. An AR.GeoObject connects one or more AR.GeoLocations 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.

// create the marker
var markerLocation = new AR.GeoLocation(poiData.latitude, poiData.longitude, poiData.altitude);
var markerImageDrawable_idle = new AR.ImageDrawable(markerDrawable_idle, 2.5, {
    zOrder: 0,
    opacity: 1.0
});

// create GeoObject
var markerObject = new AR.GeoObject(markerLocation, {
    drawables: {
        cam: [markerImageDrawable_idle]
    }
});
view source code on GitHub

Finally the status message is updated as a user feedback that everything was loaded properly:

World.updateStatusMessage('1 place loaded');
view source code on GitHub

POI with Label

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 the file is only renamed but its content is mostly identical to poiatlocation.js.

The locationChanged function adds a description and a title to the marker:

var poiData = {
    "id": 1,
    "longitude": (lon + (Math.random() / 5 - 0.1)),
    "latitude": (lat + (Math.random() / 5 - 0.1)),
    "altitude": 100.0,
    "description": "This is the description of POI#1",
    "title": "POI#1"
};
view source code on GitHub

Since there are additional changes concerning the marker it makes sense to extract the code to a separate Marker class (see marker.js). Parts of the code are moved from loadPoisFromJsonData to the Markerclass: the creation of the AR.GeoLocation, the creation of the AR.ImageDrawable and the creation of the AR.GeoObject. Then instantiate the Marker in the function loadPoisFromJsonData like this:

// create the marker
var marker = new Marker(poiData);
view source code on GitHub

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 (rendering order) and if they need a location offset. For both scenarios, ARchitect has some functionality to adjust the drawable behavior.

To position the AR.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 AR.GeoObject they will overlap. To adjust their position change the translate.x and translate.y property of an AR.Drawable object. The unit for translations are SDUs. See this chapter for more information about SDUs.

In the following both AR.Labels are initialized and positioned. Note that they are added to the cam property of the AR.GeoObject the same way as an AR.ImageDrawable.

function Marker(poiData) {

    this.poiData = poiData;

    var markerLocation = new AR.GeoLocation(poiData.latitude, poiData.longitude, poiData.altitude);
    this.markerDrawable_idle = new AR.ImageDrawable(World.markerDrawable_idle, 2.5, {
        zOrder: 0,
        opacity: 1.0
    });

    this.titleLabel = new AR.Label(poiData.title.trunc(10), 1, {
        zOrder: 1,
        translate: {
            y: 0.55
        },
        style: {
            textColor: '#FFFFFF',
            fontStyle: AR.CONST.FONT_STYLE.BOLD
        }
    });

    this.descriptionLabel = new AR.Label(poiData.description.trunc(15), 0.8, {
        zOrder: 1,
        translate: {
            y: -0.55
        },
        style: {
            textColor: '#FFFFFF'
        }
    });

    // Changed: 
    this.markerObject = new AR.GeoObject(markerLocation, {
        drawables: {
            cam: [this.markerDrawable_idle, this.titleLabel, this.descriptionLabel]
        }
    });

    return this;
}
view source code on GitHub

Additionally a function which truncates a text string that is longer than the given length is added. This function is used to shorten titles or descriptions.

String.prototype.trunc = function(n) {
       return this.substr(0, n - 1) + (this.length > n ? '...' : '');
};
view source code on GitHub

Multiple POIs

The third example consist of two parts. The first part shows how to create multiple markers and in the second part an implementation of a marker selection is described.

For creating multiple markers change the class World. Add a function requestDataFromLocal with the geo information as parameters (latitude, longitude) which are used for creating different poi data to a random location in the user's vicinity. The new function is called from locationChanged instead of calling loadPoisFromJsonData like in the previous example:

World.requestDataFromLocal(lat, lon);
view source code on GitHub

The function loadPoisFromJsonData will be called within the new function requestDataFromLocal after the POI data was created:

// request POI data
requestDataFromLocal: function requestDataFromLocalFn(centerPointLatitude, centerPointLongitude) {
    var poisToCreate = 20;
    var poiData = [];

    for (var i = 0; i < poisToCreate; i++) {
        poiData.push({
            "id": (i + 1),
            "longitude": (centerPointLongitude + (Math.random() / 5 - 0.1)),
            "latitude": (centerPointLatitude + (Math.random() / 5 - 0.1)),
            "description": ("This is the description of POI#" + (i + 1)),
            "altitude": "100.0",
            "name": ("POI#" + (i + 1))
        });
    }
    World.loadPoisFromJsonData(poiData);
}
view source code on GitHub

Since the argument of loadPoisFromJsonData is used as an array and not like a single object as before some adaptations are necessary. The POI information array which is delivered as an argument in the function loadPoisFromJsonData is used to create poiData objects. In a for loop iterate through all the poi information objects. For each object create a new object singlePoi. To create multiple markers new Marker(poiData) can be called multiple times with different locations, titles and descriptions as defined in the poiData object. Now create the Marker objects and store them in an array markerList which is defined as a member variable in the World class. The markerList array is needed for selection/deselection of markers and will be described later in this example. Finally the status message is updated with the number of POIs loaded.

// called to inject new POI data
loadPoisFromJsonData: function loadPoisFromJsonDataFn(poiData) {

    // empty list of visible markers
    World.markerList = [];

    // start loading marker assets
    World.markerDrawable_idle = new AR.ImageResource("assets/marker_idle.png");

    // loop through POI-information and create an AR.GeoObject (=Marker) per POI
    for (var currentPlaceNr = 0; currentPlaceNr < poiData.length; currentPlaceNr++) {
        var singlePoi = {
            "id": poiData[currentPlaceNr].id,
            "latitude": parseFloat(poiData[currentPlaceNr].latitude),
            "longitude": parseFloat(poiData[currentPlaceNr].longitude),
            "altitude": parseFloat(poiData[currentPlaceNr].altitude),
            "title": poiData[currentPlaceNr].name,
            "description": poiData[currentPlaceNr].description
        };

        World.markerList.push(new Marker(singlePoi));
    }

    World.updateStatusMessage(currentPlaceNr + ' places loaded');
}
view source code on GitHub

At this point the implementation for displaying multiple markers is finished. Let's have a look on how to change the marker background image after it was selected and handle the different selection states.

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)
});
view source code on GitHub

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 with the help of the variable isSelected and executes the 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);
                try {
                    World.onMarkerSelected(marker);
                } catch (err) {
                    alert(err);
                }
            }
    };
};
view source code on GitHub

The setSelected and setDeselected functions are prototype Marker functions.

Both functions perform the same steps but inverted, hence only one function (setSelected) is covered in detail. Three steps are necessary to select the marker. First the state will be set appropriately. Second the background drawable will be enabled and the standard background disabled. This is done by setting the opacity property to 1.0 for the visible state and to 0.0 for an invisible state. Third the onClick function is set only for the background drawable of the selected marker.

Marker.prototype.setSelected = function(marker) {

    marker.isSelected = true;

    marker.markerDrawable_idle.opacity = 0.0;
    marker.markerDrawable_selected.opacity = 1.0;
    marker.markerDrawable_idle.onClick = null;
    marker.markerDrawable_selected.onClick = Marker.prototype.getOnClickTrigger(marker);
};
view source code on GitHub

To be able to deselect a marker while the user taps on the empty screen, the World object holds an array that contains each marker.

World.markerList.push(new Marker(singlePoi));
view source code on GitHub

To detect clicks where no drawable was hit set a custom function on AR.context.onScreenClick where the currently selected marker is deselected.

onScreenClick: function onScreenClickFn() {
    if (World.currentMarker) {
        World.currentMarker.setDeselected(World.currentMarker);
    }
}
view source code on GitHub

Selecting POIs

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 are 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 is animated too. The marker size changes over time so the labels need to be animated too in order to keep them relative to the background drawable. AR.AnimationGroups are used to synchronize all animations in parallel or sequentially.

In marker.js there are two new variables declared. They hold a reference to an AR.AnimationGroup that is used to either start or stop the animations.

this.animationGroup_idle = null;
this.animationGroup_selected = null;
view source code on GitHub

The functions setSelected and setDeselected in marker.js have to be adapted. Again only the changes in setSelected are explained.

There are two types of AR.AnimationGroups. Parallel animations are running at the same time, sequential animations are played one 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);

    var idleDrawableResizeAnimationX = new AR.PropertyAnimation(marker.markerDrawable_idle, 'scale.x', null, 1.2, kMarker_AnimationDuration_Resize, new AR.EasingCurve(AR.CONST.EASING_CURVE_TYPE.EASE_OUT_ELASTIC, {
        amplitude: 2.0
    }));
    var selectedDrawableResizeAnimationX = new AR.PropertyAnimation(marker.markerDrawable_selected, 'scale.x', null, 1.2, kMarker_AnimationDuration_Resize, new AR.EasingCurve(AR.CONST.EASING_CURVE_TYPE.EASE_OUT_ELASTIC, {
        amplitude: 2.0
    }));

    var titleLabelResizeAnimationX = new AR.PropertyAnimation(marker.titleLabel, 'scale.x', null, 1.2, kMarker_AnimationDuration_Resize, new AR.EasingCurve(AR.CONST.EASING_CURVE_TYPE.EASE_OUT_ELASTIC, {
        amplitude: 2.0
    }));
    var descriptionLabelResizeAnimationX = new AR.PropertyAnimation(marker.descriptionLabel, 'scale.x', null, 1.2, kMarker_AnimationDuration_Resize, new AR.EasingCurve(AR.CONST.EASING_CURVE_TYPE.EASE_OUT_ELASTIC, {
        amplitude: 2.0
    }));

    var idleDrawableResizeAnimationY = new AR.PropertyAnimation(marker.markerDrawable_idle, 'scale.y', null, 1.2, kMarker_AnimationDuration_Resize, new AR.EasingCurve(AR.CONST.EASING_CURVE_TYPE.EASE_OUT_ELASTIC, {
        amplitude: 2.0
    }));
    var selectedDrawableResizeAnimationY = new AR.PropertyAnimation(marker.markerDrawable_selected, 'scale.y', null, 1.2, kMarker_AnimationDuration_Resize, new AR.EasingCurve(AR.CONST.EASING_CURVE_TYPE.EASE_OUT_ELASTIC, {
        amplitude: 2.0
    }));

    var titleLabelResizeAnimationY = new AR.PropertyAnimation(marker.titleLabel, 'scale.y', null, 1.2, kMarker_AnimationDuration_Resize, new AR.EasingCurve(AR.CONST.EASING_CURVE_TYPE.EASE_OUT_ELASTIC, {
        amplitude: 2.0
    }));
    var descriptionLabelResizeAnimationY = new AR.PropertyAnimation(marker.descriptionLabel, 'scale.y', null, 1.2, kMarker_AnimationDuration_Resize, new AR.EasingCurve(AR.CONST.EASING_CURVE_TYPE.EASE_OUT_ELASTIC, {
        amplitude: 2.0
    }));

    marker.animationGroup_selected = new AR.AnimationGroup(AR.CONST.ANIMATION_GROUP_TYPE.PARALLEL, [hideIdleDrawableAnimation, showSelectedDrawableAnimation, idleDrawableResizeAnimationX, selectedDrawableResizeAnimationX, titleLabelResizeAnimationX, descriptionLabelResizeAnimationX,idleDrawableResizeAnimationY, selectedDrawableResizeAnimationY, titleLabelResizeAnimationY, descriptionLabelResizeAnimationY]);
}
view source code on GitHub

Launch the AR.AnimationGroup using the start function.

marker.animationGroup_selected.start();
view source code on GitHub

In the function Marker.prototype.getOnClickTrigger the selection functions are only called if no animation is currently running:

if (!Marker.prototype.isAnyAnimationRunning(marker)) {
    if (marker.isSelected) {
        Marker.prototype.setDeselected(marker);
    } else {
        Marker.prototype.setSelected(marker);
        try {
            World.onMarkerSelected(marker);
        } catch (err) {
            alert(err);
        }
    }
} else {
    AR.logger.debug('a animation is already running');
}
view source code on GitHub

Create an AR.ImageResource referencing the image that should be displayed for a direction indicator. Then create an AR.ImageDrawable using the AR.ImageResource. Set options regarding the translation 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
});
view source code on GitHub

The last step is to define the AR.ImageDrawable as an indicator target on the marker AR.GeoObject. The direction indicator is displayed automatically when necessary. AR.Drawable subclasses (e.g. AR.Circle) can be used as direction indicators.

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