The sample below exercises the XPLM300 map layer API. Note that it depends on the image below (map-sample-image.png) being located in your X-Plane/Resources/plugins directory. If the image is missing, you will see a “missing resource” warning in your Log.txt file (and of course the icon drawing functionality of the map layer will instead draw nothing).

#include "XPLMMap.h"
#include "XPLMGraphics.h"
#include <string.h>
#include <stdio.h>
#if IBM
	#include <windows.h>
#endif
#if LIN
	#include <GL/gl.h>
#elif __GNUC__
	#include <OpenGL/gl.h>
#else
	#include <GL/gl.h>
#endif

XPLMMapLayerID		g_layer = NULL;

static int	coord_in_rect(float x, float y, const float bounds_ltrb[4])  {	return ((x >= bounds_ltrb[0]) && (x < bounds_ltrb[2]) && (y >= bounds_ltrb[3]) && (y < bounds_ltrb[1])); }

void createOurMapLayer(const char * mapIdentifier, void * refcon);

static void prep_cache(         XPLMMapLayerID layer, const float * inTotalMapBoundsLeftTopRightBottom, XPLMMapProjectionID projection, void * inRefcon);
static void draw_markings(      XPLMMapLayerID layer, const float * inMapBoundsLeftTopRightBottom, float zoomRatio, float mapUnitsPerUserInterfaceUnit, XPLMMapStyle mapStyle, XPLMMapProjectionID projection, void * inRefcon);
static void draw_marking_icons( XPLMMapLayerID layer, const float * inMapBoundsLeftTopRightBottom, float zoomRatio, float mapUnitsPerUserInterfaceUnit, XPLMMapStyle mapStyle, XPLMMapProjectionID projection, void * inRefcon);
static void draw_marking_labels(XPLMMapLayerID layer, const float * inMapBoundsLeftTopRightBottom, float zoomRatio, float mapUnitsPerUserInterfaceUnit, XPLMMapStyle mapStyle, XPLMMapProjectionID projection, void * inRefcon);
static void will_be_deleted(    XPLMMapLayerID layer, void * inRefcon);

PLUGIN_API int XPluginStart(
							char *		outName,
							char *		outSig,
							char *		outDesc)
{
	strcpy(outName, "MapPlugin");
	strcpy(outSig, "xpsdk.examples.mapplugin");
	strcpy(outDesc, "A test plug-in that demonstrates and exercises the X-Plane 11 map API.");
	return 1;
}

PLUGIN_API void	XPluginStop(void)
{
	// Clean up our map layer: if we created it, we should be good citizens and destroy it before the plugin is unloaded
	if(g_layer)
	{
		// Triggers the will-be-deleted callback of the layer, causing g_layer to get set back to NULL
		XPLMDestroyMapLayer(g_layer);
	}
}

PLUGIN_API int XPluginEnable(void)
{
	// We want to create our layer in the standard map used in the UI (not other maps like the IOS).
	// If the map already exists in X-Plane (i.e., if the user has opened it), we can create our layer immediately.
	// Otherwise, though, we need to wait for the map to be created, and only *then* can we create our layers.
	if(XPLMMapExists(XPLM_MAP_USER_INTERFACE))
	{
		createOurMapLayer(XPLM_MAP_USER_INTERFACE, NULL);
	}
	// Listen for any new map objects that get created
	XPLMRegisterMapCreationHook(&createOurMapLayer, NULL);
	return 1;
}
PLUGIN_API void XPluginDisable(void) { }
PLUGIN_API void XPluginReceiveMessage(XPLMPluginID inFrom, int inMsg, void * inParam) { }

