forked from Open-CT/openct-tasks
1493 lines
43 KiB
JavaScript
1493 lines
43 KiB
JavaScript
function Map2D(params) {
|
|
|
|
var defaults = {
|
|
width: false,
|
|
height: false,
|
|
zoom: 0,
|
|
max_zoom: 1,
|
|
min_zoom: -2,
|
|
simple_mode: false,
|
|
styles: {
|
|
point_radius: 6,
|
|
line_width: 3,
|
|
line_color: '#000000',
|
|
pin_color: '#FFFFFF',
|
|
area_color: '#FFFFFF66',
|
|
selection_color: '#CC3333',
|
|
text_color: '#FFFFFF',
|
|
text_outline: '#000000',
|
|
font: {
|
|
size: 14,
|
|
face: 'sans'
|
|
},
|
|
mistake: {
|
|
color: '#FF0000',
|
|
size: 30
|
|
},
|
|
marker: {
|
|
color: '#3366FF',
|
|
radius: 15,
|
|
line_width: 5
|
|
}
|
|
},
|
|
strings: {
|
|
point: 'Point',
|
|
line: 'Ligne',
|
|
area: 'Zone',
|
|
delete: 'Supprimer',
|
|
undo: 'Annuler',
|
|
redo: 'Refaire',
|
|
tag: 'Type',
|
|
name: 'Nom',
|
|
save: 'Enregistrer',
|
|
cancel: 'Annuler'
|
|
},
|
|
tags: ['']
|
|
}
|
|
params = Object.assign({}, defaults, params);
|
|
|
|
if(params.figures && params.simple_mode) {
|
|
params.figures = Object.assign({}, params.figures);
|
|
params.figures = converter.expand(params.figures);
|
|
params.tags = [''];
|
|
}
|
|
|
|
// system
|
|
function createElement(tag, className, content) {
|
|
var el = document.createElement(tag);
|
|
if(typeof className === 'string') {
|
|
el.className = className;
|
|
}
|
|
if(typeof content === 'string') {
|
|
el.innerHTML = content;
|
|
} else if(Array.isArray(content)) {
|
|
for(var i=0; i<content.length; i++) {
|
|
el.appendChild(content[i]);
|
|
}
|
|
}
|
|
return el;
|
|
}
|
|
|
|
|
|
function addEventListener(obj, evt, handler) {
|
|
if(obj.addEventListener) {
|
|
obj.addEventListener(evt, handler, false);
|
|
} else if(obj.attachEvent) {
|
|
return obj.attachEvent('on' + evt, handler);
|
|
}
|
|
}
|
|
|
|
|
|
var wrapper = createElement('div', 'map2d');
|
|
params.parent.appendChild(wrapper);
|
|
|
|
|
|
|
|
// Viewport
|
|
function Viewport(image) {
|
|
|
|
var zoom = params.zoom;
|
|
var prev_zoom = null;
|
|
|
|
var bounds = {
|
|
width: image.width,
|
|
height: image.height,
|
|
x: 0,
|
|
y: 0,
|
|
scale: 1
|
|
}
|
|
|
|
|
|
|
|
function refresh() {
|
|
bounds.resized = prev_zoom !== zoom;
|
|
if(bounds.resized) {
|
|
bounds.scale = Math.pow(2, zoom);
|
|
bounds.width = image.width * bounds.scale;
|
|
bounds.height = image.height * bounds.scale;
|
|
bounds.resized = true;
|
|
prev_zoom = zoom;
|
|
}
|
|
|
|
if(bounds.width < wrapper.clientWidth) {
|
|
bounds.x = (bounds.width - wrapper.clientWidth) * 0.5;
|
|
} else {
|
|
var max_x = bounds.width - wrapper.clientWidth;
|
|
bounds.x = Math.min(max_x, bounds.x);
|
|
bounds.x = Math.max(0, bounds.x);
|
|
}
|
|
|
|
if(bounds.height < wrapper.clientHeight) {
|
|
bounds.y = (bounds.height - wrapper.clientHeight) * 0.5;
|
|
} else {
|
|
var max_y = bounds.height - wrapper.clientHeight;
|
|
bounds.y = Math.min(max_y, bounds.y);
|
|
bounds.y = Math.max(0, bounds.y);
|
|
}
|
|
|
|
zoom_in.className = zoom == params.max_zoom ? 'button button-disabled' : 'button';
|
|
zoom_out.className = zoom == params.min_zoom ? 'button button-disabled' : 'button';
|
|
|
|
map.setBounds(bounds);
|
|
editor.setBounds(bounds);
|
|
}
|
|
|
|
|
|
|
|
function changeZoom(ofs) {
|
|
var new_zoom = zoom + ofs;
|
|
new_zoom = Math.max(new_zoom, params.min_zoom);
|
|
new_zoom = Math.min(new_zoom, params.max_zoom);
|
|
var hw = wrapper.clientWidth * 0.5;
|
|
var hh = wrapper.clientHeight * 0.5;
|
|
var scale = Math.pow(2, zoom);
|
|
var k = Math.pow(2, new_zoom) / Math.pow(2, zoom);
|
|
bounds.x = (bounds.x + hw) * k - hw;
|
|
bounds.y = (bounds.y + hh) * k - hh;
|
|
zoom = new_zoom;
|
|
refresh();
|
|
}
|
|
|
|
|
|
// zoom controls
|
|
var zoom_in = createElement('div', 'button', '+');
|
|
addEventListener(zoom_in, 'click', function(e) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
changeZoom(1);
|
|
});
|
|
var zoom_out = createElement('div', 'button', '-')
|
|
addEventListener(zoom_out, 'click', function(e) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
changeZoom(-1);
|
|
});
|
|
var holder = createElement('div', 'zoom', [zoom_in, zoom_out]);
|
|
wrapper.appendChild(holder);
|
|
|
|
refresh();
|
|
|
|
|
|
|
|
var drag;
|
|
|
|
function startDrag(point) {
|
|
drag = {
|
|
x: bounds.x + point.x,
|
|
y: bounds.y + point.y
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function handleDrag(point) {
|
|
bounds.x = drag.x - point.x;
|
|
bounds.y = drag.y - point.y;
|
|
refresh();
|
|
}
|
|
|
|
function stopDrag() {}
|
|
|
|
|
|
return {
|
|
refresh: refresh,
|
|
|
|
startDrag: startDrag,
|
|
handleDrag: handleDrag,
|
|
stopDrag: stopDrag,
|
|
|
|
destroy: function() {
|
|
wrapper && wrapper.removeChild(holder);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
// Map layer
|
|
function Map(image) {
|
|
|
|
function setBounds(bounds) {
|
|
if(bounds.resized) {
|
|
el.style.width = bounds.width + 'px';
|
|
el.style.height = bounds.height + 'px';
|
|
}
|
|
el.style.left = (-bounds.x) + 'px';
|
|
el.style.top = (-bounds.y) + 'px';
|
|
}
|
|
|
|
var el = new Image();
|
|
el.src = image.src;
|
|
el.className = 'map';
|
|
wrapper.appendChild(el);
|
|
|
|
|
|
return {
|
|
destroy: function() {
|
|
wrapper.removeChild(el);
|
|
delete el;
|
|
},
|
|
|
|
setBounds: setBounds
|
|
}
|
|
}
|
|
|
|
|
|
// Editor toolbar
|
|
function Toolbar(handlers) {
|
|
|
|
var holder;
|
|
var buttons = {}
|
|
|
|
|
|
function selectButton(name) {
|
|
if(params.simple_mode) {
|
|
return;
|
|
}
|
|
var names = ['point', 'line', 'area'];
|
|
for(var i=0; i < names.length; i++) {
|
|
buttons[names[i]].className = names[i] === name ? 'button button-selected' : 'button';
|
|
}
|
|
}
|
|
|
|
function disableButton(name, diabled) {
|
|
if(params.simple_mode) {
|
|
return;
|
|
}
|
|
buttons[name].className = diabled ? 'button button-disabled' : 'button';
|
|
}
|
|
|
|
|
|
var commands = {
|
|
point: function() {
|
|
handlers.onTypeChange('point');
|
|
},
|
|
line: function() {
|
|
handlers.onTypeChange('line');
|
|
},
|
|
area: function() {
|
|
handlers.onTypeChange('area');
|
|
},
|
|
delete: handlers.onDelete,
|
|
undo: handlers.onUndo,
|
|
redo: handlers.onRedo
|
|
}
|
|
|
|
if(!params.simple_mode) {
|
|
buttons = {
|
|
point: createElement('div', 'button', params.strings.point),
|
|
line: createElement('div', 'button', params.strings.line),
|
|
area: createElement('div', 'button', params.strings.area),
|
|
delete: createElement('div', 'button', params.strings.delete),
|
|
undo: createElement('div', 'button', params.strings.undo),
|
|
redo: createElement('div', 'button', params.strings.redo)
|
|
}
|
|
|
|
holder = createElement('div', 'toolbar', [
|
|
createElement('div', 'group', [
|
|
buttons.point,
|
|
buttons.line,
|
|
buttons.area
|
|
]),
|
|
createElement('div', 'group', [
|
|
buttons.delete,
|
|
buttons.undo,
|
|
buttons.redo
|
|
])
|
|
]);
|
|
wrapper.appendChild(holder);
|
|
|
|
for(var name in commands) {
|
|
addEventListener(buttons[name], 'click', (function(name) {
|
|
return function(e) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
commands[name]();
|
|
}
|
|
})(name));
|
|
}
|
|
}
|
|
|
|
|
|
return {
|
|
selectButton: selectButton,
|
|
|
|
disableButton: disableButton,
|
|
|
|
destroy: function() {
|
|
holder && holder.parentNode.removeChild(holder);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function State(default_state) {
|
|
|
|
var states = [];
|
|
var pointer = 0;
|
|
|
|
|
|
function read(new_pointer) {
|
|
pointer = new_pointer;
|
|
return JSON.parse(states[pointer]);
|
|
}
|
|
|
|
function write(data) {
|
|
states.push(JSON.stringify(data));
|
|
pointer = states.length - 1;
|
|
}
|
|
|
|
write(default_state);
|
|
|
|
return {
|
|
get: function() {
|
|
return read(pointer);
|
|
},
|
|
|
|
push: function(data) {
|
|
if(pointer < states.length - 1) {
|
|
states = states.slice(0, pointer + 1);
|
|
}
|
|
write(data);
|
|
},
|
|
|
|
undo: function() {
|
|
return read(pointer - 1);
|
|
},
|
|
|
|
redo: function() {
|
|
return read(pointer + 1);
|
|
},
|
|
|
|
getCapabilities: function() {
|
|
return {
|
|
undo: pointer > 0,
|
|
redo: pointer !== null && pointer < states.length - 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Drawing editor layer
|
|
function Editor(image) {
|
|
|
|
var canvas = createElement('canvas', 'editor');
|
|
wrapper.appendChild(canvas);
|
|
var context2d = canvas.getContext('2d');
|
|
var bounds;
|
|
|
|
var state;
|
|
var data;
|
|
|
|
|
|
function resetState(figures) {
|
|
state = State({
|
|
pointer: null,
|
|
type: null,
|
|
figures: figures || params.figures || []
|
|
});
|
|
data = state.get();
|
|
params.onEdit && params.onEdit(data.figures);
|
|
}
|
|
resetState();
|
|
|
|
|
|
function saveState() {
|
|
state.push(data);
|
|
refreshToolbar();
|
|
params.onEdit && params.onEdit(data.figures);
|
|
}
|
|
|
|
|
|
function drawPoint(point) {
|
|
context2d.beginPath();
|
|
if(selection.data && isSamePoint(point, selection.data.point)) {
|
|
context2d.fillStyle = params.styles.selection_color;
|
|
} else {
|
|
context2d.fillStyle = params.styles.pin_color;
|
|
}
|
|
context2d.arc(point.x, point.y, params.styles.point_radius / bounds.scale, 0, 2 * Math.PI);
|
|
context2d.stroke();
|
|
context2d.fill();
|
|
}
|
|
|
|
|
|
function drawFigureName(figure) {
|
|
var text_height = params.styles.font.size / bounds.scale;
|
|
var dx, dy;
|
|
if(figure.type == 'point' || figure.points.length < 2) {
|
|
dx = 0;
|
|
dy = text_height;
|
|
context2d.textAlign = 'center';
|
|
context2d.textBaseline = 'top';
|
|
} else {
|
|
if(figure.points[1].y > figure.points[0].y) {
|
|
context2d.textBaseline = 'bottom';
|
|
dy = -params.styles.line_width / bounds.scale;
|
|
} else {
|
|
dy = params.styles.line_width / bounds.scale;
|
|
context2d.textBaseline = 'top';
|
|
}
|
|
if(figure.points[1].x > figure.points[0].x) {
|
|
context2d.textAlign = 'right';
|
|
dx = -text_height;
|
|
} else {
|
|
context2d.textAlign = 'left';
|
|
dx = text_height;
|
|
}
|
|
}
|
|
context2d.strokeStyle = params.styles.text_outline;
|
|
context2d.strokeText(figure.name, figure.points[0].x + dx, figure.points[0].y + dy);
|
|
context2d.fillStyle = params.styles.text_color;
|
|
context2d.fillText(figure.name, figure.points[0].x + dx, figure.points[0].y + dy);
|
|
}
|
|
|
|
|
|
var shapes = {
|
|
|
|
point: function(points) {
|
|
drawPoint(points[0]);
|
|
},
|
|
|
|
line: function(points) {
|
|
context2d.beginPath();
|
|
context2d.moveTo(points[0].x, points[0].y);
|
|
for(var i=1; i<points.length; i++) {
|
|
context2d.lineTo(points[i].x, points[i].y);
|
|
}
|
|
context2d.stroke();
|
|
for(var i=0; i<points.length; i++) {
|
|
drawPoint(points[i]);
|
|
}
|
|
},
|
|
|
|
area: function(points) {
|
|
context2d.beginPath();
|
|
context2d.fillStyle = params.styles.area_color;
|
|
context2d.moveTo(points[0].x, points[0].y);
|
|
for(var i=1; i<points.length; i++) {
|
|
context2d.lineTo(points[i].x, points[i].y);
|
|
}
|
|
context2d.closePath();
|
|
context2d.stroke();
|
|
context2d.fill();
|
|
for(var i=0; i<points.length; i++) {
|
|
drawPoint(points[i]);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function draw() {
|
|
if(!bounds) {
|
|
return;
|
|
}
|
|
context2d.setTransform(1, 0, 0, 1, 0, 0);
|
|
context2d.clearRect(0, 0, bounds.width, bounds.height);
|
|
context2d.setTransform(bounds.scale, 0, 0, bounds.scale, 0, 0);
|
|
context2d.font = 'bold ' + Math.round(params.styles.font.size / bounds.scale) + 'px ' + params.styles.font.face;
|
|
context2d.strokeStyle = params.styles.line_color;
|
|
context2d.lineWidth = params.styles.line_width / bounds.scale;
|
|
for(var i=0; i<data.figures.length; i++) {
|
|
shapes[data.figures[i].type](data.figures[i].points);
|
|
drawFigureName(data.figures[i]);
|
|
}
|
|
mistake.draw();
|
|
markers.draw();
|
|
}
|
|
|
|
|
|
function setBounds(new_bounds) {
|
|
bounds = new_bounds;
|
|
if(bounds.resized) {
|
|
canvas.width = bounds.width;
|
|
canvas.height = bounds.height;
|
|
canvas.style.width = bounds.width + 'px';
|
|
canvas.style.height = bounds.height + 'px';
|
|
draw();
|
|
}
|
|
canvas.style.left = (-bounds.x) + 'px';
|
|
canvas.style.top = (-bounds.y) + 'px';
|
|
}
|
|
|
|
|
|
function isSamePoint(point1, point2) {
|
|
var d = params.styles.point_radius / bounds.scale;
|
|
return Math.abs(point1.x - point2.x) <= d && Math.abs(point1.y - point2.y) <= d;
|
|
}
|
|
|
|
|
|
|
|
var selection = {
|
|
|
|
data: null,
|
|
|
|
set: function(data) {
|
|
if(params.simple_mode) {
|
|
return;
|
|
}
|
|
this.data = data;
|
|
if(this.data) {
|
|
panel.open(data);
|
|
} else {
|
|
panel.close();
|
|
}
|
|
refreshToolbar();
|
|
}
|
|
}
|
|
|
|
|
|
function openFigure(point) {
|
|
selection.set(false);
|
|
data.figures.push({
|
|
type: data.type,
|
|
points: [point],
|
|
name: '',
|
|
tag: ''
|
|
});
|
|
if(data.type != 'point') {
|
|
data.pointer = data.figures.length - 1;
|
|
}
|
|
saveState();
|
|
}
|
|
|
|
function modifyFigure(point) {
|
|
selection.set(false);
|
|
data.figures[data.pointer].points.push(point);
|
|
saveState();
|
|
}
|
|
|
|
|
|
function closeFigure() {
|
|
selection.set(false);
|
|
if(data.pointer === null) {
|
|
return;
|
|
}
|
|
switch(data.figures[data.pointer].type) {
|
|
case 'line':
|
|
if(data.figures[data.pointer].points.length < 2) {
|
|
data.figures.splice(data.pointer, 1);
|
|
}
|
|
break;
|
|
case 'area':
|
|
if(data.figures[data.pointer].points.length < 3) {
|
|
data.figures.splice(data.pointer, 1);
|
|
}
|
|
break;
|
|
}
|
|
data.pointer = null;
|
|
saveState();
|
|
draw();
|
|
}
|
|
|
|
|
|
var toolbar = Toolbar({
|
|
onTypeChange: function(new_type) {
|
|
closeFigure();
|
|
data.type = new_type;
|
|
refreshToolbar();
|
|
},
|
|
onDelete: function() {
|
|
if(selection.data) {
|
|
data.figures[selection.data.figure_idx].points.splice(selection.data.point_idx, 1);
|
|
if(!data.figures[selection.data.figure_idx].points.length) {
|
|
data.figures.splice(selection.data.figure_idx, 1);
|
|
}
|
|
selection.set(false);
|
|
saveState();
|
|
draw();
|
|
}
|
|
},
|
|
onRedo: function() {
|
|
if(state.getCapabilities().redo) {
|
|
data = state.redo();
|
|
refreshToolbar();
|
|
draw();
|
|
}
|
|
},
|
|
onUndo: function() {
|
|
if(state.getCapabilities().undo) {
|
|
data = state.undo();
|
|
refreshToolbar();
|
|
draw();
|
|
}
|
|
}
|
|
});
|
|
|
|
function refreshToolbar() {
|
|
var caps = state.getCapabilities();
|
|
toolbar.disableButton('undo', !caps.undo);
|
|
toolbar.disableButton('redo', !caps.redo);
|
|
toolbar.disableButton('delete', !selection.data);
|
|
toolbar.selectButton(data.type);
|
|
}
|
|
|
|
|
|
function findFigure(point) {
|
|
for(var i=0; i<data.figures.length; i++) {
|
|
for(var j=0; j<data.figures[i].points.length; j++) {
|
|
if(isSamePoint(data.figures[i].points[j], point)) {
|
|
return {
|
|
figure_idx: i,
|
|
figure: data.figures[i],
|
|
point_idx: j,
|
|
point: data.figures[i].points[j]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
function normalizePoint(point) {
|
|
return {
|
|
x: (point.x + bounds.x) / bounds.scale,
|
|
y: (point.y + bounds.y) / bounds.scale
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function handleClick(point) {
|
|
mistake.set(false);
|
|
point = normalizePoint(point);
|
|
if(point.x < 0 || point.x > image.width || point.y < 0 || point.y > image.height) {
|
|
return;
|
|
}
|
|
|
|
var figure = findFigure(point);
|
|
if(figure) {
|
|
if(data.pointer) {
|
|
var points = data.figures[data.pointer].points;
|
|
if(isSamePoint(point, points[points.length - 1])) {
|
|
closeFigure();
|
|
}
|
|
}
|
|
selection.set(figure);
|
|
} else {
|
|
if(data.type === null) {
|
|
return;
|
|
}
|
|
if(data.pointer === null) {
|
|
openFigure(point);
|
|
} else {
|
|
modifyFigure(point);
|
|
}
|
|
}
|
|
draw();
|
|
}
|
|
|
|
|
|
var drag;
|
|
|
|
function startDrag(point) {
|
|
if(params.simple_mode) {
|
|
return false;
|
|
}
|
|
mistake.set(false);
|
|
point = normalizePoint(point);
|
|
drag = {
|
|
figure: findFigure(point),
|
|
mouse: point
|
|
}
|
|
return !!drag.figure;
|
|
}
|
|
|
|
function handleDrag(point) {
|
|
selection.set(false);
|
|
point = normalizePoint(point);
|
|
data.figures[drag.figure.figure_idx].points[drag.figure.point_idx] = {
|
|
x: drag.figure.point.x - drag.mouse.x + point.x,
|
|
y: drag.figure.point.y - drag.mouse.y + point.y
|
|
}
|
|
draw();
|
|
}
|
|
|
|
|
|
function stopDrag() {
|
|
drag = false;
|
|
saveState();
|
|
}
|
|
|
|
|
|
var mistake = {
|
|
|
|
data: null, // { point: ..., type: 'extra' || 'miss' }
|
|
silent: false,
|
|
|
|
set: function(data, silent) {
|
|
this.data = data;
|
|
this.silent = silent;
|
|
draw();
|
|
},
|
|
|
|
get: function() {
|
|
return this.data;
|
|
},
|
|
|
|
draw: function() {
|
|
if(!this.data || this.silent) {
|
|
return;
|
|
}
|
|
context2d.strokeStyle = params.styles.mistake.color;
|
|
context2d.lineWidth = params.styles.line_width / bounds.scale;
|
|
var s = 0.5 * params.styles.mistake.size / bounds.scale;
|
|
context2d.beginPath();
|
|
if(this.data.type == 'extra') {
|
|
context2d.arc(this.data.point.x, this.data.point.y, s, 0, 2 * Math.PI);
|
|
} else if(this.data.type == 'miss') {
|
|
context2d.rect(this.data.point.x - s, this.data.point.y - s, 2 * s, 2 * s);
|
|
}
|
|
context2d.closePath();
|
|
context2d.stroke();
|
|
}
|
|
}
|
|
|
|
|
|
var markers = {
|
|
|
|
data: [],
|
|
|
|
renderMarker: function(data) {
|
|
context2d.strokeStyle = data.color || params.styles.marker.color;
|
|
context2d.lineWidth = (data.line_width || params.styles.marker.line_width) / bounds.scale;
|
|
context2d.beginPath();
|
|
context2d.arc(
|
|
data.x,
|
|
data.y,
|
|
(data.radius || params.styles.marker.radius) / bounds.scale,
|
|
0,
|
|
2 * Math.PI
|
|
);
|
|
context2d.closePath();
|
|
context2d.stroke();
|
|
},
|
|
|
|
draw: function() {
|
|
for(var i=0; i<this.data.length; i++) {
|
|
this.renderMarker(this.data[i]);
|
|
}
|
|
},
|
|
|
|
add: function(data) {
|
|
this.data.push(data);
|
|
},
|
|
|
|
clear: function() {
|
|
this.data = [];
|
|
}
|
|
}
|
|
|
|
|
|
refreshToolbar();
|
|
|
|
return {
|
|
setBounds: setBounds,
|
|
|
|
handleClick: handleClick,
|
|
|
|
startDrag: startDrag,
|
|
handleDrag: handleDrag,
|
|
stopDrag: stopDrag,
|
|
|
|
getFigures: function() {
|
|
return data.figures;
|
|
},
|
|
|
|
setFigures: function(figures) {
|
|
selection.set(false);
|
|
resetState(figures);
|
|
draw();
|
|
},
|
|
|
|
updateFigure: function(idx, attributes) {
|
|
selection.set(false);
|
|
var changed = false;
|
|
for(var k in attributes) {
|
|
if(data.figures[idx][k] !== attributes[k]) {
|
|
data.figures[idx][k] = attributes[k];
|
|
changed = true;
|
|
}
|
|
}
|
|
if(changed) {
|
|
saveState();
|
|
draw();
|
|
}
|
|
},
|
|
|
|
clearSelection: function() {
|
|
selection.set(false);
|
|
draw();
|
|
},
|
|
|
|
refresh: function() {
|
|
data.pointer = null;
|
|
data.type = null;
|
|
selection.set(false);
|
|
refreshToolbar();
|
|
},
|
|
|
|
setMistake: function(data, silent) {
|
|
mistake.set(data, silent);
|
|
},
|
|
|
|
getMistake: function() {
|
|
return mistake.get();
|
|
},
|
|
|
|
addMarker: function(data) {
|
|
markers.add(data);
|
|
draw();
|
|
},
|
|
|
|
clearMarkers: function() {
|
|
markers.clear();
|
|
draw();
|
|
},
|
|
|
|
destroy: function() {
|
|
toolbar.destroy();
|
|
wrapper.removeChild(canvas);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// panel
|
|
function Panel() {
|
|
|
|
var holder;
|
|
var controls;
|
|
|
|
|
|
function render() {
|
|
if(controls) {
|
|
return;
|
|
}
|
|
var tag_options = '';
|
|
for(var i=0; i<params.tags.length; i++) {
|
|
tag_options += '<option value="' + params.tags[i] + '">' + params.tags[i] + '</option>';
|
|
}
|
|
controls = {
|
|
name: createElement('input'),
|
|
tag: createElement('select', '', tag_options),
|
|
save: createElement('button', '', params.strings.save),
|
|
cancel: createElement('button', '', params.strings.cancel),
|
|
}
|
|
controls.name.type = 'input';
|
|
|
|
addEventListener(controls.save, 'click', save);
|
|
addEventListener(controls.cancel, 'click', function() {
|
|
hide();
|
|
editor.clearSelection();
|
|
});
|
|
|
|
holder = createElement('div', 'panel', [
|
|
createElement('label', false, params.strings.name),
|
|
controls.name,
|
|
createElement('label', false, params.strings.tag),
|
|
controls.tag,
|
|
controls.save,
|
|
controls.cancel
|
|
]);
|
|
params.parent.appendChild(holder);
|
|
}
|
|
|
|
|
|
function hide() {
|
|
if(holder) {
|
|
holder.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
|
|
function save() {
|
|
hide();
|
|
var attrs = {
|
|
name: controls.name.value,
|
|
tag: controls.tag.value,
|
|
}
|
|
editor.updateFigure(figure_idx, attrs);
|
|
editor.clearSelection();
|
|
}
|
|
|
|
|
|
var figure_idx;
|
|
|
|
return {
|
|
open: function(selection) {
|
|
render();
|
|
controls.name.value = typeof selection.figure.name == 'string' ? selection.figure.name : '';
|
|
controls.tag.value = typeof selection.figure.tag == 'string' ? selection.figure.tag : '';
|
|
holder.style.display = '';
|
|
figure_idx = selection.figure_idx;
|
|
},
|
|
|
|
close: function() {
|
|
hide();
|
|
},
|
|
|
|
destroy: function() {
|
|
holder.parent.removeChild(holder);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
function MouseEventsHandler() {
|
|
|
|
var mouse_moved = false;
|
|
|
|
function getRelativePoint(e) {
|
|
var rect = wrapper.getBoundingClientRect();
|
|
return {
|
|
x: e.clientX - rect.x,
|
|
y: e.clientY - rect.y
|
|
}
|
|
}
|
|
|
|
var drag_handler;
|
|
|
|
addEventListener(wrapper, 'mousedown', function(e) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
|
|
var point = getRelativePoint(e);
|
|
if(editor.startDrag(point)) {
|
|
drag_handler = editor;
|
|
} else if(viewport.startDrag(point)) {
|
|
drag_handler = viewport;
|
|
}
|
|
mouse_moved = false;
|
|
});
|
|
|
|
addEventListener(wrapper, 'mousemove', function(e) {
|
|
mouse_moved = true;
|
|
if(drag_handler) {
|
|
var point = getRelativePoint(e);
|
|
drag_handler.handleDrag(point);
|
|
}
|
|
});
|
|
|
|
|
|
addEventListener(wrapper, 'mouseup', function(e) {
|
|
if(mouse_moved && drag_handler) {
|
|
drag_handler.stopDrag();
|
|
}
|
|
drag_handler = false;
|
|
});
|
|
|
|
addEventListener(wrapper, 'mouseleave', function(e) {
|
|
if(mouse_moved && drag_handler) {
|
|
drag_handler.stopDrag();
|
|
}
|
|
drag_handler = false;
|
|
});
|
|
|
|
addEventListener(wrapper, 'click', function(e) {
|
|
if(!mouse_moved) {
|
|
editor.handleClick(getRelativePoint(e));
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// comparator
|
|
function diff(image, target, silent) {
|
|
var editor_figures = editor.getFigures();
|
|
var canvas = createElement('canvas', 'editor');
|
|
canvas.width = image.width;
|
|
canvas.height = image.height;
|
|
var context2d = canvas.getContext('2d');
|
|
var color = '#FF0000';
|
|
|
|
function drawPoint(point, size) {
|
|
context2d.beginPath();
|
|
context2d.arc(point.x, point.y, size, 0, 2 * Math.PI);
|
|
context2d.closePath();
|
|
context2d.fill();
|
|
}
|
|
|
|
|
|
var shapes = {
|
|
|
|
point: function(points, size) {
|
|
drawPoint(points[0], size);
|
|
},
|
|
|
|
line: function(points, size) {
|
|
context2d.beginPath();
|
|
context2d.moveTo(points[0].x, points[0].y);
|
|
for(var i=1; i<points.length; i++) {
|
|
context2d.lineTo(points[i].x, points[i].y);
|
|
}
|
|
context2d.stroke();
|
|
for(var i=0; i<points.length; i++) {
|
|
drawPoint(points[i], size);
|
|
}
|
|
},
|
|
|
|
area: function(points, size) {
|
|
context2d.beginPath();
|
|
context2d.moveTo(points[0].x, points[0].y);
|
|
for(var i=1; i<points.length; i++) {
|
|
context2d.lineTo(points[i].x, points[i].y);
|
|
}
|
|
context2d.closePath();
|
|
context2d.stroke();
|
|
context2d.fill();
|
|
for(var i=0; i<points.length; i++) {
|
|
drawPoint(points[i], size);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function createMask(figures, bias) {
|
|
context2d.setTransform(1, 0, 0, 1, 0, 0);
|
|
context2d.clearRect(0, 0, image.width, image.height);
|
|
context2d.strokeStyle = color;
|
|
context2d.fillStyle = color;
|
|
context2d.lineWidth = bias * 2;
|
|
for(var i=0; i<figures.length; i++) {
|
|
shapes[figures[i].type](figures[i].points, bias);
|
|
}
|
|
//debug.displayCanvas('img', canvas);
|
|
|
|
var pixels = context2d.getImageData(0, 0, image.width, image.height).data;
|
|
var res = [];
|
|
for(var i=0; i<pixels.length; i+=4) {
|
|
res.push(pixels[i] != 0 ? 1 : 0);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
|
|
function getFigureCenter(figure) {
|
|
var center = {
|
|
x: 0,
|
|
y: 0
|
|
}
|
|
for(var i=0; i<figure.points.length; i++) {
|
|
center.x += figure.points[i].x;
|
|
center.y += figure.points[i].y;
|
|
}
|
|
center.x = center.x / figure.points.length;
|
|
center.y = center.y / figure.points.length;
|
|
return center;
|
|
}
|
|
|
|
function collectLayers(figures) {
|
|
var res = [];
|
|
for(var i=0; i<figures.length; i++) {
|
|
var exists = false;
|
|
for(var j=0; j<res.length; j++) {
|
|
if(res[j].name === figures[i].name && res[j].tag === figures[i].tag) {
|
|
exists = true;
|
|
break;
|
|
}
|
|
}
|
|
if(!exists) {
|
|
res.push({
|
|
name: figures[i].name,
|
|
tag: figures[i].tag
|
|
})
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
|
|
function filterFigures(figures, layer) {
|
|
var res = [];
|
|
for(var i=0; i<figures.length; i++) {
|
|
if(figures[i].name === layer.name && figures[i].tag === layer.tag) {
|
|
res.push(figures[i]);
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
|
|
function setPixelMistake(ofs, type, layer) {
|
|
var mistake = {
|
|
point: {
|
|
x: ofs % image.width,
|
|
y: Math.floor(ofs / image.width)
|
|
},
|
|
type: type,
|
|
attribute: 'pixel',
|
|
tag: layer.tag,
|
|
name: layer.name
|
|
}
|
|
editor.setMistake(mistake, silent);
|
|
}
|
|
|
|
|
|
var ofs_table = (function() {
|
|
var res = [];
|
|
|
|
function add(x, y) {
|
|
res.push(y * image.width + x);
|
|
}
|
|
add(0, 0);
|
|
for(var b=1; b<target.bias; b++) {
|
|
for(var i=-b; i<=b; i++) {
|
|
add(i, b);
|
|
add(i, -b);
|
|
if(i != -b && i != b) {
|
|
add(b, i);
|
|
add(-b, i);
|
|
}
|
|
}
|
|
}
|
|
return res;
|
|
})();
|
|
|
|
|
|
|
|
function compareDrawing(editor_drawing, target_mask, target_drawing, layer) {
|
|
for(var j=0; j<editor_drawing.length; j++) {
|
|
// check extra pixels in restricted area
|
|
if(editor_drawing[j] != 0 && target_mask[j] == 0) {
|
|
setPixelMistake(j, 'extra', layer);
|
|
return false;
|
|
}
|
|
|
|
// check missed pixels
|
|
var l;
|
|
var fl = false;
|
|
bias_loops:
|
|
for(var k=0; k<ofs_table.length; k++) {
|
|
l = j + ofs_table[k];
|
|
if(l < 0 || l >= editor_drawing.length) {
|
|
continue;
|
|
}
|
|
if(editor_drawing[l] == target_drawing[j]) {
|
|
fl = true;
|
|
break bias_loops;
|
|
}
|
|
}
|
|
if(!fl) {
|
|
setPixelMistake(j, 'miss', layer);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
var layers = collectLayers(target.figures);
|
|
|
|
// check extra names or tags
|
|
for(var i=0; i<editor_figures.length; i++) {
|
|
var extra_tag = true;
|
|
var extra_name = true;
|
|
for(var j=0; j<layers.length; j++) {
|
|
if(editor_figures[i].tag === layers[j].tag) {
|
|
extra_tag = false;
|
|
}
|
|
if(editor_figures[i].name === layers[j].name) {
|
|
extra_name = false;
|
|
}
|
|
}
|
|
if(extra_tag || extra_name) {
|
|
var mistake = {
|
|
point: getFigureCenter(editor_figures[i]),
|
|
type: 'extra',
|
|
attribute: extra_tag ? 'tag' : 'name',
|
|
tag: editor_figures[i].tag,
|
|
name: editor_figures[i].name
|
|
}
|
|
editor.setMistake(mistake, silent);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// check pixels per layer
|
|
debug.setSize({ width: image.width, height: image.height});
|
|
for(var i=0; i<layers.length; i++) {
|
|
var figures = filterFigures(target.figures, layers[i]);
|
|
|
|
var target_mask = createMask(figures, target.bias);
|
|
debug.displayMask('target_mask ' + JSON.stringify(layers[i]), target_mask)
|
|
|
|
var target_drawing = createMask(figures, 1);
|
|
debug.displayMask('target_drawing ' + JSON.stringify(layers[i]), target_drawing)
|
|
|
|
var figures = filterFigures(editor_figures, layers[i], true);
|
|
var editor_drawing = createMask(figures, 1);
|
|
|
|
debug.displayMask('editor_drawing ' + JSON.stringify(layers[i]), editor_drawing)
|
|
|
|
if(!compareDrawing(editor_drawing, target_mask, target_drawing, layers[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onResize() {
|
|
var width = params.width ? params.width : params.parent.clientWidth;
|
|
var height = params.height ? params.height : params.parent.clientHeight;
|
|
wrapper.style.width = width + 'px';
|
|
wrapper.style.height = height + 'px';
|
|
viewport && viewport.refresh();
|
|
}
|
|
|
|
|
|
var converter = {
|
|
|
|
collapse: function(figures) {
|
|
var res = [], item;
|
|
for(var i=0; i<figures.length; i++) {
|
|
item.type = figures[i].type;
|
|
if(item.type == 'point') {
|
|
item.x = figures[i].points[0].x;
|
|
item.y = figures[i].points[0].y;
|
|
} else {
|
|
item.points = figures[i].points.slice();
|
|
}
|
|
res.push(item);
|
|
}
|
|
return res;
|
|
},
|
|
|
|
|
|
expand: function(figures) {
|
|
var res = [], item;
|
|
for(var i=0; i<figures.length; i++) {
|
|
item = {
|
|
type: figures[i].type,
|
|
tag: '',
|
|
name: ''
|
|
}
|
|
if(item.type == 'point') {
|
|
item.points = [{
|
|
x: figures[i].x,
|
|
y: figures[i].y
|
|
}]
|
|
} else {
|
|
item.points = figures[i].points.slice();
|
|
}
|
|
res.push(item);
|
|
}
|
|
return res;
|
|
}
|
|
}
|
|
|
|
|
|
// marker
|
|
|
|
|
|
|
|
|
|
// main
|
|
|
|
var destroyed = false;
|
|
var viewport;
|
|
var map;
|
|
var editor;
|
|
var panel;
|
|
var mouse_events;
|
|
var image;
|
|
|
|
function loadImage(src, callback) {
|
|
if(!src) {
|
|
console.error('Map2D: url parameter missed');
|
|
return;
|
|
}
|
|
image = new Image();
|
|
image.onload = function() {
|
|
callback();
|
|
}
|
|
image.onerror = function() {
|
|
console.error('Map2D: ' + src + ' loading error');
|
|
}
|
|
image.src = src;
|
|
}
|
|
|
|
loadImage(params.url, function() {
|
|
if(destroyed) {
|
|
return;
|
|
}
|
|
onResize();
|
|
if(!params.width && !params.height) {
|
|
addEventListener(window, 'resize', onResize);
|
|
}
|
|
map = Map(image);
|
|
editor = Editor(image);
|
|
panel = Panel();
|
|
viewport = Viewport(image);
|
|
mouse_events = MouseEventsHandler();
|
|
onResize();
|
|
params.onLoad && params.onLoad();
|
|
})
|
|
|
|
|
|
return {
|
|
|
|
getFigures: function() {
|
|
if(!editor) {
|
|
return [];
|
|
}
|
|
var figures = editor.getFigures();
|
|
if(params.simple_mode) {
|
|
figures = converter.collapse(figures);
|
|
}
|
|
return figures;
|
|
},
|
|
|
|
setFigures: function(figures) {
|
|
if(editor) {
|
|
if(params.simple_mode) {
|
|
figures = converter.expand(figures);
|
|
}
|
|
editor.setFigures(figures);
|
|
}
|
|
},
|
|
|
|
addMarker: function(data) {
|
|
editor && editor.addMarker(data);
|
|
},
|
|
|
|
clearMarkers: function() {
|
|
editor && editor.clearMarkers();
|
|
},
|
|
|
|
diff: function(target, silent) {
|
|
editor && editor.refresh();
|
|
if(params.simple_mode) {
|
|
target = Object.assign({}, target);
|
|
target.figures = converter.expand(target.figures);
|
|
}
|
|
return diff(image, target, silent);
|
|
},
|
|
|
|
getMistake: function() {
|
|
return editor.getMistake();
|
|
},
|
|
|
|
destroy: function() {
|
|
destroyed = true;
|
|
map && map.destroy();
|
|
editor && editor.destroy();
|
|
viewport && viewport.destroy();
|
|
wrapper.parentNode && wrapper.parentNode.removeChild(wrapper);
|
|
if(!params.width && !params.height) {
|
|
window.removeEventListener('resize', onResize);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
debug = {
|
|
|
|
//enabled: true,
|
|
wrapper: false,
|
|
|
|
setSize: function(size) {
|
|
this.size = size;
|
|
},
|
|
|
|
getWrapper: function() {
|
|
if(!this.wrapper) {
|
|
var el = document.createElement('div');
|
|
with(el.style) {
|
|
position = 'fixed';
|
|
zIndex = 1000;
|
|
top = 0;
|
|
bottom = 0;
|
|
right = 0;
|
|
padding = '10px 10px 10px 5px';
|
|
background = '#CCC';
|
|
overflow = 'scroll';
|
|
}
|
|
document.body.appendChild(el);
|
|
this.wrapper = el;
|
|
}
|
|
return this.wrapper;
|
|
},
|
|
|
|
addTitle: function(title) {
|
|
var el = document.createElement('div');
|
|
el.innerHTML = title;
|
|
this.getWrapper().appendChild(el);
|
|
},
|
|
|
|
createCanvas: function(title) {
|
|
this.addTitle(title);
|
|
|
|
var canvas = document.createElement('canvas');
|
|
canvas.width = this.size.width;
|
|
canvas.height = this.size.height;
|
|
canvas.style.width = this.size.width + 'px';
|
|
canvas.style.height = this.size.height + 'px';
|
|
canvas.style.border = '1px solid #000';
|
|
var context2d = canvas.getContext('2d');
|
|
this.getWrapper().appendChild(canvas);
|
|
|
|
return canvas;
|
|
},
|
|
|
|
displayMask: function(title, mask) {
|
|
if(!this.enabled) {
|
|
return;
|
|
}
|
|
var canvas = this.createCanvas(title);
|
|
var context2d = canvas.getContext('2d');
|
|
|
|
context2d.clearRect(0,0, this.size.width, this.size.height);
|
|
|
|
|
|
var image_data = context2d.getImageData(0, 0, this.size.width, this.size.height);
|
|
var ofs_m = 0;
|
|
var ofs_i = 0;
|
|
for(var y=0; y<this.size.height; y++) {
|
|
for(var x=0; x<this.size.width; x++) {
|
|
var c = mask[ofs_m] * 255;
|
|
//var c = Math.floor(Math.random() * 256);
|
|
image_data.data[ofs_i] = c;
|
|
image_data.data[ofs_i + 1] = c;
|
|
image_data.data[ofs_i + 2] = c;
|
|
image_data.data[ofs_i + 3] = 255;
|
|
ofs_i += 4;
|
|
ofs_m += 1;
|
|
}
|
|
}
|
|
context2d.putImageData(image_data, 0, 0);
|
|
},
|
|
|
|
displayCanvas: function(title, canvas) {
|
|
if(!this.enabled) {
|
|
return;
|
|
}
|
|
this.addTitle(title);
|
|
|
|
var image = new Image();
|
|
image.width = this.size.width;
|
|
image.height = this.size.height;
|
|
image.src = canvas.toDataURL("image/png");
|
|
this.getWrapper().append(image);
|
|
}
|
|
|
|
} |