Drawing our lines - WazeDev/WazeDev.github.io GitHub Wiki

Interacting with the Map using the SDK

Now that we have a solid settings foundation, let's make our script interact with the WME map itself! This is where scripts become truly useful - by providing visual feedback and enhancing the editing experience.

In this section, we'll create a segment highlighter that:

  • Responds to user selections by listening to WME events
  • Provides visual feedback with custom map layers and styling
  • Manages resources efficiently by cleaning up when not needed
  • Integrates with our settings to give users control

This introduces core concepts you'll use in almost every WME script: event handling, map layers, geometry manipulation, and real-time visual feedback.

At this point we have a script that reads & loads some user information into a tab, and saves & loads the checked state of our Enabled checkbox. Now we’ll learn how to make your script react when you select segments on the map: it will highlight the selected segments in red, and remove the highlights when nothing is selected or the script is turned off.

Logic Overview

  • When enabled:
    • We listen for map selection changes using the SDK.
    • When you select a segment, the script gets its geometry (shape/line) and draws a highlight on it.
    • If nothing is selected, remove highlights.
  • When disabled:
    • The script stops listening for selection changes.
    • Removes any highlights.

Step 1: Declare Layer Name

Before you start, you’ll want to pick a name for your highlight layer and something to track if your event listener is active.

This makes your code easier to update later and helps keep track of things.

(function () {
  'use strict';

  const SCRIPT_NAME = GM_info.script.name; // Get the Script name from the @name tag of the UserScript Meta Keys
  const STORAGE_KEY = 'wazedev_Settings'; // Local Storage key

  let wmeSDK; // Declare wmeSDK globally
  let settings = {}; // Global settings object
  
  const SELECTED_LAYER_NAME = "wazedev_SelectedLayer"; // Declare map layer name globally
  let selectionSubscription = null; // Declare variable to store the event listener state

  // <rest of the code>
})();

What/Why:

  • The layer name is how you’ll refer to your custom highlight layer throughout your script.
  • The selectionSubscription variable will store the event listener, so you can remove it cleanly later.

Step 2: Create Highlight Layer (with Basic Styling)

Now, create a layer that will display your highlights. You only need to do this once when your script starts, we can put this inside the main() method after setupEventHandlers();

async function main() {
 ...
 // Setup event handlers for checkboxes
 setupEventHandlers();

// Add the Layer to the Map!
 wmeSDK.Map.addLayer({
    layerName: SELECTED_LAYER_NAME,
    styleRules: [{
        style: {
            strokeColor: "red",      // Highlight color (red)
            strokeWidth: 12,         // Thickness (12px)
            strokeLinecap: "round",  // Rounded ends
            fillOpacity: 0           // No fill needed for lines
        }
    }]
 });

 console.log(`${SCRIPT_NAME}: Settings initialized!`);
}

💡 Tip: Full list of FeatureStyles can be found at SDK: Interface FeatureStyle

What/Why:

  • This sets up a new drawing layer.
  • The style makes highlights easy to see: a thick (12px), red line with rounded ends.
  • fillOpacity: 0 means no fill color (just the line itself).
  • Creating the layer only once avoids errors from trying to re-add it.

Step 3: Attach Selection Change Handler

Now, set up your script to listen when a user selects (or unselects) something.

This should be done when your script is enabled (e.g. a checkbox is on).

function enableSelectionHighlighting() {
    if (selectionSubscription) {
      selectionSubscription(); // Safely cleanup previous before subscribing!
      selectionSubscription = null;
    }
    selectionSubscription = wmeSDK.Events.on({
      eventName: 'wme-selection-changed',
      eventHandler: drawSelectionHighlight,
    });
  }

When your script is turned off, undo any listening and clear highlights:

function disableSelectionHighlighting() {
    if (selectionSubscription) {
      selectionSubscription(); // This unregisters the event handler!
      selectionSubscription = null;
    }
    wmeSDK.Map.removeAllFeaturesFromLayer({ layerName: SELECTED_LAYER_NAME });
  }

What/Why:

  • The event handler will be triggered every time your selection changes.
  • When you’re done (checkbox off), we unsubscribe (turn off) the event handler so the script stops reacting.

Step 4: Drawing Highlights

This function actually does the work of displaying highlights.

When the selection changes, it erases the old highlight and draws a new one, if needed, but only if the Enable checkbox is "on".

