Unexpected behavior of svg draggable/panzoom - svg

I need to use svg.js 3 with draggable and panzoom plugins. What I need to do is create some shapes and group it together and then attach draggable to entire group.
This is the code I managed so far:
var canvas = SVG()
.addTo('#svg-canvas')
.size($(window).width(), $(window).height())
.viewbox('0 0 ' + $(window).width() + ' ' + $(window).height())
.panZoom({
zoomFactor: 0.1,
wheelZoom: true,
zoomMin: 0.25,
zoomMax: 4
})
.zoom(2);
const headerHeight = 40;
const rowHeight = 24;
const radius = 4;
const borderTop = 1
const margin = 2;
const table = canvas.group();
const tableWrapper = canvas.rect(200, headerHeight).fill("#fff");
tableWrapper.radius(radius);
table.add(tableWrapper);
const headerWrapper = canvas.rect(200, headerHeight - radius).fill("transparent");
headerWrapper.move(0, radius);
table.add(headerWrapper);
const headerContent = canvas.foreignObject(200, headerHeight - radius);
headerContent.move(0, radius);
headerContent.add($('\
<div style="display:flex;justify-content:space-between;display:flex;padding:8px;margin-top:-1px;">\
<div>Header</div>\
<div>PK</div>\
</div>\
')[0]);
table.add(headerContent);
tableWrapper.height(44);
table.move(180, 150);
table.draggable();
Here is jsfiddle to test: https://jsfiddle.net/grayter/uhde3ykw/29/
The problem is that when I try to drag group the content of the group is flowing. I suppose that panzoom plugin is the reason of that behavior (it seems like draggable and panzoom drag event are interfering).
What am I missing? Is there a way to fix this issue?

Related

Fixed SVG Layer Position on Open Layers Map

I have a map with multiple Layers. One of them shall project an SVG on the map, indicating certain regions (if not obvious, it's the red area in the images). I got this working on the default zoom level.
But as soon as I change the zoom in any direction, the SVG layer just moves too much in a direction depending on where I initiate the zoom change.
This is the relevant code I have:
//Map init
var mapcenter = ol.proj.fromLonLat([10.615219, 51.799502]);
var layers = [];
var map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
center: mapcenter,
zoom: 9.5
})
});
//SVG init
const svgContainer = document.createElement('div');
const xhr = new XMLHttpRequest();
xhr.open('GET', '/shared/harzregionen.svg');
xhr.addEventListener('load', function () {
const svg = xhr.responseXML.documentElement;
svgContainer.ownerDocument.importNode(svg);
svgContainer.appendChild(svg);
});
xhr.send();
const width = 350;
const height = 0;
const svgResolution = 360 / width;
svgContainer.style.width = width + 'px';
svgContainer.style.height = height + 'px';
svgContainer.className = 'svg-layer';
//SVG Layer
map.addLayer(
new ol.layer.Vector({
render: function (frameState) {
const scale = svgResolution / frameState.viewState.resolution;
const center = frameState.viewState.center;
const size = frameState.size;
const cssTransform = ol.transform.composeCssTransform(
300,
120,
480 / frameState.viewState.resolution,
480 / frameState.viewState.resolution,
frameState.viewState.rotation,
-(center[0] - mapcenter[0]) / 420, //These values were kinda
(center[1] - mapcenter[1]) / 250 //trial and error for default zoom
);
svgContainer.style.transform = cssTransform;
svgContainer.style.opacity = this.getOpacity();
return svgContainer;
}
})
);
I'm pretty sure I have to use a certain formula to calculate the values in composeCssTransform but it feels like there should be an easy way to do it instead of trial and error until it somewhat works.
So, my question is: Is there an easy way to lock an SVG layer to a set position on the world map like the layers with markers?

signature-pad - resize not working

