Réguas e linhas guia para o WME. Arraste da régua para criar; botão esquerdo move, botão direito + arrastar gira. Limite de 5 linhas guia. Botão para excluir todas linhas guia.
// ==UserScript==
// @name WME Guide Lines
// @namespace https://greasyfork.org/
// @version 1.0.1
// @description Réguas e linhas guia para o WME. Arraste da régua para criar; botão esquerdo move, botão direito + arrastar gira. Limite de 5 linhas guia. Botão para excluir todas linhas guia.
// @match https://www.waze.com/*editor*
// @match https://beta.waze.com/*editor*
// @exclude https://www.waze.com/*user/*editor/*
// @grant none
// @require https://update.greasyfork.org/scripts/450160/1704233/WME-Bootstrap.js
// ==/UserScript==
/* global W, $ */
/* jshint esversion: 11 */
(function () {
'use strict';
// ─────────────────────────────────────────────────────────────────────────
// CONFIG
// ─────────────────────────────────────────────────────────────────────────
const CFG = {
MAX_GUIDES: 5,
RULER_W: 18, // px — ruler thickness
HIT_RADIUS: 8, // px — mouse hit tolerance
COLOR_IDLE: '#00bfff',
COLOR_HOVER: '#ff6b35',
ALPHA: 0.80,
LINE_W: 1.5,
DASH: [10, 6],
TICK_SM: 4,
TICK_LG: 10,
};
// ─────────────────────────────────────────────────────────────────────────
// WME LAYOUT — selectors tried in order
// ─────────────────────────────────────────────────────────────────────────
// Header: bar that contains search + save button
const HEADER_SEL = [
'#app-head',
'#topbar',
'.app-header',
'header.toolbar',
'nav.toolbar',
'.toolbar-container',
'#toolbar',
];
// Sidebar: left panel (collapses/expands)
const SIDEBAR_SEL = [
'#sidebar',
'.sidebar',
'#edit-panel',
'.edit-panel',
'aside',
'#panel-container',
];
function findEl(selectors) {
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el && el.offsetWidth > 0) return el;
}
return null;
}
// Returns { top, left } — the offset where the map area starts
function getMapOffset() {
// Try to read directly from #WazeMap (the actual OL map container)
const wazeMap = document.getElementById('WazeMap');
if (wazeMap) {
const r = wazeMap.getBoundingClientRect();
return { top: r.top, left: r.left };
}
// Fallback: read header height + sidebar width
const header = findEl(HEADER_SEL);
const sidebar = findEl(SIDEBAR_SEL);
return {
top: header ? header.getBoundingClientRect().bottom : 60,
left: sidebar ? sidebar.getBoundingClientRect().right : 0,
};
}
// ─────────────────────────────────────────────────────────────────────────
// STATE
// ─────────────────────────────────────────────────────────────────────────
const guides = []; // { id, type:'H'|'V', pos:number, angleDeg:number }
let nextId = 1;
let hidden = false;
let layout = { top: 60, left: 0 }; // cached map offset
const drag = {
active: false,
mode: null, // 'create-H'|'create-V'|'move'|'rotate'
guideId: null,
startCX: 0,
startCY: 0,
startPos: 0,
startAngle: 0,
};
// ─────────────────────────────────────────────────────────────────────────
// DOM REFS
// ─────────────────────────────────────────────────────────────────────────
let hRuler, vRuler, lineCanvas, lCtx, angleLabel;
// ─────────────────────────────────────────────────────────────────────────
// SETUP
// ─────────────────────────────────────────────────────────────────────────
function setup() {
injectCSS();
buildDOM();
updateLayout();
// Observe sidebar & header for size changes
const ro = new ResizeObserver(updateLayout);
const header = findEl(HEADER_SEL);
const sidebar = findEl(SIDEBAR_SEL);
if (header) ro.observe(header);
if (sidebar) ro.observe(sidebar);
window.addEventListener('resize', updateLayout);
// Also poll in case sidebar animates (toggle arrow)
setInterval(updateLayout, 400);
console.log('[WME-GL] Guide Lines v3 carregado.');
}
// ─────────────────────────────────────────────────────────────────────────
// CSS
// ─────────────────────────────────────────────────────────────────────────
function injectCSS() {
if (document.getElementById('wmegl-style')) return;
const s = document.createElement('style');
s.id = 'wmegl-style';
// All elements are fixed. Positions are set dynamically via updateLayout().
s.textContent = `
#wmegl-hruler {
position: fixed;
height: ${CFG.RULER_W}px;
background: rgba(27, 31, 46, 0.4);
border-bottom: 1px solid #2e3a55;
z-index: 99980;
cursor: s-resize;
pointer-events: all;
box-sizing: border-box;
user-select: none;
}
#wmegl-vruler {
position: fixed;
width: ${CFG.RULER_W}px;
background: rgba(27, 31, 46, 0.4);
border-right: 1px solid #2e3a55;
z-index: 99980;
cursor: e-resize;
pointer-events: all;
box-sizing: border-box;
user-select: none;
}
#wmegl-corner {
position: fixed;
width: ${CFG.RULER_W}px;
height: ${CFG.RULER_W}px;
background: #141824;
border-right: 1px solid #2e3a55;
border-bottom: 1px solid #2e3a55;
z-index: 99982;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
}
#wmegl-canvas {
position: fixed;
pointer-events: none;
z-index: 99979;
}
.wmegl-hit {
position: fixed;
z-index: 99981;
pointer-events: all;
cursor: move;
}
#wmegl-ghost {
position: fixed;
pointer-events: none;
z-index: 99983;
opacity: 0.55;
display: none;
background: ${CFG.COLOR_IDLE};
}
#wmegl-angle-label {
position: fixed;
pointer-events: none;
z-index: 99985;
display: none;
background: rgba(14,18,28,0.92);
border: 1px solid #2e3a55;
border-radius: 4px;
padding: 3px 8px;
font: 11px/1.4 monospace;
color: #7eb8f7;
}
#wmegl-clear-btn {
position: fixed;
z-index: 99984;
background: rgba(20,24,36,0.92);
border: 1px solid #2e3a55;
border-radius: 5px;
padding: 4px 10px;
font: 11px/1.5 'Segoe UI', system-ui, sans-serif;
color: #b0bbce;
cursor: pointer;
pointer-events: all;
white-space: nowrap;
box-shadow: 0 2px 8px rgba(0,0,0,0.5);
transition: background 0.12s, color 0.12s, border-color 0.12s;
}
#wmegl-clear-btn:hover {
background: #2e1a1a;
color: #f87171;
border-color: #f87171;
}
`;
document.head.appendChild(s);
}
// ─────────────────────────────────────────────────────────────────────────
// BUILD DOM
// ─────────────────────────────────────────────────────────────────────────
function buildDOM() {
// Horizontal ruler (top edge of map area)
hRuler = document.createElement('canvas');
hRuler.id = 'wmegl-hruler';
hRuler.title = 'Arraste para baixo para criar uma linha guia horizontal';
document.body.appendChild(hRuler);
// Vertical ruler (left edge of map area)
vRuler = document.createElement('canvas');
vRuler.id = 'wmegl-vruler';
vRuler.title = 'Arraste para a direita para criar uma linha guia vertical';
document.body.appendChild(vRuler);
// Corner square (intersection of the two rulers)
const corner = document.createElement('div');
corner.id = 'wmegl-corner';
corner.title = 'WME Guide Lines';
corner.innerHTML = `<svg width="12" height="12" viewBox="0 0 12 12">
<line x1="1" y1="6" x2="11" y2="6" stroke="#3a4060" stroke-width="1.5"/>
<line x1="6" y1="1" x2="6" y2="11" stroke="#3a4060" stroke-width="1.5"/>
</svg>`;
document.body.appendChild(corner);
// Guide lines canvas (covers the entire map area)
lineCanvas = document.createElement('canvas');
lineCanvas.id = 'wmegl-canvas';
document.body.appendChild(lineCanvas);
lCtx = lineCanvas.getContext('2d');
// Ghost line while dragging from ruler
const ghost = document.createElement('div');
ghost.id = 'wmegl-ghost';
document.body.appendChild(ghost);
// Angle label while rotating
angleLabel = document.createElement('div');
angleLabel.id = 'wmegl-angle-label';
document.body.appendChild(angleLabel);
// Clear button (top-left of map area, just after the corner)
const clearBtn = document.createElement('button');
clearBtn.id = 'wmegl-clear-btn';
clearBtn.textContent = 'Limpar Linhas Guia';
clearBtn.title = 'Remove todas as linhas guia';
clearBtn.addEventListener('click', clearAll);
document.body.appendChild(clearBtn);
// Ruler mouse events
hRuler.addEventListener('mousedown', e => onRulerDown(e, 'H'));
vRuler.addEventListener('mousedown', e => onRulerDown(e, 'V'));
// Global events
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('contextmenu', e => {
if (drag.mode === 'rotate') e.preventDefault();
});
}
// ─────────────────────────────────────────────────────────────────────────
// LAYOUT — recompute positions whenever the WME panels change
// ─────────────────────────────────────────────────────────────────────────
function updateLayout() {
const off = getMapOffset();
// Snap to pixel to avoid sub-pixel gaps
layout.top = Math.round(off.top);
layout.left = Math.round(off.left);
const R = CFG.RULER_W;
const vw = window.innerWidth;
const vh = window.innerHeight;
// Horizontal ruler: sits at the top of the map area, spanning its width
Object.assign(hRuler.style, {
top: layout.top + 'px',
left: (layout.left + R) + 'px',
width: (vw - layout.left - R) + 'px',
});
hRuler.width = Math.max(1, vw - layout.left - R);
hRuler.height = R;
// Vertical ruler: sits at the left edge of the map area
Object.assign(vRuler.style, {
top: (layout.top + R) + 'px',
left: layout.left + 'px',
height: (vh - layout.top - R) + 'px',
});
vRuler.width = R;
vRuler.height = Math.max(1, vh - layout.top - R);
// Corner square: intersection of the two rulers
const corner = document.getElementById('wmegl-corner');
if (corner) {
corner.style.top = layout.top + 'px';
corner.style.left = layout.left + 'px';
}
// Canvas: covers the full map area (below both rulers)
Object.assign(lineCanvas.style, {
top: (layout.top + R) + 'px',
left: (layout.left + R) + 'px',
width: (vw - layout.left - R) + 'px',
height: (vh - layout.top - R) + 'px',
});
lineCanvas.width = Math.max(1, vw - layout.left - R);
lineCanvas.height = Math.max(1, vh - layout.top - R);
// Clear button: just to the right of the corner, vertically centered in ruler
const clearBtn = document.getElementById('wmegl-clear-btn');
if (clearBtn) {
clearBtn.style.top = (layout.top + R + 8) + 'px';
clearBtn.style.left = (layout.left + R + 8) + 'px';
}
redraw();
}
// ─────────────────────────────────────────────────────────────────────────
// COORDINATE HELPERS
// Because the canvas starts at (layout.left + R, layout.top + R),
// we map client coords into canvas space as:
// canvasX = clientX - layout.left - R
// canvasY = clientY - layout.top - R
// Guide pos values are stored in canvas space.
// ─────────────────────────────────────────────────────────────────────────
const R = CFG.RULER_W;
function clientToCanvas(cx, cy) {
return {
x: cx - layout.left - R,
y: cy - layout.top - R,
};
}
function canvasToClient(cx, cy) {
return {
x: cx + layout.left + R,
y: cy + layout.top + R,
};
}
// ─────────────────────────────────────────────────────────────────────────
// DRAW
// ─────────────────────────────────────────────────────────────────────────
function redraw(hoverGuide) {
drawLines(hoverGuide);
drawHRuler();
drawVRuler();
rebuildHitZones();
}
function drawLines(hoverGuide) {
const W = lineCanvas.width, H = lineCanvas.height;
lCtx.clearRect(0, 0, W, H);
if (hidden) return;
guides.forEach(g => drawOneLine(g, g === hoverGuide));
}
function drawOneLine(g, highlight) {
const W = lineCanvas.width, H = lineCanvas.height;
const ep = endpoints(g, W, H);
lCtx.save();
lCtx.beginPath();
lCtx.moveTo(ep.x1, ep.y1);
lCtx.lineTo(ep.x2, ep.y2);
lCtx.setLineDash(CFG.DASH);
lCtx.strokeStyle = highlight ? CFG.COLOR_HOVER : CFG.COLOR_IDLE;
lCtx.globalAlpha = CFG.ALPHA;
lCtx.lineWidth = highlight ? CFG.LINE_W + 1.5 : CFG.LINE_W;
lCtx.stroke();
lCtx.restore();
}
// Two far endpoints of an infinite guide line within the canvas
function endpoints(g, W, H) {
const BIG = Math.max(W, H) * 4;
const rad = (g.angleDeg % 180) * Math.PI / 180;
const dx = Math.cos(rad);
const dy = Math.sin(rad);
// Anchor: for H-guide the anchor y = g.pos; for V-guide the anchor x = g.pos
const ax = g.type === 'V' ? g.pos : W / 2;
const ay = g.type === 'H' ? g.pos : H / 2;
return {
x1: ax - dx * BIG, y1: ay - dy * BIG,
x2: ax + dx * BIG, y2: ay + dy * BIG,
};
}
function distToGuide(g, cx, cy) {
const W = lineCanvas.width, H = lineCanvas.height;
const ep = endpoints(g, W, H);
const dx = ep.x2 - ep.x1, dy = ep.y2 - ep.y1;
const len = Math.sqrt(dx * dx + dy * dy) || 1;
return Math.abs((cy - ep.y1) * dx - (cx - ep.x1) * dy) / len;
}
function findGuideAt(cx, cy) {
for (let i = guides.length - 1; i >= 0; i--) {
if (distToGuide(guides[i], cx, cy) <= CFG.HIT_RADIUS) return guides[i];
}
return null;
}
// ─────────────────────────────────────────────────────────────────────────
// DRAW RULERS
// ─────────────────────────────────────────────────────────────────────────
function drawHRuler() {
if (!hRuler.width) return;
const W = hRuler.width, H = hRuler.height;
const ctx = hRuler.getContext('2d');
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = 'rgba(27, 31, 46, 0.4)';
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = '#2e3a55';
ctx.fillStyle = '#4a5a7a';
ctx.font = '7px monospace';
ctx.lineWidth = 1;
for (let x = 0; x < W; x += 50) {
const isMaj = (x % 100 === 0);
ctx.beginPath();
ctx.moveTo(x, H);
ctx.lineTo(x, H - (isMaj ? CFG.TICK_LG : CFG.TICK_SM));
ctx.stroke();
if (isMaj && x > 0) ctx.fillText(x, x + 2, H - 2);
}
// Hint text in the middle
ctx.fillStyle = '#2e3a55';
ctx.font = '9px sans-serif';
const hint = '↓ arraste para criar guia horizontal';
ctx.fillText(hint, W / 2 - ctx.measureText(hint).width / 2, H - 2);
// Markers for existing guides
if (!hidden) {
guides.forEach(g => {
// For H-guides: mark their Y position on the H-ruler? No, H-guides are horizontal.
// For V-guides: mark their X position on the H-ruler.
if (g.type === 'V') {
const x = g.pos; // already in canvas space
ctx.fillStyle = CFG.COLOR_IDLE;
ctx.globalAlpha = 0.9;
ctx.beginPath();
ctx.moveTo(x, H);
ctx.lineTo(x - 4, H - 7);
ctx.lineTo(x + 4, H - 7);
ctx.closePath();
ctx.fill();
ctx.globalAlpha = 1;
}
});
}
}
function drawVRuler() {
if (!vRuler.height) return;
const W = vRuler.width, H = vRuler.height;
const ctx = vRuler.getContext('2d');
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = 'rgba(27, 31, 46, 0.4)';
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = '#2e3a55';
ctx.fillStyle = '#4a5a7a';
ctx.font = '7px monospace';
ctx.lineWidth = 1;
for (let y = 0; y < H; y += 50) {
const isMaj = (y % 100 === 0);
ctx.beginPath();
ctx.moveTo(W, y);
ctx.lineTo(W - (isMaj ? CFG.TICK_LG : CFG.TICK_SM), y);
ctx.stroke();
if (isMaj && y > 0) {
ctx.save();
ctx.translate(W - 2, y - 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText(y, 0, 0);
ctx.restore();
}
}
// Hint
ctx.save();
ctx.fillStyle = '#2e3a55';
ctx.font = '9px sans-serif';
ctx.translate(W - 2, H / 2);
ctx.rotate(-Math.PI / 2);
const hint = '→ arraste para criar guia vertical';
ctx.fillText(hint, -ctx.measureText(hint).width / 2, 0);
ctx.restore();
// Markers for H-guides on v-ruler
if (!hidden) {
guides.forEach(g => {
if (g.type === 'H') {
const y = g.pos;
ctx.fillStyle = CFG.COLOR_IDLE;
ctx.globalAlpha = 0.9;
ctx.beginPath();
ctx.moveTo(W, y);
ctx.lineTo(W - 7, y - 4);
ctx.lineTo(W - 7, y + 4);
ctx.closePath();
ctx.fill();
ctx.globalAlpha = 1;
}
});
}
}
// ─────────────────────────────────────────────────────────────────────────
// HIT ZONES (invisible divs over each line for mouse interaction)
// ─────────────────────────────────────────────────────────────────────────
function rebuildHitZones() {
document.querySelectorAll('.wmegl-hit').forEach(el => el.remove());
if (hidden) return;
const W = lineCanvas.width, H = lineCanvas.height;
const HIT = CFG.HIT_RADIUS * 2;
guides.forEach(g => {
const ep = endpoints(g, W, H);
// Convert canvas endpoints back to client coords
const cl1 = canvasToClient(ep.x1, ep.y1);
const cl2 = canvasToClient(ep.x2, ep.y2);
const hit = document.createElement('div');
hit.className = 'wmegl-hit';
hit.dataset.id = g.id;
const mx = (cl1.x + cl2.x) / 2;
const my = (cl1.y + cl2.y) / 2;
const len = Math.hypot(cl2.x - cl1.x, cl2.y - cl1.y);
const ang = Math.atan2(cl2.y - cl1.y, cl2.x - cl1.x) * 180 / Math.PI;
Object.assign(hit.style, {
left: (mx - len / 2) + 'px',
top: (my - HIT / 2) + 'px',
width: len + 'px',
height: HIT + 'px',
transformOrigin: `${len / 2}px ${HIT / 2}px`,
transform: `rotate(${ang}deg)`,
});
hit.addEventListener('mousedown', e => {
const guide = guides.find(g => g.id === +hit.dataset.id);
if (!guide) return;
e.preventDefault();
e.stopPropagation();
if (e.button === 0) {
drag.active = true;
drag.mode = 'move';
drag.guideId = guide.id;
drag.startCX = e.clientX;
drag.startCY = e.clientY;
drag.startPos = guide.pos;
} else if (e.button === 2) {
drag.active = true;
drag.mode = 'rotate';
drag.guideId = guide.id;
drag.startCX = e.clientX;
drag.startCY = e.clientY;
drag.startAngle = guide.angleDeg;
}
});
hit.addEventListener('dblclick', e => {
e.preventDefault();
e.stopPropagation();
removeGuide(+hit.dataset.id);
redraw();
});
document.body.appendChild(hit);
});
}
// ─────────────────────────────────────────────────────────────────────────
// RULER DRAG — create guide
// ─────────────────────────────────────────────────────────────────────────
function onRulerDown(e, type) {
if (e.button !== 0) return;
if (guides.length >= CFG.MAX_GUIDES) return;
e.preventDefault();
e.stopPropagation();
drag.active = true;
drag.mode = type === 'H' ? 'create-H' : 'create-V';
drag.startCX = e.clientX;
drag.startCY = e.clientY;
showGhost(type, e.clientX, e.clientY);
}
// ─────────────────────────────────────────────────────────────────────────
// MOUSEMOVE
// ─────────────────────────────────────────────────────────────────────────
function onMouseMove(e) {
if (!drag.active) {
// Hover highlight — only inside canvas area
const { x, y } = clientToCanvas(e.clientX, e.clientY);
if (x >= 0 && y >= 0 && x <= lineCanvas.width && y <= lineCanvas.height) {
const g = findGuideAt(x, y);
redraw(g || null);
}
return;
}
const dx = e.clientX - drag.startCX;
const dy = e.clientY - drag.startCY;
if (drag.mode === 'create-H') {
moveGhost('H', e.clientX, e.clientY);
} else if (drag.mode === 'create-V') {
moveGhost('V', e.clientX, e.clientY);
} else if (drag.mode === 'move') {
const g = guides.find(g => g.id === drag.guideId);
if (g) {
g.pos = drag.startPos + (g.type === 'H' ? dy : dx);
redraw();
}
} else if (drag.mode === 'rotate') {
const g = guides.find(g => g.id === drag.guideId);
if (g) {
g.angleDeg = ((drag.startAngle + dx) % 180 + 180) % 180;
redraw();
angleLabel.textContent = `${Math.round(g.angleDeg)}°`;
angleLabel.style.left = (e.clientX + 14) + 'px';
angleLabel.style.top = (e.clientY - 10) + 'px';
angleLabel.style.display = 'block';
}
}
}
// ─────────────────────────────────────────────────────────────────────────
// MOUSEUP
// ─────────────────────────────────────────────────────────────────────────
function onMouseUp(e) {
if (!drag.active) return;
if (drag.mode === 'create-H' || drag.mode === 'create-V') {
hideGhost();
const type = drag.mode === 'create-H' ? 'H' : 'V';
const { x, y } = clientToCanvas(e.clientX, e.clientY);
// Only create if dropped inside the map canvas area
if (x >= 0 && y >= 0 && x <= lineCanvas.width && y <= lineCanvas.height) {
addGuide(type, type === 'H' ? y : x);
redraw();
}
} else if (drag.mode === 'rotate') {
angleLabel.style.display = 'none';
redraw();
} else if (drag.mode === 'move') {
redraw();
}
drag.active = false;
drag.mode = null;
drag.guideId = null;
}
// ─────────────────────────────────────────────────────────────────────────
// GHOST LINE
// ─────────────────────────────────────────────────────────────────────────
function showGhost(type, cx, cy) {
const ghost = document.getElementById('wmegl-ghost');
ghost.style.display = 'block';
if (type === 'H') {
Object.assign(ghost.style, {
left: '0', right: '0', top: cy + 'px', bottom: 'auto',
width: '100vw', height: '2px',
transform: 'translateY(-50%)',
});
} else {
Object.assign(ghost.style, {
top: '0', bottom: '0', left: cx + 'px', right: 'auto',
width: '2px', height: '100vh',
transform: 'translateX(-50%)',
});
}
}
function moveGhost(type, cx, cy) {
const ghost = document.getElementById('wmegl-ghost');
if (ghost.style.display === 'none') return;
if (type === 'H') ghost.style.top = cy + 'px';
else ghost.style.left = cx + 'px';
}
function hideGhost() {
document.getElementById('wmegl-ghost').style.display = 'none';
}
// ─────────────────────────────────────────────────────────────────────────
// GUIDE CRUD
// ─────────────────────────────────────────────────────────────────────────
function addGuide(type, pos) {
guides.push({ id: nextId++, type, pos, angleDeg: type === 'H' ? 0 : 90 });
}
function removeGuide(id) {
const i = guides.findIndex(g => g.id === id);
if (i !== -1) guides.splice(i, 1);
}
function clearAll() {
guides.length = 0;
document.querySelectorAll('.wmegl-hit').forEach(el => el.remove());
redraw();
}
// ─────────────────────────────────────────────────────────────────────────
// INIT
// ─────────────────────────────────────────────────────────────────────────
function init() {
if (document.getElementById('wmegl-style')) return;
// Wait until the WME map container is rendered
const wait = () => {
const wazeMap = document.getElementById('WazeMap') || findEl(HEADER_SEL);
if (wazeMap && wazeMap.clientWidth > 0) setup();
else setTimeout(wait, 600);
};
wait();
}
$(document).on('bootstrap.wme', init);
})();