function drawSelectionHighlight() {
    // Check if the script's "Enable" setting is ON
    if (!settings.Enabled) {
        wmeSDK.Map.removeAllFeaturesFromLayer({ layerName: SELECTED_LAYER_NAME });
        return;
    }

    // Get the current selection object from the SDK
    const selection = wmeSDK.Editing.getSelection();

    // Always remove all previous features from our layer
    wmeSDK.Map.removeAllFeaturesFromLayer({ layerName: SELECTED_LAYER_NAME });

    // Check if there are any segments actively selected in the selection object
    if (!selection || !selection.ids || selection.objectType !== 'segment') return;

    // Add the selected segments as features to the layer
    selection.ids.forEach((segmentId) => {
        const segment = wmeSDK.DataModel.Segments.getById({ segmentId });
        if (segment && segment.geometry) {
            wmeSDK.Map.addFeatureToLayer({
                layerName: SELECTED_LAYER_NAME,
                feature: {
                    id: `highlighted-${segmentId}`,
                    type: 'Feature',
                    geometry: segment.geometry,
                    properties: {},
                },
            });
        }
    });
}

What/Why:

  • Erases highlights so you only see the latest selection.
  • Checks that you actually have a segment selected (skips places/nodes for now).
  • For every selected segment, gets its shape and adds it as a feature to your highlight layer.
  • The result: whenever you select a segment, it’s outlined in red.

Step 5: Tie to Settings (Checkbox Logic)

Connect highlighting to your script’s on/off setting.

Whenever your enable checkbox or option is changed, activate or deactivate highlighting.

function onEnabledCheckboxChanged(value) {
    if (settings.Enabled) {
      enableSelectionHighlighting();
      drawSelectionHighlight();
    } else {
      disableSelectionHighlighting();
    }
  }

So now let's hook it up in the setupEventHandlers() method:

function setupEventHandlers() {
    // Find all checkboxes with the settings class in the DOM
    const checkboxes = document.querySelectorAll('.wazedevSettingsCheckbox');
    
    // For each settings checkbox, add a change event listener
    checkboxes.forEach((checkbox) => {
        checkbox.addEventListener('change', function () {
            // Extract the setting name from the checkbox ID by removing the 'wazedev' prefix
            // Example: 'wazedevEnabled' -> 'Enabled'
            const settingName = this.id.substring(7);
            
            // Update the corresponding value in the settings object
            settings[settingName] = this.checked;
            
            // Save the updated settings (persist changes, e.g., to storage)
            saveSettings();
            
            // If the 'Enabled' setting changed, trigger additional actions to update highlighting
            if (settingName === 'Enabled') {
                onEnabledCheckboxChanged(this.checked);
            }
        });
    });
}

What/Why:

  • This lets users control whether the highlight feature is on.
  • If enabled, highlighting is immediately shown for any currently selected segments.
  • If disabled, the script stops reacting and removes any highlights.

Step 6: Clean-up

Whenever your script disables (checkbox off), disableSelectionHighlighting() makes sure:

  • The script isn’t listening for selection changes anymore.
  • All highlight lines are immediately removed from the map.

What/Why:

  • Prevents your script from using resources it doesn’t need.
  • Keeps the map clean with no leftover highlights.

Key Concepts Introduced

Event-Driven Map Interaction:

  • WME fires events when selections change (wme-selection-changed)
  • Event subscriptions return cleanup functions for proper resource management
  • Always check if script is enabled before processing events

Map Layers and Styling:

  • Custom layers separate your features from WME's built-in elements
  • Style rules define appearance (color, width, opacity, etc.)
  • Features are GeoJSON-compliant objects with geometry and properties

Geometry and Features:

  • Segments have geometry objects that define their shape on the map
  • Features combine geometry with styling to create visual elements
  • Layer management prevents memory leaks and visual clutter

Resource Management:

  • Event listeners consume memory and CPU - clean them up when not needed
  • Remove all features from layers when disabling functionality
  • Subscription pattern allows easy enable/disable of event handling

Testing Your Map Integration

After implementing these changes, test the functionality:

  1. Enable the script and select a segment - you should see a red highlight
  2. Select multiple segments - all should be highlighted simultaneously
  3. Disable the script - highlights should disappear immediately
  4. Select other objects (places, nodes) - no highlights should appear

