WME Image Overlays

Makes it possible to add images as overlay on the Waze Map Editor

目前為 2017-05-01 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        WME Image Overlays
// @author      Tom 'Glodenox' Puttemans
// @namespace   http://www.tomputtemans.com/
// @description Makes it possible to add images as overlay on the Waze Map Editor
// @include     /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/.*$/
// @icon        data:image/png;base64,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
// @version     0.7
// @grant       none
// ==/UserScript==

// Based on the OpenLayers.Layer.Image class
OpenLayers.Layer.OverlayImage = OpenLayers.Class(OpenLayers.Layer, {
  isBaseLayer: false,
  url: null,
  extent: null,
  size: null,
  tile: null,
  rotation: null,
  key: null,
  initialize: function(name, url, extent, size, key, options) {
    options = options || {};
    this.url = url;
    this.extent = extent;
    this.maxExtent = extent;
    this.size = size;
    this.key = key;
    this.rotation = options.rotation || 0;
    OpenLayers.Layer.prototype.initialize.apply(this, [name, options]);
  },
  destroy: function() {
    if (this.tile) {
      this.removeTileMonitoringHooks(this.tile);
      this.tile.destroy();
      this.tile = null;
    }
    OpenLayers.Layer.prototype.destroy.apply(this, arguments);
  },
  setMap: function(map) {
    OpenLayers.Layer.prototype.setMap.apply(this, arguments);
  },
  moveTo:function(bounds, zoomChanged, dragging) {
    OpenLayers.Layer.prototype.moveTo.apply(this, arguments);
    var firstRendering = (this.tile == null);
    if (zoomChanged || firstRendering) {
      this.setTileSize();
      var ulPx = this.map.getLayerPxFromLonLat({
        lon: this.extent.left,
        lat: this.extent.top
      });

      if (firstRendering) {
        this.tile = new OpenLayers.Tile.Image(this, ulPx, this.extent, null, this.tileSize);
        this.addTileMonitoringHooks(this.tile);
      } else {
        this.tile.size = this.tileSize.clone();
        this.tile.position = ulPx.clone();
      }
      this.tile.draw();
      if (firstRendering) {
        this.setRotation(this.rotation);
      }
    }
  },
  shift: function(x, y) {
    this.extent = this.extent.add(x, y);
    var ulPx = this.map.getLayerPxFromLonLat({
      lon: this.extent.left,
      lat: this.extent.top
    });
    this.tile.position = ulPx.clone();
    this.tile.draw();
  },
  scale: function(factor) {
    this.extent = this.extent.scale(factor);
    this.setTileSize();
    var ulPx = this.map.getLayerPxFromLonLat({
      lon: this.extent.left,
      lat: this.extent.top
    });
    this.tile.position = ulPx.clone();
    this.tile.size = this.tileSize.clone();
    this.tile.draw();
  },
  rotate: function(rotation) {
    this.setRotation(this.rotation + rotation);
  },
  setTileSize: function() {
    var tileWidth = this.extent.getWidth() / this.map.getResolution();
    var tileHeight = this.extent.getHeight() / this.map.getResolution();
    this.tileSize = new OpenLayers.Size(tileWidth, tileHeight);
  },
  addTileMonitoringHooks: function(tile) {
    tile.onLoadStart = function() {
      this.events.triggerEvent("loadstart");
    };
    tile.events.register("loadstart", this, tile.onLoadStart);
    tile.onLoadEnd = function() {
      this.events.triggerEvent("loadend");
    };
    tile.events.register("loadend", this, tile.onLoadEnd);
    tile.events.register("unload", this, tile.onLoadEnd);
  },
  removeTileMonitoringHooks: function(tile) {
    tile.unload();
    tile.events.un({
      "loadstart": tile.onLoadStart,
      "loadend": tile.onLoadEnd,
      "unload": tile.onLoadEnd,
      scope: this
    });
  },
  setUrl: function(newUrl) {
    this.url = newUrl;
    this.tile.draw();
  },
  getURL: function(bounds) {
    return this.url;
  },
  setRotation: function(rotation) {
    this.rotation = rotation;
    this.tile.imgDiv.style.transform = 'rotate(' + rotation + 'deg)';
  },
  CLASS_NAME: "OpenLayers.Layer.OverlayImage"
});

