Drawing our lines - WazeDev/WazeDev.github.io GitHub Wiki
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.
- 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.
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.
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.
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.
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.
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.
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.
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
After implementing these changes, test the functionality:
- Enable the script and select a segment - you should see a red highlight
- Select multiple segments - all should be highlighted simultaneously
- Disable the script - highlights should disappear immediately
- 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
// ==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!`);
}
})();Then check out the final version of the script wiht JSDOC support here!