void createOurMapLayer(const char * mapIdentifier, void * refcon)
{
	if(!g_layer && // Confirm we haven't created our markings layer yet (e.g., as a result of a previous callback), or if we did, it's been destroyed
			!strcmp(mapIdentifier, XPLM_MAP_USER_INTERFACE)) // we only want to create a layer in the normal user interface map (not the IOS)
	{
		XPLMCreateMapLayer_t params;
		params.structSize = sizeof(XPLMCreateMapLayer_t);
		params.mapToCreateLayerIn = XPLM_MAP_USER_INTERFACE;
		params.willBeDeletedCallback = &will_be_deleted;
		params.prepCacheCallback = &prep_cache;
		params.showUiToggle = 1;
		params.refcon = NULL;
		params.layerType = xplm_MapLayer_Markings;
		params.drawCallback = &draw_markings;
		params.iconCallback = &draw_marking_icons;
		params.labelCallback = &draw_marking_labels;
		params.layerName = "Markings";
		// Note: this could fail (return NULL) if we hadn't already confirmed that params.mapToCreateLayerIn exists in X-Plane already
		g_layer = XPLMCreateMapLayer(&params);
	}
}

int s_num_cached_coords = 0;
#define MAX_COORDS (360 * 180)
float s_cached_x_coords[MAX_COORDS]; // The map x coordinates at which we will draw our icons; only the range [0, s_num_cached_coords) are valid
float s_cached_y_coords[MAX_COORDS]; // The map y coordinates at which we will draw our icons; only the range [0, s_num_cached_coords) are valid
float s_cached_lon_coords[MAX_COORDS]; // The real latitudes that correspond to our cached map (x, y) coordinates; only the range [0, s_num_cached_coords) are valid
float s_cached_lat_coords[MAX_COORDS]; // The real latitudes that correspond to our cached map (x, y) coordinates; only the range [0, s_num_cached_coords) are valid
float s_icon_width = 0; // The width, in map units, that we should draw our icons.

void prep_cache(XPLMMapLayerID layer, const float * inTotalMapBoundsLeftTopRightBottom, XPLMMapProjectionID projection, void * inRefcon)
{
	// We're simply going to cache the locations, in *map* coordinates, of all the places we want to draw.
	s_num_cached_coords = 0;
	for(int lon = -180; lon < 180; ++lon)
	{
		for(int lat =  -90; lat <  90; ++lat)
		{
			float x, y;
			const float offset = 0.25; // to avoid drawing on grid lines
			XPLMMapProject(projection, lat + offset, lon + offset, &x, &y);
			if(coord_in_rect(x, y, inTotalMapBoundsLeftTopRightBottom))
			{
				s_cached_x_coords[s_num_cached_coords] = x;
				s_cached_y_coords[s_num_cached_coords] = y;
				s_cached_lon_coords[s_num_cached_coords] = lon + offset;
				s_cached_lat_coords[s_num_cached_coords] = lat + offset;
				++s_num_cached_coords;
			}
		}
	}

	// Because the map uses true cartographical projections, the size of 1 meter in map units can change
	// depending on where you are asking about. We'll ask about the midpoint of the available bounds
	// and assume the answer won't change too terribly much over the size of the maps shown in the UI.
	const float midpoint_x = (inTotalMapBoundsLeftTopRightBottom[0] + inTotalMapBoundsLeftTopRightBottom[2]) / 2;
	const float midpoint_y = (inTotalMapBoundsLeftTopRightBottom[1] + inTotalMapBoundsLeftTopRightBottom[3]) / 2;
	// We'll draw our icons to be 5000 meters wide in the map
	s_icon_width = XPLMMapScaleMeter(projection, midpoint_x, midpoint_y) * 5000;
}