function init(e) {
  if (e && e.user == null) {
    return;
  }
  if (document.getElementById('user-info') == null) {
    setTimeout(init, 500);
    log('user-info element not yet available, page still loading');
    return;
  }
  if (typeof Waze.loginManager === 'undefined') {
    setTimeout(init, 300);
    return;
  }
  if (!Waze.loginManager.hasUser()) {
    Waze.loginManager.events.register('login', null, init);
    Waze.loginManager.events.register('loginStatus', null, init);
    // Double check as event might have triggered already
    if (!Waze.loginManager.hasUser()) {
      return;
    }
  }

  var om_strings = {
    en: {
      tab_title: 'Image Overlays',
      add_image: 'Add image overlay',
      empty_list: 'No images added yet',
      name_missing: 'No name specified',
      hide_overlay: 'Hide overlay',
      opacity: 'Opacity',
      parent_map_layer: 'Parent map layer',
      parent_map_layer_help: 'This decides on top of which map layer the image overlay will be drawn',
      layer_hidden: 'Hidden',
      image_name: 'Name',
      image_edit: 'Edit image',
      image_remove: 'Remove image',
      import_image: 'Import image',
      import_image_description: 'You can now paste an image from your clipboard in the WME with Ctrl+V or select an image with the file input field below.',
      import_error: 'Could not import image, the image is probably too big to retrieve. If you used the clipboard, you may want to download the image and try the file input field above instead.',
      align_image: 'Align with map',
      align_image_description: "You can use the controls below to align the image overlay with the map. Use the 'Attach to map' button above to finish.",
      attach_image: 'Attach to map',
      cancel: 'Cancel'
    }
  };
  om_strings.en_GB = om_strings['en-US'] = om_strings.en;
  for (var i = 0; i < I18n.availableLocales.length; i++) {
    var locale = I18n.availableLocales[i];
    if (I18n.translations[locale]) {
      I18n.translations[locale].image_overlays = om_strings[locale];
    }
  }

  var tab = addTab();
  // Deal with events mode
  if (Waze.app.modeController) {
    Waze.app.modeController.model.bind('change:mode', function(model, modeId) {
      if (modeId == 0) {
        addTab(tab);
      }
    });
  }

  var layer = null;
  var currentBlob = null;
  var emptyList = document.createElement('span');
  emptyList.style.fontStyle = 'italic';
  emptyList.appendChild(document.createTextNode(I18n.t('image_overlays.empty_list')));
  tab.appendChild(emptyList);
  var imagesList = document.createElement('div');
  imagesList.className = 'result-list';
  imagesList.style.marginBottom = '1em';
  tab.appendChild(imagesList);
  getIndexedDB(function(db) {
    db.transaction(['overlays'], 'readonly')
      .objectStore('overlays')
      .openCursor()
      .addEventListener('success', function(e) {
      var cursor = e.target.result;
      if (cursor) {
        addImageOverlay(cursor.value.name, cursor.key);
        cursor.continue();
      }
    });
  });

  var panel = document.createElement('div');
  panel.className = 'hidden';
  panel.style.backgroundColor = '#f2f3f4';
  panel.style.padding = '15px';

  var importError = document.createElement('p');
  importError.className = 'hidden text-danger';
  importError.textContent = I18n.t('image_overlays.import_error');
  var pasteListener = function(e) {
    var items = e.clipboardData.items;
    for (var i = 0; i < items.length; ++i) {
      if (items[i].kind == 'file' && items[i].type.indexOf('image/') !== -1) {
        var blob = items[i].getAsFile();
        if (blob) {
          importError.classList.add('hidden');
          displayAlignPage(blob);
        } else {
          importError.classList.remove('hidden');
          Waze.map.resize();
        }
        break;
      }
    }
  };

  var cancelButton, importLabel, alignLabel, pinLabel;
  cancelButton = document.createElement('button');
  cancelButton.className = 'btn btn-default';
  cancelButton.style.position = 'absolute';
  cancelButton.style.right = '15px';
  cancelButton.style.fontSize = '14px';
  var cancelButtonIcon = document.createElement('i');
  cancelButtonIcon.className = 'fa fa-trash-o fa-fw';
  cancelButton.appendChild(cancelButtonIcon);
  cancelButton.appendChild(document.createTextNode(I18n.t('image_overlays.cancel')));
  cancelButton.addEventListener('click', function() {
    panel.classList.add('hidden');
    Waze.map.resize();
    removeLayer();
  });
  panel.appendChild(cancelButton);
  var breadcrumbs = document.createElement('div');
  breadcrumbs.className = 'text-center';
  breadcrumbs.style.marginBottom = '1.5em';
  var breadcrumbSeparator = document.createElement('i');
  breadcrumbSeparator.className = 'fa fa-fw fa-angle-right';
  importLabel = createBreadcrumb('download', I18n.t('image_overlays.import_image'), 'primary');
  breadcrumbs.appendChild(importLabel);
  breadcrumbs.appendChild(breadcrumbSeparator);
  alignLabel = createBreadcrumb('arrows-alt', I18n.t('image_overlays.align_image'), 'default');
  breadcrumbs.appendChild(alignLabel);
  breadcrumbs.appendChild(breadcrumbSeparator.cloneNode());
  pinLabel = createBreadcrumb('map-pin', I18n.t('image_overlays.attach_image'), 'default');
  breadcrumbs.appendChild(pinLabel);
  panel.appendChild(breadcrumbs);
  var description = document.createElement('p');
  description.className = 'text-center';
  panel.appendChild(description);
  var instructions = document.createElement('div');
  instructions.className = 'text-center';
  panel.appendChild(instructions);
  document.getElementById('map').insertBefore(panel, document.getElementById('map').firstChild);

  var addImageOverlayButton = document.createElement('button');
  addImageOverlayButton.className = 'btn btn-primary';
  var addSpan = document.createElement('span');
  addSpan.className = 'fa fa-plus';
  addSpan.style.marginRight = '5px';
  addImageOverlayButton.appendChild(addSpan);
  addImageOverlayButton.appendChild(document.createTextNode(I18n.t('image_overlays.add_image')));
  addImageOverlayButton.addEventListener('click', displayImportPage);
  tab.appendChild(addImageOverlayButton);
  var hideOverlayButton = document.createElement('button');
  hideOverlayButton.className = 'btn btn-default hidden';
  hideOverlayButton.style.float = 'right';
  hideOverlayButton.textContent = I18n.t('image_overlays.hide_overlay');
  hideOverlayButton.addEventListener('click', removeLayer);
  tab.appendChild(hideOverlayButton);

  var layerControls = document.createElement('div');
  layerControls.className = 'hidden clearfix';
  layerControls.style.marginTop = '8px';
  var opacityRange = document.createElement('input');
  opacityRange.type = 'range';
  opacityRange.min = 0;
  opacityRange.max = 50;
  opacityRange.value = 50;
  opacityRange.id = 'imageOverlaysOpacity';
  var opacityLabel = document.createElement('label');
  opacityLabel.textContent = I18n.t('image_overlays.opacity');
  opacityLabel.htmlFor = opacityRange.id;
  layerControls.appendChild(opacityLabel);
  layerControls.appendChild(opacityRange);
  var rangeListener = function() {
    if (layer && layer.key) {
      getIndexedDB(function(db) {
        var objectStore = db.transaction(['overlays'], 'readwrite').objectStore('overlays');
        objectStore.get(layer.key).addEventListener('success', function(e) {
          var overlay = e.target.result;
          overlay.opacity = opacityRange.value / 50;
          objectStore.put(overlay, layer.key).addEventListener('success', function() {
            layer.setOpacity(opacityRange.value / 50);
          });
        });
      });
    } else if (layer) {
      layer.setOpacity(opacityRange.value / 50);
    }
  };
  opacityRange.addEventListener('input', rangeListener);
  opacityRange.addEventListener('change', rangeListener);
  var parentLayer = document.createElement('select');
  parentLayer.className = 'form-control';
  parentLayer.id = 'imageOverlaysParentLayer';
  Waze.map.events.on({
    addlayer: updateParentLayer,
    removelayer: updateParentLayer,
    changelayer: updateParentLayer
  });
  parentLayer.addEventListener('change', function() {
    if (layer && layer.key) {
      getIndexedDB(function(db) {
        var objectStore = db.transaction(['overlays'], 'readwrite').objectStore('overlays');
        objectStore.get(layer.key).addEventListener('success', function(e) {
          var overlay = e.target.result;
          overlay.layerTarget = parentLayer.value;
          objectStore.put(overlay, layer.key).addEventListener('success', function() {
            var targetIndex = Waze.map.getLayerIndex(Waze.map.getLayersByName(parentLayer.value)[0]) + 1;
            Waze.map.setLayerIndex(layer, targetIndex);
          });
        });
      });
    } else if (layer) {
      var targetIndex = Waze.map.getLayerIndex(Waze.map.getLayersByName(parentLayer.value)[0]) + 1;
      Waze.map.setLayerIndex(layer, targetIndex);
    }
  });
  var parentLayerLabel = document.createElement('label');
  parentLayerLabel.textContent = I18n.t('image_overlays.parent_map_layer') + ' ';
  parentLayerLabel.htmlFor = parentLayer.id;
  parentLayerLabel.style.marginTop = '10px';
  var parentLayerHelp = document.createElement('i');
  parentLayerHelp.className = 'waze-tooltip';
  parentLayerHelp.title = I18n.t('image_overlays.parent_map_layer_help');
  $(parentLayerHelp).tooltip();
  parentLayerLabel.appendChild(parentLayerHelp);
  layerControls.appendChild(parentLayerLabel);
  layerControls.appendChild(parentLayer);
  tab.appendChild(layerControls);

  var versionBlock = document.createElement('p');
  versionBlock.style.fontSize = '0.9em';
  versionBlock.style.marginTop = '10px';
  var versionInfo = document.createElement('a');
  versionInfo.appendChild(document.createTextNode(GM_info.script.name + ' (v' + GM_info.script.version + ')'));
  versionInfo.href = 'https://greasyfork.org/scripts/29381-wme-image-overlays';
  versionInfo.target = '_blank';
  versionBlock.appendChild(versionInfo);
  tab.appendChild(versionBlock);

  function addImageOverlay(name, key, selected) {
    emptyList.classList.add('hidden');
    var container = document.createElement('div');
    container.dataset.key = key;
    container.className = 'result session-available';
    if (selected) {
      container.style.fontWeight = '700';
    }
    var remove = document.createElement('button');
    remove.style.fontSize = '14px';
    remove.style.float = 'right';
    remove.style.fontWeight = 'normal';
    remove.style.marginTop = '-6px';
    remove.className = 'fa fa-trash-o';
    remove.addEventListener('click', function(e) {
      e.stopPropagation();
      getIndexedDB(function(db) {
        db.transaction(['overlays'], 'readwrite').objectStore('overlays').delete(key).addEventListener('success', function(e) {
          container.parentNode.removeChild(container);
          removeLayer();
        });
      });
    });
    container.appendChild(remove);
    var nameContainer = document.createElement('div');
    var rename = document.createElement('button');
    rename.style.fontSize = '14px';
    rename.style.float = 'right';
    rename.style.fontWeight = 'normal';
    rename.style.marginTop = '-6px';
    rename.style.marginLeft = '4px';
    rename.className = 'fa fa-pencil';
    rename.addEventListener('click', function(e) {
      e.stopPropagation();
      getIndexedDB(function(db) {
        var objectStore = db.transaction(['overlays'], 'readwrite').objectStore('overlays');
        objectStore.get(key).addEventListener('success', function(e) {
          var overlay = e.target.result;
          var response = prompt('Please enter a new name for this image overlay', overlay.name);
          if (response && response.length > 0) {
            overlay.name = response;
            objectStore.put(overlay, key).addEventListener('success', function() {
              nameContainer.textContent = overlay.name;
              nameContainer.style.fontStyle = '';
            });
          }
        });
      });
    });
    container.appendChild(rename);
    if (name && name.length > 0) {
      nameContainer.textContent = name;
    } else {
      nameContainer.style.fontStyle = 'italic';
      nameContainer.textContent = I18n.t('image_overlays.name_missing');
    }
    container.appendChild(nameContainer);
    container.addEventListener('click', function() {
      getIndexedDB(function(db) {
        db.transaction(['overlays'], 'readonly').objectStore('overlays').get(key).addEventListener('success', function(e) {
          var overlay = e.target.result;
          overlay.extent = new OL.Bounds(overlay.extent);
          overlay.key = key;
          displayImageOverlay(overlay);
        });
      });
    });
    imagesList.appendChild(container);
  }

  function updateParentLayer(currentLayer) {
    if (!currentLayer || typeof currentLayer == 'object') {
      currentLayer = parentLayer.value;
    }
    while (parentLayer.firstChild) {
      parentLayer.removeChild(parentLayer.firstChild);
    }
    Waze.map.layers.forEach(function(layer) {
      if (layer.name != 'Image Overlay') {
        var layerOption = document.createElement('option');
        layerOption.value = layer.name;
        layerOption.textContent = layer.name + (layer.visibility ? '' : ' (' + I18n.t('image_overlays.layer_hidden') + ')');
        layerOption.selected = layer.name == currentLayer;
        parentLayer.appendChild(layerOption);
      }
    });
  }

  function displayImportPage() {
    document.addEventListener('paste', pasteListener);
    importLabel.className = 'label label-primary';
    alignLabel.className = 'label label-default';
    pinLabel.style.cursor = 'default';
    pinLabel.removeEventListener('click', pinToMap);
    removeLayer();
    
    description.textContent = I18n.t('image_overlays.import_image_description');
    var addImageInput = document.createElement('input');
    addImageInput.type = 'file';
    addImageInput.accepts = 'image/*';
    addImageInput.className = 'center-block';
    addImageInput.addEventListener('change', function() {
      displayAlignPage(addImageInput.files[0]);
    });
    instructions.textContent = '';
    instructions.appendChild(addImageInput);
    instructions.appendChild(importError);
    panel.classList.remove('hidden');
    Waze.map.resize();
  }

  function displayAlignPage(blob) {
    currentBlob = blob;
    document.removeEventListener('paste', pasteListener);
    importLabel.className = 'label label-success';
    alignLabel.className = 'label label-primary';
    parentLayer.selectedIndex = 0;

    displayImageOverlay({
      'blob': blob,
      'extent': Waze.map.getExtent(),
      'rotation': 0
    }, true);

    pinLabel.style.cursor = 'pointer';
    pinLabel.addEventListener('click', pinToMap);

    description.textContent = I18n.t('image_overlays.align_image_description');
    instructions.textContent = '';
    instructions.appendChild(createControlButton('rotate-left', function() {
      layer.rotate(-45);
    }, '45°'));
    instructions.appendChild(createControlButton('rotate-left', function() {
      layer.rotate(-0.5);
    }));
    instructions.appendChild(createControlButton('arrow-up', function() {
      layer.shift(0, 10 * Waze.map.getResolution());
    }));
    instructions.appendChild(createControlButton('rotate-right', function() {
      layer.rotate(0.5);
    }));
    instructions.appendChild(createControlButton('rotate-right', function() {
      layer.rotate(45);
    }, '45°'));
    instructions.appendChild(document.createElement('br'));
    instructions.appendChild(createControlButton('arrow-left', function() {
      layer.shift(-10 * Waze.map.getResolution(), 0);
    }));
    instructions.appendChild(createControlButton('crosshairs', function() {
      var layerCenter = layer.extent.getCenterLonLat();
      layer.shift(Waze.map.getCenter().lon - layerCenter.lon, Waze.map.getCenter().lat - layerCenter.lat);
    }));
    instructions.appendChild(createControlButton('arrow-right', function() {
      layer.shift(10 * Waze.map.getResolution(), 0);
    }));
    instructions.appendChild(document.createElement('br'));
    instructions.appendChild(createControlButton('minus', function() {
      layer.scale(1 - (0.05 * Waze.map.getResolution()));
    }));
    instructions.appendChild(createControlButton('arrow-down', function() {
      layer.shift(0, -10 * Waze.map.getResolution());
    }));
    instructions.appendChild(createControlButton('plus', function() {
      layer.scale(1 + (0.05 * Waze.map.getResolution()));
    }));

    Waze.map.resize();
  }

  function pinToMap() {
    getIndexedDB(function(db) {
      var obj = {
        'blob': currentBlob,
        'name': currentBlob.name,
        'extent': layer.extent.toArray(),
        'rotation': layer.rotation,
        'opacity': opacityRange.value,
        'layerTarget': parentLayer.value
      };
      var req = db.transaction(['overlays'], 'readwrite').objectStore('overlays').add(obj);
      req.addEventListener('success', function(e) {
        panel.className = 'hidden';
        Waze.map.resize();
        addImageOverlay(obj.name, e.target.result, true);
      });
    });
  }

  function displayImageOverlay(overlay, rescale) {
    var url = window.URL.createObjectURL(overlay.blob);
    var img = document.createElement('img');
    img.addEventListener('load', function() {
      removeLayer();
      if (rescale) { // Rescale the extent for the image so it has the correct aspect ratio
        var mapExtentAspectRatio = overlay.extent.getWidth() / overlay.extent.getHeight();
        var imageAspectRatio = img.naturalWidth / img.naturalHeight;
        if (mapExtentAspectRatio > imageAspectRatio) {
          var widthDiff = overlay.extent.getWidth() - (overlay.extent.getHeight() * imageAspectRatio);
          overlay.extent = new OL.Bounds([overlay.extent.left + widthDiff/2 , overlay.extent.bottom, overlay.extent.right - widthDiff/2, overlay.extent.top]);
        } else {
          var heightDiff = overlay.extent.getHeight() - (overlay.extent.getWidth() / imageAspectRatio);
          overlay.extent = new OL.Bounds([overlay.extent.left, overlay.extent.bottom + heightDiff/2, overlay.extent.right, overlay.extent.top - heightDiff/2]);
        }
        overlay.extent = overlay.extent.scale(0.8);
      }
      layer = new OL.Layer.OverlayImage('Image Overlay', url, overlay.extent, new OL.Size(img.naturalWidth, img.naturalHeight), overlay.key, { 'rotation': overlay.rotation, 'opacity': overlay.opacity || 1 });
      Waze.map.addLayer(layer);
      for (var i = 0; i < imagesList.childNodes.length; i++) {
        imagesList.childNodes[i].style.fontWeight = (imagesList.childNodes[i].dataset.key == overlay.key ? '700' : '');
      }
      layerControls.classList.remove('hidden');
      hideOverlayButton.classList.remove('hidden');
      opacityRange.value = (overlay.opacity ? overlay.opacity * 50 : 50);
      updateParentLayer(overlay.layerTarget);
      var targetIndex = Waze.map.getLayerIndex(Waze.map.getLayersByName(overlay.layerTarget || "")[0]);
      if (!targetIndex || targetIndex <= 0) {
        targetIndex = Waze.map.getLayerIndex(Waze.map.getLayerByUniqueName('satellite_imagery'));
      }
      Waze.map.setLayerIndex(layer, targetIndex+1);
      Waze.map.zoomToExtent(overlay.extent);
    });
    img.src = url;
  }

  function removeLayer() {
    if (layer) {
      Waze.map.removeLayer(layer);
      layer = null;
      layerControls.classList.add('hidden');
      hideOverlayButton.classList.add('hidden');
      for (var i = 0; i < imagesList.childNodes.length; i++) {
        imagesList.childNodes[i].style.fontWeight = '';
      }
    }
  }
}