Expected Behavior:

  • Highlights appear instantly when selecting segments
  • Only segments are highlighted (places/nodes are ignored)
  • Highlights are removed when selection changes or script is disabled
  • Console shows no errors during selection changes

Troubleshooting:

  • No highlights appear: Check console for errors, verify layer was created successfully
  • Highlights don't disappear: Ensure disableSelectionHighlighting() is called when disabling
  • Multiple highlights persist: Verify removeAllFeaturesFromLayer() is called before adding new ones
  • Script affects performance: Check that event listeners are properly cleaned up when disabled

Our script should now look like the following

// ==UserScript==
// @name         WazeDev First Script              // Display name in Tampermonkey dashboard
// @namespace    http://tampermonkey.net/          // Unique identifier (use your GreasyFork profile for updates)
// @version      0.1                               // Version number for update tracking
// @description  Learning to script!               // Brief description of functionality
// @author       You                               // Your name or username
// @include      https://beta.waze.com/*           // Run on beta WME (testing environment)
// @include      https://www.waze.com/editor*      // Run on production WME
// @include      https://www.waze.com/*/editor*    // Run on localized WME URLs
// @exclude      https://www.waze.com/user/editor* // Don't run on user profile pages
// @exclude      https://www.waze.com/editor/sdk/* // Don't run on SDK documentation
// @grant        none                              // No special permissions needed
// ==/UserScript==

