Displaying Floor Plans

Now that we have a building outline drawn on the screen, the next step is to download and display the floorplan for that floor. In this section we’ll download a floorplan from the NEON Location Service and display it as a ground overlay on google maps.

In the same way we kept track of the outlines drawn to the screen with a hashmap, we’ll also create a hashmap with the floorplan ID and the GroundOverlay that is currently drawn to the map.

private ConcurrentHashMap<String, GroundOverlay> floorPlanMap = new ConcurrentHashMap<>();

Let’s implement INeonFloorPlanListener, which will provide us with our images:

public class MapsActivity extends AppCompatActivity implements OnMapReadyCallback, INeonLocationListener, INeonEventListener, INeonBuildingListener, INeonFloorPlanListener {

    private ConcurrentHashMap<String, GroundOverlay> floorPlanMap = new ConcurrentHashMap<>();

    @Override
    public void onComplete(NeonFloorPlan floorplan, ImageResult result) {
        
    }
}

Don’t forget to clear the hashmap on shutdown:

private void shutdown()
{
    disconnectFromNeonAPI();

    if (dataLooper != null) {
        dataLooper.quit();
        dataLooper = null;
    }

    mainHandler.removeCallbacks(loadBuildingsRunnable);

    buildingHashMap.clear();
    floorPlanMap.clear();
}

Now, when we get the NeonBuildingFloor in our INeonBuildingListener, let’s check if there is a floorplan to go along with it and draw that too:

@Override
public void onComplete(List<NeonBuilding> buildings, DownloadResult result) {

    if(result == DownloadResult.SUCCESS)
    {
        for (final NeonBuilding building : buildings) {
            //check if the building has already been drawn to the screen
            if (buildingHashMap.containsKey(building.getID()))
                continue;

            //since we are drawing for the first time, get the ground floor for display
            final NeonBuildingFloor buildingFloor = building.getFloor(0);

            //draw the outline of this floor to the screen
            //since this is in a background thread, we draw on the UI thread
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    drawOutline(building, buildingFloor);
                }
            });

            //if the building floor has a floor plan image, we should download it and then draw it to the screen
            //since this requires network and bitmap decoding, we put in on the dataHandler thread
            if (buildingFloor.hasFloorPlan())
                NeonEnvironment.downloadFloorPlan(getApplicationContext(), buildingFloor, dataHandler, this);
        }
    }
    else
        Log.i("NEONSampleApp","result failed with: "+result.toString());
}

Then we fill in the INeonFloorPlanListener to draw the floorplan to the screen if it returns SUCCESS, and then a function to draw the floorplan to the map:

NOTE: The NEON floorplan image format is slightly different than the input that Google Maps expects. Google Maps ground overlays can only handle square images, while placing an image with our control points editor can technically produce an image that is sheared to better fit the outline. If you use control points, there may be some discrepancy between the floorplan and outline due to the limitations in the Google Maps display.

/**
    * The floorPlanListener receives a floorplan bitmap that can be saved or drawn to the screen
    * A NeonFloorPlan includes the bitmap, a helper function to clip the bitmap to the floor outlines,
    * and LatLongs for the corners of the image that georeference it to the screen. NEON allows these
    * four corners to be non-square, but google maps does not support this, so we approximate by creating
    * a bearing along the line between the topLeft and topRight corners.  If the image was warped when placed,
    * this will lead to a placement error on the screen.
    */
    @Override
    public void onComplete(final NeonFloorPlan floorplan, ImageResult result) {

        //if the download was successful, attempt to draw the image to the map
        if (result == ImageResult.SUCCESS) {
            Log.i("NEONSampleApp", "Got image with ID: " + floorplan.getID());

            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    drawFloorPlan(floorplan);
                }
            });
        } else Log.i("NEONSampleApp", "Failed to get image with error: " + result);
    }

/**
    * Draws a floor plan image to the map
    */
private void drawFloorPlan(NeonFloorPlan floorPlan) {
    //set up Location objects for the topLeft, topRight, and bottomRight corners
    final Location topLeft = new Location("topLeft");
    topLeft.setLatitude(floorPlan.getTopLeft().Latitude);
    topLeft.setLongitude(floorPlan.getTopLeft().Longitude);
    final Location topRight = new Location("topRight");
    topRight.setLatitude(floorPlan.getTopRight().Latitude);
    topRight.setLongitude(floorPlan.getTopRight().Longitude);
    final Location bottomRight = new Location("bottomRight");
    bottomRight.setLatitude(floorPlan.getBottomRight().Latitude);
    bottomRight.setLongitude(floorPlan.getBottomRight().Longitude);

    //Get the bitmap of the image, clipped to the floor outline
    //This will make segments of the image outside the outline
    //transparent.  Looks nicer on a map.
    final Bitmap bm = floorPlan.getBitmapClippedToFloor();

    //Puts the image on the map
    //Since google does not allow shear on the image
    //we approximate by creating a bearing along the
    //line between the topLeft and topRight corners
    //and scaling it by the distance.  Not perfect, but
    //the best we can do in the Google API.
    GroundOverlay go = mMap.addGroundOverlay(new GroundOverlayOptions()
            .image(BitmapDescriptorFactory.fromBitmap(bm))
            .anchor(0, 0)   //anchor at the topLeft
            .position(new LatLng(topLeft.getLatitude(), topLeft.getLongitude()), //position it at the topLeft
                    topRight.distanceTo(topLeft),   //scale by distance between topRight and topLeft
                    bottomRight.distanceTo(topRight))   //scale by distance between topRight and BottomRight
            .bearing(-(90 - topLeft.bearingTo(topRight)))); //rotate by the bearing between topLeft and topRight

    floorPlan.getBitmap().recycle();    //since we are done with the bitmaps, we should recycle them
    bm.recycle();

    floorPlanMap.put(floorPlan.getID(), go);    //store the groundOverlay in the hashmap
}

Floor Plan