i'm using the signature-pad plugin and i'm having some issues whith the resize event:
- Multiple resizes lead to a loss in quality and the signature "moves" at each resize of the browser window ending with no signature in canvas.
- In some cases, the isEmpty() function wont work and i'll be able to save the empty signature.
Optional question : how can i detect an empty signature on php side ?
Thank you :)
Below my code :
$(window).resize(function() {
resizeCanvas();
});
var wrapper1 = document.getElementById("signature-pad"),
clearButton1 = wrapper1.querySelector("[data-action=clear]"),
canvas1 = wrapper1.querySelector("canvas"),
signaturePad1;
var wrapper2 = document.getElementById("signature-pad-paraphe"),
clearButton2 = wrapper2.querySelector("[data-action=clear]"),
canvas2 = wrapper2.querySelector("canvas"),
signaturePad2;
// Adjust canvas coordinate space taking into account pixel ratio,
// to make it look crisp on mobile devices.
// This also causes canvas to be cleared.
signaturePad1 = new SignaturePad(canvas1);
signaturePad2 = new SignaturePad(canvas2);
function resizeCanvas() {
//Sauvegarde sig / par
var sig = signaturePad1.toDataURL();
var par = signaturePad2.toDataURL();
var ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas1.width = canvas1.offsetWidth * ratio;
canvas1.height = canvas1.offsetHeight * ratio;
canvas1.getContext("2d").scale(ratio, ratio);
canvas2.width = canvas2.offsetWidth * ratio;
canvas2.height = canvas2.offsetHeight * ratio;
canvas2.getContext("2d").scale(ratio, ratio);
// redraw
signaturePad1.fromDataURL(sig);
signaturePad2.fromDataURL(par);
}
window.onresize = resizeCanvas;
resizeCanvas();
// Init -> retourne la bonne valeur de isEmpty -> !!? Not sure if needed
signaturePad1.clear();
signaturePad2.clear();
var signature = $('#confirm_delete_signature').val();
if(signature){
signaturePad1.fromDataURL(signature);
}
var paraphe = $('#confirm_delete_paraphe').val();
if(paraphe){
signaturePad2.fromDataURL(paraphe);
}
clearButton1.addEventListener("click", function (event) {
signaturePad1.clear();
});
clearButton2.addEventListener("click", function (event) {
signaturePad2.clear();
});
Here is i developed a little solution;
Here are two key DOM elements:
div#id_wrapper
canvas#id
Considered it may be applied at devices with different devicePixelRatio and on screens changins theirs width (f.i.: portrait-landscape orientation).
export class FlexSignatureComponent extends React.Component {
state = {
width: 0,
lines: [],
storedValue: undefined,
validationClass: '', // toggles between 'is-invalid'/'is-valid'
validationMessage: ''
}
The lib initiation is right after the component got loaded:
componentDidMount = () => {
this.signPad = new SignaturePad(document.getElementById(this.props.htmlid), {
onEnd: this.onChangeSignaturePad,
backgroundColor: '#fff'
});
if (this.valueHolder.current.value) {
const data = JSON.parse(this.valueHolder.current.value);
this.state.lines = data.value;
this.state.width = 100;
}
//you need the next workarounds if you have other onWidnowResize handlers manarging screen width
//setTimeout-0 workaround to move windowResizeHandling at the end of v8-enging procedures queue
// otherwise omit setTimeout and envoke func as it is
setTimeout(this.handleWindowResize, 0);
window.addEventListener("resize", () => setTimeout(this.handleWindowResize, 0));
}
First handle window resize change
handleWindowResize = () => {
if (this.state.storedValue) {
const prevWrapperWidth = this.state.width;
const currentWrapperWidth = $(`#${this.props.htmlid}_wrapper`).width();
const scale = prevWrapperWidth / currentWrapperWidth;
this.state.width = currentWrapperWidth;
this.setRescaledSignature(this.state.lines, scale);
this.resetCanvasSize();
this.signPad.fromData(this.state.lines)
} else
this.resetCanvasSize()
}
Second rescaleSignature to another width
setRescaledSignature = (lines, scale) => {
lines.forEach(line => {
line.points.forEach(point => {
point.x /= scale;
point.y /= scale;
});
});
}
Finally updated canvas size
resetCanvasSize = () => {
const canvas = document.getElementById(this.props.htmlid);
canvas.style.width = '100%';
canvas.style.height = canvas.offsetWidth / 1.75 + "px";
canvas.width = canvas.offsetWidth * devicePixelRatio;
canvas.height = canvas.offsetHeight * devicePixelRatio;
canvas.getContext("2d").scale(devicePixelRatio, devicePixelRatio);
}
Here we on every change add new drawn line to this.state.lines
and prepare the lines to be submited as json.
But before the submission they need to create deepCopy and to be rescaled to conventional size (its width is equal 100px and DPR is 1)
onChangeSignaturePad = () => {
const value = this.signPad.toData();
this.state.lines = value;
const currentWrapperWidth = $(`#${this.props.htmlid}_wrapper`).width();
const scale = currentWrapperWidth / 100;
const ratio = 1 / devicePixelRatio;
const linesCopy = JSON.parse(JSON.stringify(value));
this.setRescaledSignature(linesCopy, scale, ratio);
const data = {
signature_configs: {
devicePixelRatio: 1,
wrapper_width: 100
},
value: linesCopy
};
this.state.storedValue = JSON.stringify(data);
this.validate()
}
One more thing is the red button to swipe the previous signatures
onClickClear = (e) => {
e.stopPropagation();
this.signPad.clear();
this.valueHolder.current.value = null;
this.validate()
}
render() {
let {label, htmlid} = this.props;
const {validationClass = ''} = this.state;
return (
<div className="form-group fs_form-signature">
<label>{Label}</label>
<div className="fs_wr-signature">
<button className={'fs_btn-clear'} onClick={this.onClickClear}>
<i className="fas fa-times"></i>
</button>
<div id={htmlid + '_wrapper'} className={`w-100 fs_form-control ${validationClass}`}>
<canvas id={htmlid}/>
</div>
</div>
<div className={' invalid-feedback fs_show-feedback ' + validationClass}>Signature is a mandatory field</div>
</div>
)
}
postWillUnmount() {
this.signPad.off();
}
the used lib signature pad by szimek
Used React and Bootstrap and some custome styles
the result would be
You didn't provide a full example, or much explanation of the code, so it's hard to tell what all is going on here, but I'll do my best to give as full an answer as I can.
Saving
First, if I understand the docs correctly, $(window).resize will be triggered at the same time as window.onresize. You use both. That might be causing some issues, maybe even the issues with saving.
The following code is run once, and I'm not sure what it's supposed to do:
var signature = $('#confirm_delete_signature').val();
if(signature){
signaturePad1.fromDataURL(signature);
}
var paraphe = $('#confirm_delete_paraphe').val();
if(paraphe){
signaturePad2.fromDataURL(paraphe);
}
It looks like it's supposed to be deleting the signature (since the selector is #confirm_delete_signature), but it instead, it's restoring a signature from some data stored in the node as a string. That might be causing issues too.
That said, I'm not sure why saving isn't working, but I can't find the code of your saving function, so it's very hard to say. Maybe I missed something.
I'm not familiar with php, sorry.
Resizing
For resizing, I think the React version that #Alexey Nikonov made might work with React (I didn't run it). You have to scale the positions of the points of the lines along with the changing size of the canvas.
I wanted a version closer to vanilla js, so I recreated it with just signature_pad v4.1.4 and jQuery at https://jsfiddle.net/j2Lurpd5/1/ (with an improvement to ratio calculation).
The code is as follows, though it doesn't have a button to clear the canvas:
<div id="wrapper">
<canvas id="pad" width="200" height="100"></canvas>
</div>
canvas {
border: red 1px solid;
}
// Inspiration: https://stackoverflow.com/a/60057521
// Version with no React
const canvas = document.querySelector('#pad');
const signPad = new SignaturePad(canvas);
// Doesn't work without the #wrapper. Probably because #pad
// needs it to be able to be 100% of it. Not sure exactly
// why that makes a difference when #wrapper doesn't have
// a width set on it. Though #pad alone does work after the
// first resize.
let prevWidth = $('#wrapper').width();
let lines = [];
setTimeout(resizeSignatureAndCanvas, 0);
window.addEventListener("resize", () => setTimeout(resizeSignatureAndCanvas, 0));
window.addEventListener("orientationchange", () => setTimeout(resizeSignatureAndCanvas, 0));
function resizeSignatureAndCanvas () {
// Get the current canvas contents
lines = signPad.toData();
// if there are no lines drawn, don't need to scale them
if ( signPad.isEmpty() ) {
// Set initial size
resizeCanvas();
} else {
// Calculate new size
let currentWidth = $('#wrapper').width();
let scale = currentWidth / prevWidth;
prevWidth = currentWidth; // Prepare for next time
// Scale the contents along with the width
setRescaledSignature(lines, scale);
// Match canvas to window size/device change
resizeCanvas();
// Load the adjusted canvas contents
signPad.fromData(lines);
}
};
// This is really the key to keeping the contents
// inside the canvas. Getting the scale right is important.
function setRescaledSignature (lines, scale) {
lines.forEach(line => {
line.points.forEach(point => {
// Same scale to avoid warping
point.x *= scale;
point.y *= scale;
});
});
};
function resizeCanvas () {
/** Have to resize manually to keep the canvas the width of the
* window without distorting the location of the "pen". */
// I'm not completely sure of everything in here
const canvas = $('#pad')[0];
// Not sure why we need both styles and props
canvas.style.width = '100%';
canvas.style.height = (canvas.offsetWidth / 1.75) + 'px';
// When zoomed out to less than 100%, for some very strange reason,
// some browsers report devicePixelRatio as less than 1
// and only part of the canvas is cleared then.
let ratio = Math.max(window.devicePixelRatio || 1, 1);
// This part causes the canvas to be cleared
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext("2d").scale(ratio, ratio);
};
As you can see from my notes, I'm not completely sure why every part works, but from what I can tell it does preserve the behavior of the version that #Alexey Nikonov made.

KonvaJS, positioning editable text inputs

I need to position text inputs at various places on a KonvaJS layer. I found the following code at https://konvajs.github.io/docs/sandbox/Editable_Text.html and I'm trying to understand the textPosition, stageBox, and areaPosition vars in this code. I want my stage centered in the browser window, but when I do that, the textarea (activated on dblclick) pops up way off to the left. I can't get a console readout of the x/y coordinates, so I can't visualize how the positioning works &, thus, how to change it. Can anyone explain, or point me in the right direction?
var text_overlay = new Konva.Layer();
stage.add(text_overlay);
var textNode = new Konva.Text({
text: 'Some text here',
x: 20,
y: 50,
fontSize: 20
});
text_overlay.add(textNode);
text_overlay.draw();
textNode.on('dblclick', () => {
// create textarea over canvas with absolute position
// first we need to find its position
var textPosition = textNode.getAbsolutePosition();
var stageBox = stage.getContainer().getBoundingClientRect();
var areaPosition = {
x: textPosition.x + stageBox.left,
y: textPosition.y + stageBox.top
};
// create textarea and style it
var textarea = document.createElement('textarea');
document.body.appendChild(textarea);
textarea.value = textNode.text();
textarea.style.position = 'absolute';
textarea.style.top = areaPosition.y + 'px';
textarea.style.left = areaPosition.x + 'px';
textarea.style.width = textNode.width();
textarea.focus();
textarea.addEventListener('keydown', function (e) {
// hide on enter
if (e.keyCode === 13) {
textNode.text(textarea.value);
text_overlay.draw();
document.body.removeChild(textarea);
}
});
})
// add the layer to the stage
stage.add(text_overlay);
UPDATE: I solved part of the problem--the textarea showing up way out of position. You need to use 2 divs in the HTML file instead of one, like so:
<div id="containerWrapper" align="center"><div id="container"></div></div>
Thanks to Frens' answer on Draw border edges of the Konvajs container Stage in html for that one!

OpenLayers 4 - feature with circle with radius in meters doesn´t work?

I came from OpenLayers 2 now to OpenLayers 4.
I want to draw a circle with a special radius of meters.
I tried:
var circleStyle = new ol.style.Style({
fill: new ol.style.Fill({ color: 'rgba(255,0,0,0.4)' }),
stroke: new ol.style.Stroke({ color: 'rgb(255,0,0)', width: 2 }),
});
//var units = map.getView().getProjection().getUnits();
// var radiusM = circle.getRadius() * ol.proj.METERS_PER_UNIT[units];
var circleFeature = new ol.Feature({
geometry: new ol.geom.Circle(ol.proj.fromLonLat([Element.Position.Longitude, Element.Position.Latitude]), MetersToRadius(Element.RadiusInMetern)),
UserID: Element.UserID,
ID: Element.ID
});
circleFeature.setStyle(circleStyle);
vectorSource.addFeature(circleFeature);
but when I check the circle with control.ScaleLine it´s to small!
I found this code, but it doesn´t work on OpenLayer 4.
The object "projection.getPointResolution(resolutionAtEquator,
center);" doesnt exists!
var view = map.getView();
var projection = view.getProjection();
var resolutionAtEquator = view.getResolution();
var center = map.getView().getCenter();
var pointResolution=projection.getPointResolution(resolutionAtEquator, center);
var resolutionFactor = resolutionAtEquator / pointResolution;
var radius = (radius / ol.proj.METERS_PER_UNIT.m) * resolutionFactor;
return(radius);
you need to use
ol.proj.getPointResolution(projection,resolutionAtEquator,center);

