さまざまなPOIデータの表示方法

1度にいくつのマーカーを表示すべきか? 同じ方向にあるPOIデータの扱いをどうするか? 表示対象となるPOIデータの最大距離範囲は?POIデータに関する 説明が長文になる場合どのように表示すればいいか?など、ARchitect Worldに多数のマーカーを表示する場合、多くの検討すべき点があります。 以下のサンプルは6つのパートで構成されており、ロケーションベース型ARを実装する場合によくある質問を取り上げています。

POIデータの詳細情報の表示

対応するサンプルタイトル名「Presenting Details」

POIデータには通常名前があり、その説明にかなり長い文章が必要なこともあります。そのような場合、たとえばマーカーとその名前、および説明の一部を表示するだけにし、ユーザーがそのマーカーを選択すると詳しい情報が表示されるようにするといった対処をすることが考えられます。

jQuery Mobileは、「より少ないコードでより多くのことを実行する」という思想で設計され、モバイルデバイス用の魅力的なユーザーインタフェースを作成する簡単な手段の1つです。jQuery Mobileを使用すれば、モバイルデバイスまたはOSごとに固有のアプリケーションを作成するのではなく、一般に普及しているあらゆるスマートフォン、タブレット、およびデスクトッププラットフォームで動作する高度にブランディングされた単一のWebサイトまたはアプリケーションを設計できます(jQuery MobileのWebサイトからの引用)。このセクションのサンプルではARchitect WorldへのUIの実装にはjQuery Mobileを使用します。jQuery Mobileは十分なドキュメントを備えており、アプリケーションにバンドルすることも、独自のWebサーバーでホストすることもできます。

このサンプルでは、マーカーが選択されたときに対応するPOIデータの詳細情報をパネルに表示します。サンプルのディレクトリにあるindex.htmlは以下のようになっています。

<!-- panel containing POI detail information -->
<div data-role="panel" id="panel-poidetail" data-position="right" data-display="overlay" style="background-color:#F0F0F0;" data-theme="c">
<!-- header with "close" button -->
<div data-role="header" data-theme="c">
    <h1>Details</h1>
    <a href="#header" data-rel="close">Close</a>
</div>
<!-- content of POI detail page, you may also add thumbnails etc. here if you like -->
<div data-role="content">
    <!-- title -->
    <h3 id="poi-detail-title"></h3>
    <!-- description -->
    <h4 id="poi-detail-description"></h4>
    <!-- distance -->
    <h4>Distance: <a id="poi-detail-distance"></a></h4>
</div>
ソースコードをGitHubで見る

マーカーを選択すると、パネルが右から左にアニメーション表示され、POIデータの詳細情報を確認できます。

マーカーの選択状態を解除するため、panelbeforecloseイベントを使用します。presentingdetails.jsを参照してください。