void draw_markings(XPLMMapLayerID layer, const float * inMapBoundsLeftTopRightBottom, float zoomRatio, float mapUnitsPerUserInterfaceUnit, XPLMMapStyle mapStyle, XPLMMapProjectionID projection, void * inRefcon)
{
	// The arbitrary OpenGL drawing done for our markings layer.
	// We will simply draw a green box around the icon; the icon itself will be enqueued when we get a callback to draw_marking_icons().

	XPLMSetGraphicsState(
			0 /* no fog */,
			0 /* 0 texture units */,
			0 /* no lighting */,
			0 /* no alpha testing */,
			1 /* do alpha blend */,
			1 /* do depth testing */,
			0 /* no depth writing */
	);

	glColor3f(0, 1, 0); // green

	const float half_width = s_icon_width / 2;
	const float half_height = half_width * 0.6667; // our images are in a 3:2 aspect ratio, so the height is 2/3 the width
	for(int coord = 0; coord < s_num_cached_coords; ++coord)
	{
		const float x = s_cached_x_coords[coord];
		const float y = s_cached_y_coords[coord];
		if(coord_in_rect(x, y, inMapBoundsLeftTopRightBottom))
		{
			// Draw the box around the icon (we use half the width and height, since the icons will be *centered* at this (x, y)
			glBegin(GL_LINE_LOOP);
			{
				glVertex2f(x - half_width, y + half_height);
				glVertex2f(x + half_width, y + half_height);
				glVertex2f(x + half_width, y - half_height);
				glVertex2f(x - half_width, y - half_height);
			}
			glEnd();
		}
	}
}

void draw_marking_icons(XPLMMapLayerID layer, const float * inMapBoundsLeftTopRightBottom, float zoomRatio, float mapUnitsPerUserInterfaceUnit, XPLMMapStyle mapStyle, XPLMMapProjectionID projection, void * inRefcon)
{
	for(int coord = 0; coord < s_num_cached_coords; ++coord)
	{
		const float x = s_cached_x_coords[coord];
		const float y = s_cached_y_coords[coord];
		if(coord_in_rect(x, y, inMapBoundsLeftTopRightBottom))
		{
			#define SAMPLE_IMG "Resources/plugins/map-sample-image.png"
			if(coord % 2)
			{
				XPLMDrawMapIconFromSheet(
						layer, SAMPLE_IMG,
						0, 0, // draw the image cell at (s, t) == (0, 0) (i.e., the bottom left cell in the sample image)
						2, 2, // our sample image is two image cell wide, and two image cells tall
						x, y,
						xplm_MapOrientation_Map, // Orient the icon relative to the map itself, rather than relative to the UI
						0, // Zero degrees rotation
						s_icon_width);
			}
			else
			{
				// Draw the image at cell (s, t) == (1, 1) (i.e., the top right cell in the sample image)
				XPLMDrawMapIconFromSheet(layer, SAMPLE_IMG, 1, 1, 2, 2, x, y, xplm_MapOrientation_Map, 0, s_icon_width);
			}
		}
	}
}

void draw_marking_labels(XPLMMapLayerID layer, const float * inMapBoundsLeftTopRightBottom, float zoomRatio, float mapUnitsPerUserInterfaceUnit, XPLMMapStyle mapStyle, XPLMMapProjectionID projection, void * inRefcon)
{
	if(zoomRatio >= 18) // don't label when zoomed too far out... everything will run together in a big, illegible mess
	{
		for(int coord = 0; coord < s_num_cached_coords; ++coord)
		{
			const float x = s_cached_x_coords[coord];
			const float y = s_cached_y_coords[coord];
			if(coord_in_rect(x, y, inMapBoundsLeftTopRightBottom))
			{
				char scratch_buffer[150];
				sprintf(scratch_buffer, "%0.2f / %0.2f Lat/Lon", s_cached_lat_coords[coord], s_cached_lon_coords[coord]);
				
				// The text will be centered at the (x, y) we pass in. But, instead of drawing the label in the center
				// of the icon, we'd really like the text to be shifted down *beneath* the icon we drew,
				// so we'll subtract some amount from the y coordinate
				const float icon_bottom = y - s_icon_width / 2;
				const float text_center_y = icon_bottom - (mapUnitsPerUserInterfaceUnit * icon_bottom / 2); // top of the text will touch the bottom of the icon
				XPLMDrawMapLabel(layer, scratch_buffer, x, text_center_y, xplm_MapOrientation_Map, 0);
			}
		}
	}
}