How to draw custom dynamic billboards in Cesium.js

I'm currently using Cesium for a mapping application, and I have a requirement to have status indicators for each item I'm plotting (for example, if the item I'm plotting is an airplane, then I need to have a fuel status indicator). I can't use Cesium's drawing tools to do this because they are drawn using geographic locations, but I need my status indicators to simply be located near the billboard and not get farther away from the billboard as users zoom in and out.
Cesium's CZML documentation states that the billboard's "image" property can take a data URI, so I figured the easiest way to handle this would be for me to create an SVG path on the fly and embed it in the image property, but when I do this in Cesium, it does not show up. For example, I tried a simple test like this:
"data:image/svg+xml,<svg viewBox='0 0 40 40' height='25' width='25'
xmlns='http://www.w3.org/2000/svg'><path fill='rgb(91, 183, 91)' d='M2.379,
14.729L5.208,11.899L12.958,19.648L25.877,6.733L28.707,9.561L12.958,25.308Z'
/></svg>"
When that didn't show up, I tried just simple HTML and text values, like this:
"data:,Hello%2C%20World!"
and:
"data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E"
but those don't show up either. I am able to get a png to show up if I put the base64 string in the data URI, as well as a path to an image stored on the server, but I really need to be able to draw custom images on the fly. I can't use a fixed set of pre-generated images set with various statuses as a hack (I can explain why if anyone wants those details :) ).
Does anyone know if there's something I'm doing wrong here, or if there is another way to accomplish what I need to do?
Edit Just wanted to add that I am using Firefox version 29 and it normally has no problem displaying the non-encoded embedded SVGs like that. Just in case, that's one of the reasons I was also trying simple HTML or text.
Edit2 I am using CZML streaming from the back end to plot my items, here is a simple test example showing where I am trying to put the image information:
{
"id":"test",
"billboard" : {
"image" : "data:image/svg+xml,<svg viewBox='0 0 40 40' height='25' width='25' xmlns='http://www.w3.org/2000/svg'><path fill='rgb(91, 183, 91)' d='M2.379,
14.729L5.208,11.899L12.958,19.648L25.877,6.733L28.707,9.561L12.958,25.308Z'
/></svg>",
"show" : [ {"boolean" : true} ]
},
"position":{
"cartographicDegrees":[0.0, 0.0, 0.0]
},
"label":{"text":"TEST"},
}
If I put a base64 png string in there, or a path to a static image file, it works fine.
Thank you!
Simple JS code to insert SVG into cesium
// create the svg image string
var svgDataDeclare = "data:image/svg+xml,";
var svgCircle = '<circle cx="10" cy="10" r="5" stroke="black" stroke-width="3" fill="red" /> ';
var svgPrefix = '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="40px" height="40px" xml:space="preserve">';
var svgSuffix = "</svg>";
var svgString = svgPrefix + svgCircle + svgSuffix;
// create the cesium entity
var svgEntityImage = svgDataDeclare + svgString;
viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(-80.12, 25.46),
billboard: {
image: svgEntityImage
}
});
// test the image in a dialog
$("#dialog").html(svgString );
$("#dialog").dialog({
position: {my: "left top", at: "left bottom"}
});
My method to accomplish this is to create a canvas element, add text to it, crop it and convert that result into a data URL. It works great and it's quick.
I'm not using CZML, but this is how I did it:
buildImage:function(text,text2){
var self = this;
var canvas = new Element('canvas',{width:500,height:500});
var ctx = canvas.getContext("2d");
ctx.font = "bold 16px monospace";
ctx.fillText(text, 60, 20);
ctx.fillText(text2, 60, 50);
imagedata = self.cropCanvas(canvas,ctx);
return imagedata;
},
cropCanvas:function(canvas,ctx){
ww = canvas.width;
wh = canvas.height;
imageData = ctx.getImageData(0, 0, ww, wh);
var topLeftCorner = {};
topLeftCorner.x = 9999;
topLeftCorner.y = 9999;
var bottomRightCorner = {};
bottomRightCorner.x = -1;
bottomRightCorner.y = -1;
for (y = 0; y < wh; y++) {
for (x = 0; x < ww; x++) {
var pixelPosition = (x * 4) + (y * wh * 4);
a = imageData.data[pixelPosition+3]; //alpha
if (a > 0) {
if (x < topLeftCorner.x) {
topLeftCorner.x = x;
}
if (y < topLeftCorner.y) {
topLeftCorner.y = y;
}
if (x > bottomRightCorner.x) {
bottomRightCorner.x = x;
}
if (y > bottomRightCorner.y) {
bottomRightCorner.y = y;
}
}
}
}
topLeftCorner.x -= 2;
topLeftCorner.y -= 2;
bottomRightCorner.x += 2;
bottomRightCorner.y += 2;
relevantData = ctx.getImageData(topLeftCorner.x, topLeftCorner.y, bottomRightCorner.x -topLeftCorner.x, bottomRightCorner.y - topLeftCorner.y);
canvas.width = bottomRightCorner.x - topLeftCorner.x;
canvas.height = bottomRightCorner.y - topLeftCorner.y;
ww = canvas.width;
wh = canvas.height;
ctx.clearRect(0,0,ww,wh);
ctx.putImageData(relevantData, 0, 0);
return canvas.toDataURL();
}
These are two methods of a MooTools class, but can be easily rewritten into whatever framework (or no framework) you need.
Drawing an SVG does work as I expected it to, but I believe I may have just been getting one of the size elements wrong (height/width or x, y). Whenever those values don't match up just right, the image isn't shown because it's outside of the view area I've defined for it.
Note that I never did get the simple html example work, but that's not what I needed anyway, so I didn't pursue it further.

Resources