OpenSphere
latest

Developer Documentation

  • Contributing
  • Getting Started
  • Windows Development
  • Application Guide
  • ES6 Guide
  • Plugin Guide
  • Settings Guide
  • Deployment Guide
  • Developers’ Cookbook
    • Add Layer Page
    • SubMenu
      • Problem
      • Solution
      • Discussion
      • Full code
    • Logging
    • OpenStreetMap
    • Audio Notification
    • Pelias Search (Forward geocoding)
    • Metrics
    • Tracks
    • External Javascript Libraries
OpenSphere
  • Docs »
  • Developers’ Cookbook »
  • SubMenu
  • Edit on GitHub

SubMenu¶

Problem¶

Your plugin needs to show additional menu entries in a context menu (e.g. the Spatial menu that appears when you right click on a feature), but you don’t want to modify OpenSphere menu code for your specific case.

../../_images/submenuscreenshot.png

Solution¶

Extend the existing menu with Group, SubMenu and Item elements to provide your plugin specific functionality.

SubMenu Cookbook example - menu creation¶
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
    const menu = spatial.getMenu();
    if (menu) {
      const root = menu.getRoot();
      let group = root.find(MYGROUP);
      if (!group) {
        group = root.addChild({
          type: MenuItemType.GROUP,
          label: MYGROUP,
          tooltip: 'Added by cookbook submenu example',
          beforeRender: shouldShowGroup
        });
      }
      const submenu1 = group.addChild({
        type: MenuItemType.SUBMENU,
        label: 'SubMenu 1'
      });
      // Submenus can be nested
      const submenu2 = submenu1.addChild({
        type: MenuItemType.SUBMENU,
        label: 'SubSubMenu',

        // You can specify child menu items directly
        children: [{
          type: MenuItemType.ITEM,
          label: 'Item 1',
          sort: 10,
          handler: handleItem1
        }, {
          type: MenuItemType.ITEM,
          eventType: EventType.DO_SOMETHING,
          label: 'Item 2',
          sort: 30,
          handler: handleItem,
          beforeRender: visibleIfIsPointInSouthernHemisphere
        }]
      });

      // You can also add items programmatically
      submenu2.addChild({
        type: MenuItemType.ITEM,
        eventType: EventType.DO_ANOTHER_THING,
        label: 'Another item',
        sort: 20,
        handler: handleItem
      });
    }

Discussion¶

The Spatial menu (os.ui.menu.spatial.MENU) is extensible from plugins. You can then attach groups, separators, submenus and items (plain items, checkboxes, or radio buttons) to the root, or to sub-items. In the image and code, a group item is added to the root, then submenus are nested below that group, and some items are available for selection.

If your plugin has all items known in advance, its possible to use the children property to nest the whole structure, as shown here:

const menu = spatial.getMenu();
if (menu) {
  const root = menu.getRoot();
  let group = root.find(MYGROUP);
  if (!group) {
    group = root.addChild({
      type: MenuItemType.GROUP,
      label: MYGROUP,
      tooltip: 'Added by cookbook submenu example',
      beforeRender: shouldShowGroup,
      children:[{
        type: MenuItemType.SUBMENU,
        label: 'SubMenu 1',
        children:[{
          type: MenuItemType.SUBMENU,
          label: 'SubSubMenu',
          children: [{
            type: MenuItemType.ITEM,
            label: 'Item 1',
            sort: 10,
            handler: handleItem1
          }, {
            type: MenuItemType.ITEM,
            eventType: EventType.DO_SOMETHING,
            label: 'Item 2',
            sort: 30,
            handler: handleItem,
            beforeRender: visibleIfIsPointInSouthernHemisphere
          }, {
            type: MenuItemType.ITEM,
            eventType: EventType.DO_ANOTHER_THING,
            label: 'Another item',
            sort: 20,
            handler: handleItem
          }]
        }]
      }]
    });
  }

This is equivalent - you can mix and match the addChild function calls and nested children array as needed.

Note that items will be enabled and visible by default, which might not make sense for your use. Instead, you may want to selectively enable, or not display, some menu items, depending on the menu context. The usual way to do this is to set a beforeRender, as in visibleIfIsPointInSouthernHemisphere which is implemented as:

SubMenu Cookbook example - selectable visibility¶
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
 * If our item should be shown.
 * @param {Object|undefined} context The menu context.
 * @this {os.ui.menu.MenuItem}
 */
const visibleIfIsPointInSouthernHemisphere = function(context) {
  this.visible = ifIsPointInSouthernHemisphere(context);
};

/**
 * If feature associated with menu entry is a point in southern hemisphere.
 * @param {Object|undefined} context The menu context.
 * @return {boolean}
 */
const ifIsPointInSouthernHemisphere = function(context) {
  // Get feature associated with menu context
  const features = spatial.getFeaturesFromContext(context);
  if (features.length === 1) {
    const feature = features[0];
    const geom = feature.getGeometry();
    if (geom instanceof Point) {
      const coords = toLonLat(geom.getFlatCoordinates(), PROJECTION);
      if (coords[1] < 0) {
        return true;
      }
    }
  }
  return false;
};

As the name suggests, one menu entry will only be shown if the feature geometry is a point located in the southern hemisphere (i.e. negative latitude).

Full code¶

SubMenu Cookbook example - Full code¶
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
goog.declareModuleId('plugin.cookbook_submenu.CookbookSubmenu');

import {PROJECTION} from 'opensphere/src/os/map/map.js';
import AbstractPlugin from 'opensphere/src/os/plugin/abstractplugin.js';
import PluginManager from 'opensphere/src/os/plugin/pluginmanager.js';
import MenuItemType from 'opensphere/src/os/ui/menu/menuitemtype.js';
import * as spatial from 'opensphere/src/os/ui/menu/spatial.js';

const Point = goog.require('ol.geom.Point');
const {toLonLat} = goog.require('ol.proj');

const {default: MenuEvent} = goog.requireType('os.ui.menu.MenuEvent');


/**
 * Cookbook example of a submenu
 */
export default class CookbookSubmenu extends AbstractPlugin {
  /**
   * Constructor.
   */
  constructor() {
    super();
    this.id = ID;
    this.errorMessage = null;
  }

  /**
   * @inheritDoc
   */
  init() {
    const menu = spatial.getMenu();
    if (menu) {
      const root = menu.getRoot();
      let group = root.find(MYGROUP);
      if (!group) {
        group = root.addChild({
          type: MenuItemType.GROUP,
          label: MYGROUP,
          tooltip: 'Added by cookbook submenu example',
          beforeRender: shouldShowGroup
        });
      }
      const submenu1 = group.addChild({
        type: MenuItemType.SUBMENU,
        label: 'SubMenu 1'
      });
      // Submenus can be nested
      const submenu2 = submenu1.addChild({
        type: MenuItemType.SUBMENU,
        label: 'SubSubMenu',

        // You can specify child menu items directly
        children: [{
          type: MenuItemType.ITEM,
          label: 'Item 1',
          sort: 10,
          handler: handleItem1
        }, {
          type: MenuItemType.ITEM,
          eventType: EventType.DO_SOMETHING,
          label: 'Item 2',
          sort: 30,
          handler: handleItem,
          beforeRender: visibleIfIsPointInSouthernHemisphere
        }]
      });

      // You can also add items programmatically
      submenu2.addChild({
        type: MenuItemType.ITEM,
        eventType: EventType.DO_ANOTHER_THING,
        label: 'Another item',
        sort: 20,
        handler: handleItem
      });
    }
  }
}

/**
 * @type {string}
 */
const ID = 'cookbook_submenu';

/**
 * @type {string}
 */
const MYGROUP = 'Cookbook Group';

/**
 * Our event types
 * @enum {string}
 */
const EventType = {
  DO_SOMETHING: 'cookbook:do_y',
  DO_ANOTHER_THING: 'cookbook:do_x'
};

/**
 * If our example group should be shown.
 * @param {Object|undefined} context The menu context.
 * @this {os.ui.menu.MenuItem}
 */
const shouldShowGroup = function(context) {
  // This shows always, and could just be omitted from the menu item definition.
  // Your code could use the menu context if needed.
  this.visible = true;
};

/**
 * If our item should be shown.
 * @param {Object|undefined} context The menu context.
 * @this {os.ui.menu.MenuItem}
 */
const visibleIfIsPointInSouthernHemisphere = function(context) {
  this.visible = ifIsPointInSouthernHemisphere(context);
};

/**
 * If feature associated with menu entry is a point in southern hemisphere.
 * @param {Object|undefined} context The menu context.
 * @return {boolean}
 */
const ifIsPointInSouthernHemisphere = function(context) {
  // Get feature associated with menu context
  const features = spatial.getFeaturesFromContext(context);
  if (features.length === 1) {
    const feature = features[0];
    const geom = feature.getGeometry();
    if (geom instanceof Point) {
      const coords = toLonLat(geom.getFlatCoordinates(), PROJECTION);
      if (coords[1] < 0) {
        return true;
      }
    }
  }
  return false;
};

/**
 * Process a menu item
 * @param {MenuEvent} event The menu event.
 */
const handleItem1 = function(event) {
  alert('cookbook_submenu item1 selected');
};

/**
 * Process a menu item
 * @param {MenuEvent} event The menu event.
 */
const handleItem = function(event) {
  // event provides context for the elected item
  const eventType = event.type;
  alert('cookbook_submenu item selected:' + eventType);
};

// add the plugin to the application
PluginManager.getInstance().addPlugin(new CookbookSubmenu());
Next Previous

© Copyright 2020 Revision 46dfd223.

Built with Sphinx using a theme provided by Read the Docs.