onMarkerSelected: function onMarkerSelectedFn(marker) {
    World.currentMarker = marker;
    // update panel values
    $("#poi-detail-title").html(marker.poiData.title);
    $("#poi-detail-description").html(marker.poiData.description);
    var distanceToUserValue = (marker.distanceToUser > 999) ? ((marker.distanceToUser / 1000).toFixed(2) + " km") : (Math.round(marker.distanceToUser) + " m");
    $("#poi-detail-distance").html(distanceToUserValue);
    // show panel
    $("#panel-poidetail").panel("open", 123);
    $("#panel-poidetail").on("panelbeforeclose", function(event, ui) {
    World.currentMarker.setDeselected(World.currentMarker);
});
ソースコードをGitHubで見る

POIデータのレーダー表示

対応するサンプルタイトル名「Adding Radar」

ユーザーの近辺にある任意のロケーションに関してユーザーにヒントを与えることを推奨します。方位を示す最も簡単な方法は、AR.Radarを使用することです。AR.GeoObjectはすべてレーダーに表示でき、通常は小さな点でロケーションが示されます。

レーダーにおけるAR.GeoObjectの表現は、その拡張オブジェクトの設定で定義します(AR.GeoObjectコンストラクターの2番目の引数)。drawables.radarを設定すると、そのオブジェクトがたとえばAR.Circleとしてレーダーに示されます。詳しくはmarker.jsを参照してください。

this.radarCircle = new AR.Circle(0.03, {
    horizontalAnchor: AR.CONST.HORIZONTAL_ANCHOR.CENTER,
    opacity: 0.8,
    style: {
        fillColor: "#ffffff"
    }
}); 
this.radardrawables = [];
this.radardrawables.push(this.radarCircle);
ソースコードをGitHubで見る

さらに、選択されたマーカーをレーダー上に異なる色で表示します。詳しくはmarker.jsを参照してください。

this.radarCircleSelected = new AR.Circle(0.05, {
       horizontalAnchor: AR.CONST.HORIZONTAL_ANCHOR.CENTER,
       opacity: 0.8,
       style: {
           fillColor: "#0066ff"
       }
   });
this.radardrawablesSelected = [];
   this.radardrawablesSelected.push(this.radarCircleSelected);
this.markerObject = new AR.GeoObject(markerLocation, {
       drawables: {
           cam: [    this.markerDrawable_idle, 
                   this.markerDrawable_selected, 
                   this.titleLabel, 
                   this.descriptionLabel ],
           indicator: this.directionIndicatorDrawable,
           radar: this.radardrawables
       }
   });
ソースコードをGitHubで見る

次の図のように、選択されたマーカーをレーダーで強調するため、

Marker.prototype.setSelected関数とMarker.prototype.setDeselected関数でレーダー上の表示を更新します。詳しくはmarker.jsを参照してください。

marker.markerObject.drawables.radar = marker.radardrawablesSelected;
[...]
marker.markerObject.drawables.radar = marker.radardrawables;
ソースコードをGitHubで見る

レーダーの位置とサイズはDOM要素を使用して定義します。このサンプルでは、idがradarContainerであるdiv要素を使用しています。index.htmlを参照してください。

<div class="radarContainer_left" id="radarContainer"></div>
ソースコードをGitHubで見る

レーダーのサイズと位置はCSSクラスで定義します。poi-radar.cssを参照してください。

/* position of POI-radar*/
.radarContainer_left {
    position:absolute;
    top:0px;
    left:0px;
    width:100px;
    height:100px;
}
ソースコードをGitHubで見る

レーダーコンテナーとなるDOM要素では絶対位置を使用することを推奨します。DOM要素がjQueryまたはレスポンシブデザインによって即座に更新される場合は、AR.radar.notifyUpdateRadarPosition();を使用してレーダーの位置とサイズを強制的に更新します。それ以外の場合は、最初に設定した位置とサイズが使用されます。

レーダーはカスタマイズ可能であり、サンプルでは、JavaScriptコードで独自のコンポーネントとして実装します。詳しくはradar.jsを参照してください。

var PoiRadar = {
    hide: function hideFn() {
        AR.radar.enabled = false;
    },
    show: function initFn() {
        // the div defined in the index.htm
        AR.radar.container = document.getElementById("radarContainer");
        // set the back-ground image for the radar
        AR.radar.background = new AR.ImageResource("assets/radar_bg.png");
        // set the north-indicator image for the radar 
        // (not necessary if you don't want to display a north-indicator)
        AR.radar.northIndicator.image = new AR.ImageResource("assets/radar_north.png");
        // center of north indicator and radar-points in the radar asset, 
        // usually center of radar is in the exact middle of the background, 
        // meaning 50% X and 50% Y axis --> 0.5 for centerX/centerY
        AR.radar.centerX = 0.5;
        AR.radar.centerY = 0.5;
        AR.radar.radius = 0.3;
        AR.radar.northIndicator.radius = 0.0;
        AR.radar.enabled = true;
    },
    updatePosition: function updatePositionFn() {
        if (AR.radar.enabled) {
            AR.radar.notifyUpdateRadarPosition();
        }
    },
    // you may define some custom action when user pressed radar, 
    // e.g. display distance, custom filtering etc.
    clickedRadar: function clickedRadarFn() {
        alert("Radar Clicked");
    },
    setMaxDistance: function setMaxDistanceFn(maxDistanceMeters) {
        AR.radar.maxDistance = maxDistanceMeters;
    }
};
ソースコードをGitHubで見る

レーダーをアクティブにするには、PoiRadar.show関数を呼び出します。必要に応じてonClickトリガーを定義できます。詳しくはaddingradar.jsを参照してください。

// show radar & set click-listener
PoiRadar.show();
$('#radarContainer').unbind('click');
$("#radarContainer").click(PoiRadar.clickedRadar);
ソースコードをGitHubで見る

レーダーに表示するPOIデータの距離による表示制限

対応するサンプルタイトル名「Limiting Range」

ユーザーはある特定の距離範囲内のPOIデータにのみ関心がある場合があります。このサンプルでは、タイトルバーにボタンを追加してユーザーが対象範囲を変更できるようにします。

まず、タイトルバーにボタンを追加します。

index.html

<!-- header of UI holding feature buttons -->
<div id ="header-status" data-role="header" data-position="fixed" data-theme="c">
    <a href="javascript: World.showRange();" data-icon="gear" data-inline="true" data-mini="true">Range</a>
    <h1></h1>
</div>
ソースコードをGitHubで見る

次に、対象範囲の変更に使用するパネルを定義します。このサンプルでは、現在の対象範囲(m単位)とその範囲内にあるPOIデータの数をパネルに表示します。

index.html

<!-- range panel -->
<div data-role="panel" id="panel-distance" data-position="left" data-display="overlay" style="background-color:#F0F0F0;" data-theme="c">
    <!-- header with close button -->
    <div data-role="header" data-theme="c">
        <h1>Range</h1>
        <a href="#header" data-rel="close">Close</a>
    </div>
    <!-- distance information, calculated/updated in code  -->
    <div data-role="content">
    <!-- Range in m/km-->
    <h4> Range: <a id="panel-distance-value"></a></h4>
    <!-- Amount of visible places -->
    <h4> Visible: <a id="panel-distance-places"></a></h4>
    <!-- default slider -->
    <input id="panel-distance-range" type="range" data-highlight="true" name="rangeSlider" min="0" max="100" value="100" data-show-value="false" step="5" data-popup-enabled="false">
    </div>
</div>
ソースコードをGitHubで見る

ユーザーがスライダーの値を変更するたびにWorld.updateRangeValues関数が実行されます。この関数は、最大範囲と範囲内にあるPOIデータの総数を計算し、AR.context.scene.cullingDistanceを設定します。さらに、PoiRadar.setMaxDistanceを実行してレーダーとマーカーの表示を更新します。詳しくはlimitingrange.jsを参照してください。

    // updates values show in "range panel"
updateRangeValues: function updateRangeValuesFn() {
    // get current slider value (0..100);
    var slider_value = $("#panel-distance-range").val();
    // max range relative to the maximum distance of all visible places
    var maxRangeMeters = Math.round(World.getMaxDistance() * (slider_value / 100));
    // range in meters including metric m/km
    var maxRangeValue = (maxRangeMeters > 999) ? ((maxRangeMeters / 1000).toFixed(2) + " km") : (Math.round(maxRangeMeters) + " m");
    // number of places within max-range
    var placesInRange = World.getNumberOfVisiblePlacesInRange(maxRangeMeters);
    // update UI labels accordingly
    $("#panel-distance-value").html(maxRangeValue);
    $("#panel-distance-places").html((placesInRange != 1) ? (placesInRange + " Places") : (placesInRange + " Place"));
    // update culling distance, so only places within given range are rendered
    AR.context.scene.cullingDistance = Math.max(maxRangeMeters, 1);
    // update radar's maxDistance so radius of radar is updated too
    PoiRadar.setMaxDistance(Math.max(maxRangeMeters, 1));
},
// returns number of places with same or lower distance than given range
getNumberOfVisiblePlacesInRange: function getNumberOfVisiblePlacesInRangeFn(maxRangeMeters) {
    // sort markers by distance
    World.markerList.sort(World.sortByDistanceSorting);
    // loop through list and stop once a placemark is out of range ( -> very basic implementation )
    for (var i = 0; i < World.markerList.length; i++) {
        if (World.markerList[i].distanceToUser > maxRangeMeters) {
            return i;
        }
    };
    // in case no placemark is out of range -> all are visible
    return World.markerList.length;
},
ソースコードをGitHubで見る

レーダーの位置を更新するには、別のCSSスタイル(たとえば、jQueryのremoveClassaddClassを使用)を使用してPoiRadar.updatePosition();を呼び出します。このサンプルでは、パネルが開いたときにレーダーを右に移動しています。詳しくはlimitingrange.jsを参照してください。

handlePanelMovements: function handlePanelMovementsFn() {
    $("#panel-distance").on("panelclose", function(event, ui) {
        $("#radarContainer").addClass("radarContainer_left");
        $("#radarContainer").removeClass("radarContainer_right");
        PoiRadar.updatePosition();
    });
    $("#panel-distance").on("panelopen", function(event, ui) {
        $("#radarContainer").removeClass("radarContainer_left");
        $("#radarContainer").addClass("radarContainer_right");
        PoiRadar.updatePosition();
    });
},
ソースコードをGitHubで見る

ユーザーが[Range]ボタンをクリックすると、World.showRange関数が実行されます。

// display range slider
showRange: function showRangeFn() {
    if (World.markerList.length > 0) {
        // update labels on every range movement
        $('#panel-distance-range').change(function() {
            World.updateRangeValues();
        });
        World.updateRangeValues();
        World.handlePanelMovements();
        // open panel
        $("#panel-distance").trigger("updatelayout");
        $("#panel-distance").panel("open", 1234);
    } else {
        // no places are visible, because the are not loaded yet
        World.updateStatusMessage('No places available yet', true);
    }
}
ソースコードをGitHubで見る

マーカーのリロード

対応するサンプルタイトル名「Reloading Content」

ユーザーの移動やその他のなにかしらの理由でPOIデータをリロードしなければならないことがあります。このサンプルでは、ユーザーがrefreshボタンをクリックときにPOIデータがリロードされます。ボタンはindex.htmlで定義されており、クリック時にWorld.reloadPlaces()を呼び出します。

<a href="javascript: World.reloadPlaces()" data-icon="refresh" >Reload</a>
ソースコードをGitHubで見る

World.reloadPlaces()の実装はARchitect Worldの一部です(reloadingcontent.js)。この関数はWorld.requestDataFromServerを実行して、ユーザーの現在の位置に従ってWebサービスからデータを取得します。

メモ: 状況によっては、Webサービスが使用できなかったり、その他の接続の問題が発生したりする可能性があります。接続の問題をユーザーに知らせるため、ステータスメッセージを更新しています。実際のアプリケーションでは、ポップアップのようなものを使用してもかまいません。

var World = {
    […]
    // reload places from content source
    reloadPlaces: function reloadPlacesFn() {
        if (!World.isRequestingData) {
            if (World.userLocation) {
                World.requestDataFromServer(World.userLocation.latitude,
                                            World.userLocation.longitude);
            } else {
                World.updateStatusMessage('Unknown user-location.', true);
            }
        } else {
            World.updateStatusMessage('Already requesting places...', true);
        }
    }
    […]
}
ソースコードをGitHubで見る

POIデータの詳細情報をアクティビティに表示

対応するサンプルタイトル名「Reloading Content」

POIデータの詳細情報をネイティブのアクティビティに表示したい場合があります。このサンプルでは、ユーザーがHTMLの[More]ボタンを押したときにシンプルなアクティビティが開きます。このサンプルではJavaScriptとネイティブコード間でやり取りする方法を示します。

[More]ボタンはindex.htmlで定義されており、クリック時にWorld.onPoiDetailMoreButtonClicked関数を呼び出します。

<!-- more button-->
<a href="javascript: World.onPoiDetailMoreButtonClicked();" 
   data-role="button" data-icon="arrow-r" data-iconpos="right" data-inline="true">
    More
</a>
ソースコードをGitHubで見る

World.onPoiDetailMoreButtonClickednativedetailscreen.jsで実装されており、AR.platform.sendJSONObject(...)を実行します。ネイティブプロジェクトがこの呼び出しをインターセプトすると、次のようになります。

var World = {
    […]
    // user clicked "More" button in POI-detail panel -> fire event to open native screen
    onPoiDetailMoreButtonClicked: function onPoiDetailMoreButtonClickedFn() {
        var currentMarker = World.currentMarker;
        var markerSelectedJSON = {
            name: "markerselected",
            id: currentMarker.poiData.id,
            title: currentMarker.poiData.title,
            description: currentMarker.poiData.description
        };
        AR.platform.sendJSONObject(markerSelectedJSON);
    }
    […]
}
ソースコードをGitHubで見る

カスタムURLスキームのネイティブ部分についてはここのセクションで説明します。

サンプルは、AR.platform.sendJSONObjectのコールバックメソッドを使用して、JSとObjCの間で通信行います。 AR.platform.sendJSONObjectはいつでも呼び出すことができますが、オプションのWTArchitectViewDelegateメソッド-architectView:receivedJSONObject:を実装して、ObjCで対応する呼び出しを取得する必要があります。 WTArchitectViewデリゲートを設定するには、delegateオブジェクトをWTArchitectViewDelegateプロトコルに準拠させ、前述の省略可能なオプションのメソッドを実装します。 WTArchitectViewオブジェクトにdelegateを設定すると、すべてのカスタムURL呼び出しが受信されます。 デリゲートメソッドが呼び出されるとき、次のパラメータが与えられます。

  • 最初のパラメーターは、カスタムURLスキームが呼び出された、JSコンテキストのWTArchitectViewインスタンスです。
  • 2番目のパラメーターは、完全なURLを表すNSURLオブジェクトです。

クラス定義のコードを以下に示します。

@interface WTPresentingPoiDetailsARViewController () <WTArchitectViewDelegate>
@end

クラスをプロトコルに準拠させたら、WTArchitectViewインスタンスを作成し、自身をWTArchitectViewのデリゲートに設定します。

self.architectView.delegate = self;

最後に、オプションのWTArchitectViewDelegateメソッドである-architectView:invokedURL:に必要な処理を実装します。以下のコードは、iOSサンプルアプリケーションの実装を示します(WTPresentingPoiDetailsARViewController.m)。

- (void)architectView:(WTArchitectView *)architectView receivedJSONObject:(NSDictionary *)jsonObject
{
    if ( jsonObject ) {
        if ( [[jsonObject objectForKey:@"name"] isEqualToString:@"markerselected"]  )
        {
            NSUInteger poiId = [[jsonObject objectForKey:@"id"] integerValue];
            WTPoi *poi = [self.poiManager poiForId:poiId];
            if (poi) {
                WTPoiDetailViewController *poiDetailViewController = [[WTPoiDetailViewController alloc] initWithNibName:@"WTPoiDetailViewController" bundle:nil];
                poiDetailViewController.poi = poi;
                [self.navigationController pushViewController:poiDetailViewController animated:YES];
            }
        }
    }
}

スクリーンショットの取得

対応するサンプルタイトル名「Bonus: Capture Screen」

このサンプルは、captureScreen関数を使用してスクリーンショットを共有する方法を示します。JavaScriptとネイティブコード間のやり取りの概念は前のパートのサンプルと同じですが、今回はurlListenerで写真の共有を処理します。[Snapshot]ボタンはタイトルバーの右上にあります。このボタンをクリックすると現在の画面がキャプチャされ、その画面を共有するかどうかを確認するメッセージが表示されます。

 <!-- header of UI holding feature buttons -->
        <div id ="header-status" data-role="header" data-position="fixed" data-theme="c">
            <a href="javascript: World.showRange();" data-icon="gear" data-inline="true" data-mini="true">Range</a>
            <a href="javascript: World.captureScreen()" data-icon="refresh" >Snapshot</a>
            <h1></h1>
        </div>
ソースコードをGitHubで見る

写真共有の処理はネイティブコードで行います。

    // tell native (urlListener) that user pressed 'Snapshot' button
    captureScreen: function captureScreenFn() {
        AR.platform.sendJSONObject({
            name: "button",
            action: "captureScreen"
        });
    },
ソースコードをGitHubで見る