// Create a tab and possibly receive a previous tab to restore (usually in case of a mode change)
function addTab(recoveredTab) {
  var userInfo = document.getElementById('user-info'),
      tabHandles = userInfo.querySelector('.nav-tabs'),
      tabs = userInfo.querySelector('.tab-content'),
      tabHandle = document.createElement('li'),
      tab = document.createElement('div');
  tabHandle.innerHTML = '<a href="#sidepanel-imageoverlays" data-toggle="tab" title="' + I18n.t('image_overlays.tab_title') + '"><span class="fa fa-picture-o"></span></a>';
  if (recoveredTab) {
    tab = recoveredTab;
  } else {
    tab.id = 'sidepanel-imageoverlays';
    tab.className = 'tab-pane';
  }
  tabHandles.appendChild(tabHandle);
  $(tabHandle.childNodes[0]).tooltip();
  tabs.appendChild(tab);
  return tab;
}

function createBreadcrumb(icon, text, status) {
  var label = document.createElement('span');
  label.className = 'label label-' + status;
  label.style.fontSize = '1.2em';
  label.style.cursor = 'default';
  var i = document.createElement('i');
  i.className = 'fa fa-fw fa-' + icon;
  label.appendChild(i);
  label.appendChild(document.createTextNode(' ' + text));
  return label;
}

function createControlButton(icon, callback, text) {
  var controlButton = document.createElement('button');
  var controlButtonIcon = document.createElement('i');
  controlButtonIcon.className = 'fa fa-fw fa-' + icon;
  controlButton.appendChild(controlButtonIcon);
  if (text) {
    controlButton.appendChild(document.createTextNode(' ' + text));
  }
  controlButton.addEventListener('click', callback);
  return controlButton;
}

function getIndexedDB(callback) {
  var req = indexedDB.open('ImageOverlays', 1);
  req.addEventListener('upgradeneeded', function(e) {
    e.target.result.createObjectStore('overlays', { autoIncrement: true });
  });
  req.addEventListener('error', log);
  req.addEventListener('success', function(e) {
    callback(e.target.result);
  });
}

function log(message) {
  if (typeof message === 'string') {
    console.log('%c' + GM_info.script.name + ' (v' + GM_info.script.version + '): %c' + message, 'color:black', 'color:#d97e00');
  } else {
    console.log('%c' + GM_info.script.name + ' (v' + GM_info.script.version + ')', 'color:black', message);
  }
}

init();