(function () {
  'use strict';

  const SCRIPT_NAME = GM_info.script.name; // Get the Script name from the @name tag of the UserScript Meta Keys
  const STORAGE_KEY = 'wazedev_Settings'; // Local Storage key

  let wmeSDK; // Declare wmeSDK globally
  let settings = {}; // Global settings object

  const SELECTED_LAYER_NAME = 'wazedev_SelectedLayer'; // Declare map layer name globally
  let selectionSubscription = null; // Declare variable to store the event listener state

  if (window.SDK_INITIALIZED) {
    console.log(`${SCRIPT_NAME}: SDK initialized...`);

    window.SDK_INITIALIZED.then(bootstrap).catch((err) => {
      console.error(`${SCRIPT_NAME}: SDK initialization failed`, err);
    });
  } else {
    console.warn(`${SCRIPT_NAME}: SDK_INITIALIZED is undefined`);
  }

  function bootstrap() {
    try {
      wmeSDK = getWmeSdk({
        scriptId: SCRIPT_NAME.replaceAll(' ', ''),
        scriptName: SCRIPT_NAME,
      });

      Promise.all([wmeReady()])
        .then(() => {
          console.log(`${SCRIPT_NAME}: All dependencies are ready.`);
          init();
        })
        .catch((error) => {
          console.error(`${SCRIPT_NAME}: Error during bootstrap`, error);
        });
    } catch (error) {
      console.error(`${SCRIPT_NAME}: Failed to initialize SDK`, error);
    }
  }

  function wmeReady() {
    return new Promise((resolve) => {
      if (wmeSDK.State.isReady()) {
        console.log(`${SCRIPT_NAME}: WME is ready.`);
        resolve();
      } else {
        console.log(`${SCRIPT_NAME}: Waiting for WME to be ready...`);
        wmeSDK.Events.once({ eventName: 'wme-ready' })
          .then(() => {
            console.log(`${SCRIPT_NAME}: WME is ready now.`);
            resolve();
          })
          .catch((error) => {
            console.error(`${SCRIPT_NAME}: Error while waiting for WME to be ready:`, error);
          });
      }
    });
  }

  function init() {
    console.log(`${SCRIPT_NAME}: Script initialized successfully!`);

    // Build HTML content for our tab
    const section = document.createElement('div');
    section.innerHTML = `
    <div>
        <h2>Our First Script!</h2>
        <section>
            <input type="checkbox" id="wazedevEnabled" class="wazedevSettingsCheckbox">
            <label for="wazedevEnabled">Enable WazeDev Script</label>
        </section>
        <hr>
        <section>
            <h3>User Info</h3>
            Username: <a href="#" id="wazedevUsernameLink" target="_blank"><span id="wazedevUsername"></span></a><br>
            Rank: <span id="wazedevRank"></span><br>
            Area manager: <span id="wazedevAM"></span><br>
            Country manager: <span id="wazedevCM"></span>
        </section>
        <hr>
        <section>
            <h3>Detailed Profile</h3>
            <div id="wazedevProfileLoading">Loading profile data...</div>
            <div id="wazedevProfileData" style="display: none;">
                Total Edits: <span id="wazedevTotalEdits"></span><br>
                Today's Edits: <span id="wazedevTodayEdits"></span>
            </div>
        </section>
        <hr>
        <section id="wazedevEditsByType" style="display: none;">
            <h3>Edits by Type</h3>
            Segments: <span id="wazedevSegments"></span><br>
            Venues: <span id="wazedevVenues"></span><br>
            Map Problems: <span id="wazedevMapProblems"></span><br>
            Update Requests: <span id="wazedevUpdateRequests"></span><br>
            Place Update Requests: <span id="wazedevPlaceUpdateRequests"></span><br>
            Segment House Numbers: <span id="wazedevSegmentHouseNumbers"></span>
        </section>
    </div>
`;

    // Register the script tab using the SDK
    wmeSDK.Sidebar.registerScriptTab()
      .then(({ tabLabel, tabPane }) => {
        tabLabel.textContent = 'WazeDev';
        tabLabel.title = SCRIPT_NAME;
        tabPane.appendChild(section);

        // Initialize the main() function after tab is created
        main();
      })
      .catch((error) => {
        console.error(`${SCRIPT_NAME}: Error creating script tab`, error);
      });
  }

  // Save settings to localStorage
  function saveSettings() {
    try {
      const localSettings = { Enabled: settings.Enabled };
      localStorage.setItem(STORAGE_KEY, JSON.stringify(localSettings));
      console.log(`${SCRIPT_NAME}: Settings saved`, localSettings);
    } catch (error) {
      console.error(`${SCRIPT_NAME}: Error saving settings:`, error);
    }
  }

  // Load settings from localStorage
  function loadSettings() {
    try {
      const loadedSettings = JSON.parse(localStorage.getItem(STORAGE_KEY));
      const defaultSettings = { Enabled: false };
      settings = loadedSettings ? loadedSettings : defaultSettings;

      // Add missing default settings
      for (const prop in defaultSettings) {
        if (!settings.hasOwnProperty(prop)) {
          settings[prop] = defaultSettings[prop];
        }
      }
      console.log(`${SCRIPT_NAME}: Settings loaded`, settings);
    } catch (error) {
      console.error(`${SCRIPT_NAME}: Error loading settings:`, error);
      settings = { Enabled: false };
    }
  }

  // Helper to set checkbox state
  function setChecked(checkboxId, checked) {
    const checkbox = document.getElementById(checkboxId);
    if (checkbox) checkbox.checked = checked;
  }

  // Setup event handlers for our controls
  function setupEventHandlers() {
    // Find all checkboxes with the settings class in the DOM
    const checkboxes = document.querySelectorAll('.wazedevSettingsCheckbox');

    // For each settings checkbox, add a change event listener
    checkboxes.forEach((checkbox) => {
      checkbox.addEventListener('change', function () {
        // Extract the setting name from the checkbox ID by removing the 'wazedev' prefix
        // Example: 'wazedevEnabled' -> 'Enabled'
        const settingName = this.id.substring(7);

        // Update the corresponding value in the settings object
        settings[settingName] = this.checked;

        // Save the updated settings (persist changes, e.g., to storage)
        saveSettings();

        // If the 'Enabled' setting changed, trigger additional actions to update highlighting
        if (settingName === 'Enabled') {
          onEnabledCheckboxChanged(this.checked);
        }
      });
    });
  }

  function enableSelectionHighlighting() {
    if (selectionSubscription) {
      selectionSubscription(); // Safely cleanup previous before subscribing!
      selectionSubscription = null;
    }
    selectionSubscription = wmeSDK.Events.on({
      eventName: 'wme-selection-changed',
      eventHandler: drawSelectionHighlight,
    });
  }

  function disableSelectionHighlighting() {
    if (selectionSubscription) {
      selectionSubscription(); // This unregisters the event handler!
      selectionSubscription = null;
    }
    wmeSDK.Map.removeAllFeaturesFromLayer({ layerName: SELECTED_LAYER_NAME });
  }

  function drawSelectionHighlight() {
    // Check if the script's "Enable" setting is ON
    if (!settings.Enabled) {
      wmeSDK.Map.removeAllFeaturesFromLayer({ layerName: SELECTED_LAYER_NAME });
      return;
    }

    // Get the current selection object from the SDK
    const selection = wmeSDK.Editing.getSelection();

    // Always remove all previous features from our layer
    wmeSDK.Map.removeAllFeaturesFromLayer({ layerName: SELECTED_LAYER_NAME });

    // Check if there are any segments actively selected in the selection object
    if (!selection || !selection.ids || selection.objectType !== 'segment') return;

    // Add the selected segments as features to the layer
    selection.ids.forEach((segmentId) => {
      const segment = wmeSDK.DataModel.Segments.getById({ segmentId });
      if (segment && segment.geometry) {
        wmeSDK.Map.addFeatureToLayer({
          layerName: SELECTED_LAYER_NAME,
          feature: {
            id: `highlighted-${segmentId}`,
            type: 'Feature',
            geometry: segment.geometry,
            properties: {},
          },
        });
      }
    });
  }

  function onEnabledCheckboxChanged(value) {
    if (settings.Enabled) {
      enableSelectionHighlighting();
      drawSelectionHighlight();
    } else {
      disableSelectionHighlighting();
    }
  }

  async function main() {
    // Load saved settings first
    loadSettings();

    // Set the default state of the "Enable" checkbox at script startup
    setChecked('wazedevEnabled', settings.Enabled);

    // Continue with user info/profile update
    const userSession = wmeSDK.State.getUserInfo();

    // Set basic user info
    document.getElementById('wazedevUsername').textContent = userSession.userName;
    document.getElementById('wazedevRank').textContent = userSession.rank;
    document.getElementById('wazedevAM').textContent = userSession.isAreaManager ? 'Yes' : 'No';
    document.getElementById('wazedevCM').textContent = userSession.isCountryManager ? 'Yes' : 'No';

    // Set profile link
    const profileLink = wmeSDK.DataModel.Users.getUserProfileLink({ userName: userSession.userName });
    document.getElementById('wazedevUsernameLink').href = profileLink;

    // Fetch detailed user profile
    try {
      const userProfile = await wmeSDK.DataModel.Users.getUserProfile({ userName: userSession.userName });

      document.getElementById('wazedevProfileLoading').style.display = 'none';
      document.getElementById('wazedevProfileData').style.display = 'block';
      document.getElementById('wazedevEditsByType').style.display = 'block';

      document.getElementById('wazedevTotalEdits').textContent = userProfile.totalEditCount.toLocaleString();
      const todayEdits = userProfile.dailyEditCount[userProfile.dailyEditCount.length - 1] || 0;
      document.getElementById('wazedevTodayEdits').textContent = todayEdits;

      const editTypes = userProfile.editCountByType;
      document.getElementById('wazedevSegments').textContent = editTypes.segments.toLocaleString();
      document.getElementById('wazedevVenues').textContent = editTypes.venues.toLocaleString();
      document.getElementById('wazedevMapProblems').textContent = editTypes.mapProblems.toLocaleString();
      document.getElementById('wazedevUpdateRequests').textContent = editTypes.updateRequests.toLocaleString();
      document.getElementById('wazedevPlaceUpdateRequests').textContent = editTypes.placeUpdateRequests.toLocaleString();
      document.getElementById('wazedevSegmentHouseNumbers').textContent = editTypes.segmentHouseNumbers.toLocaleString();

      console.log(`${SCRIPT_NAME}: Profile data loaded successfully`);
    } catch (error) {
      console.error(`${SCRIPT_NAME}: Error fetching user profile:`, error);
      document.getElementById('wazedevProfileLoading').textContent = 'Error loading profile data';
    }

    // Setup event handlers for checkboxes
    setupEventHandlers();

    // Add the Layer to the Map!
    wmeSDK.Map.addLayer({
      layerName: SELECTED_LAYER_NAME,
      styleRules: [
        {
          style: {
            strokeColor: 'red', // Highlight color (red)
            strokeWidth: 12, // Thickness (12px)
            strokeLinecap: 'round', // Rounded ends
            fillOpacity: 0, // No fill needed for lines
          },
        },
      ],
    });

    onEnabledCheckboxChanged(settings.Enabled); // Activate script effect if previously enabled

    console.log(`${SCRIPT_NAME}: Settings initialized!`);
  }
})();

Use a Modern code editors like VS Code, WebStorm, and many browser-based IDEs?

Then check out the final version of the script wiht JSDOC support here!

⚠️ **GitHub.com Fallback** ⚠️