void will_be_deleted(XPLMMapLayerID layer, void * inRefcon)
{
	if(layer == g_layer)
		g_layer = NULL;
}

21 comments on “X-Plane 11 Map API Sample

  1. Thanks for the example.
    One short question though, Will the GL code be replaced once you will move to “full” Vulkan/Metal drawing framework, or can they co-exists, the new one only for XPlane 3D space and the map can work with “legacy” code even in the future.

    Thanks
    Saar

    1. Our _design goal_* is for the map, panel, and 2-d UI to keep working with plugin-based OpenGL code.

      I say “design goal” because we do not have this already coded, or even a working prototype, so if we find out that this is impossible due to the GL and Vulkan APIs, it won’t happen. But it is what we want to have happen and it will be our first attempt to build something.

    1. If you want to display that radar as an overlay for the regular map, you could do that. I could imagine the overlay being fully opaque, and hiding most of the map details beneath it. But you’ll still have the icons layers on top of your radar texture.

  2. This works great.

    If I want to draw a map icon scaled relative to the screen (same size, independent of zoom level), like the aircraft icons, what’s the best way?

    1. You’ll just need to know the map’s scale in map units* per meter (obtained via XPLMMapScaleMeter()). Take the reciprocal of that to get meters per map unit. Then, having decided how many map units you want to make your icon, just multiply it with the meters per unit to obtain a fixed size.

      Something like this:

      const xflt map_units_per_meter = XPLMMapScaleMeter(my_projection, x, y); // x, y in map units
      const xflt meters_per_map_unit = 1 / map_units_per_meter;
      const xflt desired_size_map_units = 50;
      const xflt desired_size_meters = desired_size_map_units * meters_per_map_unit;

      * Note that for the time being, it happens that a map unit is equivalent to a boxel (i.e., a DPI-scale-independent “pixel”) but at some point in the future, when you might be drawing on something like the Garmin in an instrument panel, that relationship might no longer hold.

      1. Makes sense, but for some reason I couldn’t get it to work. However in the draw icon callback the parameter “mapUnitsPerUserInterfaceUnit” (float32) is useful as and one can simply multiply the desired size with this value and have the icon always be the same size.

      1. Thanks. I was afraid of that 🙂

        I have a number of objects I want to put on the map, but they only have local coordinates. It seems like a lot of extra processing to convert them to lon/lat format first, and then one more time to map coordinates.

        Would a direct projection function be a possibility in the future?

        1. Um… I’m not an OpenGL expert, but I don’t know that it’s possible to create something like an OpenGL projection to replace mapping the coordinates, just because cartographic projections are “weird.”

          I’d write a wrapper function to do the projection into map coordinates once, then use it everywhere.

  3. will_be_deleted() doesn’t appear to make sense:

    Ignoring all the actual drawing and focusing on just the create/prep/delete callbacks:
    You’ll get
    1) createOurMapLayer(), when map is first opened (or, if already opened)
    2) immediate call to prep_cache()

    Move aircraft on map such that map loads new scenery and you’ll get
    3) another call to prep_cache() — makes sense, there are new bounds
    4) call to will_be_deleted() — huh?

    Continue to move aircraft and you’ll get more prep_cache() followed by will_be_deleted().

    The layer provided on every call is the same, so it’s not like you’re deleting one layer to create another layer.

    In the example code, will_be_deleted() simple sets global g_layer to null, but since that’s not actually used or referenced for drawing, it doesn’t really accomplish much.

    Documentation says “Optional callback to inform you this layer is being deleted (due to its owning map being destroyed).” But the map isn’t being destroyed when the aircraft is merely moved (if it were, then we should be seeing another Create).

Leave a Reply

Your email address will not be published. Required fields are marked *

Please do not report bugs in the blog comments.
Only bugs reported via the X-Plane Bug Reporter are tracked.