I've currently got an existing application that uses Highcharts. Everything in the application works perfectly and in all browsers. The graph consists of multiple series all with different colours.
Recently I added the ability server side using PhantomJS to generate automated PDF reports from various of the application pages. Including the Highcharts rendered graph.
I do this using the following command:
phantomjs convertpage.js url file size
The convertpage.js contains the following:
var page = require('webpage').create(),
system = require('system'),
address, output, size;
if (system.args.length < 3 || system.args.length > 5) {
console.log('Usage: rasterize.js URL filename [paperwidth*paperheight|paperformat] [zoom]');
console.log(' paper (pdf output) examples: "5in*7.5in", "10cm*20cm", "A4", "Letter"');
phantom.exit(1);
} else {
address = system.args[1];
output = system.args[2];
page.viewportSize = { width: 600, height: 600 };
if (system.args.length > 3 && system.args[2].substr(-4) === ".pdf") {
size = system.args[3].split('*');
page.paperSize = size.length === 2 ? { width: size[0], height: size[1], margin: '0px' }
: { format: system.args[3], orientation: 'portrait', margin: '1cm' };
}
if (system.args.length > 4) {
page.zoomFactor = system.args[4];
}
page.open(address, function (status) {
if (status !== 'success') {
console.log('Unable to load the address!');
phantom.exit();
} else {
var timeout = address.indexOf('graphs')>-1 ? 20000 : 500;
console.log('Timeout: '+timeout);
window.setTimeout(function () {
page.render(output);
phantom.exit();
}, timeout);
}
});
}
On the application page that is rendering chart I am setting the series in the following manner, which is working well on the pages themselves via a browser:
$.each(dbFields, function (key, field){
var tmp = new Object();
tmp.name = fieldsObject[key];
tmp.color = colorsObject[key];
tmp.data = filtered[field];
tmp.min = 0;
tmp.lineWidth = 1.5;
seriesCalculated.push(tmp);
});
However, when I generate it using the script above and PhantomJS none of the colours are applied and neither the line thickness... it's simply a very thick light grey line for each series.
Any help/ideas would by much appreciated!
Sample of incorrect output can be found here
Related
There are 2 examples in the Matterport SDK for Embeds documentation to show how to place Mattertags in a scene:
The Intersection Inspector which only allows you to see coordinates for placing a Mattertag where the cursor is if you wait a little bit ... Not very user friendly, you need to copy the coordinates manually in your program.
The Transient Tags Editor which enable you to interactively place multiple Mattertags visually, edit them and then to extract them easily in a JSON file ...
I was wondering how to reproduce the Transient Tags Editor visual UX as I would like to use it in an application.
Insert Mattertags into the model visually
The source code of the app of the Transient Tags Editor is privately hosted on github (Maybe because it doesn't run perfectly on Firefox?), unlike the source code of the Intersection Inspector which is publicly hosted on JSFiddle.
But the user friendliness of the Transient Tags Editor intrigued me and I wanted to understand the difference between the two tools Matterport SDK provides to find out Mattertags coordinates.
How the Intersection Inspector works
The Intersection Inspector uses a timer to display a button at the position of the Pointer when the user does not move the pointer for more than one second. The user can then click the button to see the Mattertag coordinates and copy them manually ...
To achieve that, it needs the current Camera position, which it obtains by observing the camera's pose property:
var poseCache;
mpSdk.Camera.pose.subscribe(function(pose) {
poseCache = pose;
});
Also, it needs the current Pointer position, which it obtains by observing the pointer's intersection property:
var intersectionCache;
mpSdk.Pointer.intersection.subscribe(function(intersection) {
intersectionCache = intersection;
intersectionCache.time = new Date().getTime();
button.style.display = 'none';
buttonDisplayed = false;
});
※ An intersection event is triggered the user moves the pointer, so we hide the button to make sure it is not displayed before the one second delay is over.
Then, a timer is set up using setInterval() to display the button at the right time:
setInterval(() => {
// ...
}, 16);
In the timer callback, we check wether all the conditions to display the button are met ...
First, check we have the information we need:
setInterval(() => {
if (!intersectionCache || !poseCache) {
return;
}
// ...
}, 16);
Then, check one second has elapsed since the last intersection event was received, or we wait the next tick to check again:
var delayBeforeShow = 1000;
setInterval(() => {
if (!intersectionCache || !poseCache) {
return;
}
const nextShow = intersectionCache.time + delayBeforeShow;
if (new Date().getTime() > nextShow) {
// ...
}
}, 16);
Finally, we check the button is not already being displayed:
var delayBeforeShow = 1000;
var buttonDisplayed = false;
setInterval(() => {
if (!intersectionCache || !poseCache) {
return;
}
const nextShow = intersectionCache.time + delayBeforeShow;
if (new Date().getTime() > nextShow) {
if (buttonDisplayed) {
return;
}
// ...
}
}, 16);
Once the conditions are met, we can display the button using Conversion.worldToScreen() to get the screen coordinate of the pointer :
// ...
setInterval(() => {
// ...
if (new Date().getTime() > nextShow) {
// ...
var size = {
w: iframe.clientWidth,
h: iframe.clientHeight,
};
var coord = mpSdk.Conversion.worldToScreen(intersectionCache.position, poseCache, size);
button.style.left = `${coord.x - 25}px`;
button.style.top = `${coord.y - 22}px`;
button.style.display = 'block';
buttonDisplayed = true;
}
}, 16);
The button is a simple HTML button hidden by default using display: none; and positioned relative to the body with position: absolute;.
When the user clicks the button, the current coordinates of the pointer are displayed in a <div> tag above the <iframe> and the button is hidden:
button.addEventListener('click', function() {
text.innerHTML = `position: ${pointToString(intersectionCache.position)}\nnormal: ${pointToString(intersectionCache.normal)}\nfloorId: ${intersectionCache.floorId}`;
button.style.display = 'none';
iframe.focus();
});
The coordinates are formatted using the following function:
function pointToString(point) {
var x = point.x.toFixed(3);
var y = point.y.toFixed(3);
var z = point.z.toFixed(3);
return `{ x: ${x}, y: ${y}, z: ${z} }`;
}
Now, let's see how the easier-to-use and user-friendlier Transient Tags Editor interface works ...
How the Transient Tag Editor works
The Intersection Inspector is enough if you just have a few __Mattertag__s to set permanently in a few models in your application. But if you need your users to set tags interactively in models, something like the Transient Tags Editor's GUI is a better starting point.
The main advantage of using the Transient Tags Editor is that you can see how the Mattertag will be displayed before creating it and! That allows you to place the tag precisely without trial and error ...
To add a tag, you must click on the "Place New Tag" button to enter the "add tag" mode, then you can place one new tag anywhere you want.
We will only focus on that aspect of the editor and produce a simplified code sample that only add tags:
As the user move a tag along the pointer when in "add tag" mode, the first step is to create a new tag and place it. Let's create a function for that using Mattertag.add():
function addTag() {
if(!tag){
mpSdk.Mattertag.add([{
label: "Matterport Tag",
description: "",
anchorPosition: {x: 0, y: 0, z: 0},
stemVector: {x:0, y: 0, z: 0},
color: {r: 1, g: 0, b: 0},
}])
.then((sid) => {
tag = sid[0];
})
.catch( (e) => {
console.error(e);
})
}
}
Then we will have to place the tag at a position near the pointer, and update its position as the user moves the pointer, so let's create a function for that using Mattertag.editPosition():
function updateTagPos(newPos, newNorm=undefined, scale=undefined){
if(!newPos) return;
if(!scale) scale = .33;
if(!newNorm) newNorm = {x: 0, y: 1, z: 0};
mpSdk.Mattertag.editPosition(tag, {
anchorPosition: newPos,
stemVector: {
x: scale * newNorm.x,
y: scale * newNorm.y,
z: scale * newNorm.z,
}
})
.catch(e =>{
console.error(e);
tag = null;
});
}
As you can see the updateTagPos() function takes 3 parameters:
newPos: the new anchor position for the Mattertag.
newNorm: an optional new stem vector for the Mattertag.
scale: an optional new scale for the stem of the Mattertag.
To update the tag position as the user moves the pointer, let's observe the pointer's intersection property to call updateTagPos():
mpSdk.Pointer.intersection.subscribe(intersectionData => {
if(tag){
if(intersectionData.object === 'intersectedobject.model' || intersectionData.object === 'intersectedobject.sweep'){
updateTagPos(intersectionData.position, intersectionData.normal);
}
}
});
To place the new tag, the user simply clicks their mouse button, the Transient Tags Editor provides its own version of the document.activeElement method for intercepting clicks on the <iframe> (but does not work with Firefox so the editor use a quite complex workaround):
function focusDetect(){
const eventListener = window.addEventListener('blur', function() {
if (document.activeElement === iframe) {
placeTag(); //function you want to call on click
setTimeout(function(){ window.focus(); }, 0);
}
window.removeEventListener('blur', eventListener );
});
}
But, we will use our version which works better with Firefox (But still stop working after the first click in Firefox for whatever reason):
window.addEventListener('blur',function(){
window.setTimeout(function () {
if (document.activeElement === iframe) {
placeTag(); //function you want to call on click
window.focus()
addTag();
}
}, 0);
});
Finally, let's the function that navigates to the new tag and opens its billboard, usingMattertag.navigateToTag()
function placeTag(){
if(tag) mpSdk.Mattertag.navigateToTag(tag, mpSdk.Mattertag.Transition.INSTANT);
tag = null;
}
Simple Editor Code Sample
First, the complete JavaScript source code:
"use strict";
const sdkKey = "aaaaXaaaXaaaXaaaXaaaXaaa"
const modelSid = "iL4RdJqi2yK";
let iframe;
let tag;
document.addEventListener("DOMContentLoaded", () => {
iframe = document.querySelector('.showcase');
iframe.setAttribute('src', `https://my.matterport.com/show/?m=${modelSid}&help=0&play=1&qs=1>=0&hr=0`);
iframe.addEventListener('load', () => showcaseLoader(iframe));
});
function showcaseLoader(iframe){
try{
window.MP_SDK.connect(iframe, sdkKey, '3.10')
.then(loadedShowcaseHandler)
.catch(console.error);
} catch(e){
console.error(e);
}
}
function loadedShowcaseHandler(mpSdk){
addTag()
function placeTag(){
if(tag) mpSdk.Mattertag.navigateToTag(tag, mpSdk.Mattertag.Transition.INSTANT);
tag = null;
}
window.addEventListener('blur',function(){
window.setTimeout(function () {
if (document.activeElement === iframe) {
placeTag(); //function you want to call on click
window.focus()
addTag();
}
}, 0);
});
function updateTagPos(newPos, newNorm=undefined, scale=undefined){
if(!newPos) return;
if(!scale) scale = .33;
if(!newNorm) newNorm = {x: 0, y: 1, z: 0};
mpSdk.Mattertag.editPosition(tag, {
anchorPosition: newPos,
stemVector: {
x: scale * newNorm.x,
y: scale * newNorm.y,
z: scale * newNorm.z,
}
})
.catch(e =>{
console.error(e);
tag = null;
});
}
mpSdk.Pointer.intersection.subscribe(intersectionData => {
if(tag){
if(intersectionData.object === 'intersectedobject.model' || intersectionData.object === 'intersectedobject.sweep'){
updateTagPos(intersectionData.position, intersectionData.normal);
}
}
});
function addTag() {
if(!tag){
mpSdk.Mattertag.add([{
label: "Matterport Tag",
description: "",
anchorPosition: {x: 0, y: 0, z: 0},
stemVector: {x:0, y: 0, z: 0},
color: {r: 1, g: 0, b: 0},
}])
.then((sid) => {
tag = sid[0];
})
.catch( (e) => {
console.error(e);
})
}
}
} // loadedShowcaseHandler
The HTML source code:
<!DOCTYPE html>
<html>
<head>
<title>Transient Tag Editor</title>
<style>
#showcase {
width: 100%;
height: 100vh;
}
</style>
<script src="https://static.matterport.com/showcase-sdk/2.0.1-0-g64e7e88/sdk.js"></script>
<script src="/js/app-editor.js" type="text/javascript" defer></script>
</head>
<body>
<iframe id="showcase" frameborder="0" allowFullScreen allow="xr-spatial-tracking"></iframe>
</body>
</html>
It works!
Complete Code
The complete code for this sample and others is available on github:
github.com/loic-meister-guild/pj-matterport-sdk-2021-tutorial
See Also
Matterport SDK 2021 Tutorial
Node.js + Express Tutorial for 2021
How to detect a click event on a cross domain iframe
I am implementing direct upload with Shrine, jquery.fileupload and cropper.js
in the add portion I am loading the image from the file upload to modal, define the cropper and show the modal
if (data.files && data.files[0]) {
var reader = new FileReader();
var $preview = $('#preview_avatar');
reader.onload = function(e) {
$preview.attr('src', e.target.result); // insert preview image
$preview.cropper({
dragMode: 'move',
aspectRatio: 1.0 / 1.0,
autoCropArea: 0.65,
data: {width: 270, height: 270}
})
};
reader.readAsDataURL(data.files[0]);
$('#crop_modal').modal('show', {
backdrop: 'static',
keyboard: false
});
}
Then on the modal button click I get the cropped canvas call on it toBlob and submit to S3
$('#crop_button').on('click', function(){
var options = {
extension: data.files[0].name.match(/(\.\w+)?$/)[0], // set extension
_: Date.now() // prevent caching
};
var canvas = $preview.cropper('getCroppedCanvas');
$.getJSON('/images/cache/presign', options).
then(function (result) {
data.formData = result['fields'];
data.url = result['url'];
data.paramName = 'file';
if (canvas.toBlob) {
canvas.toBlob(
function (blob) {
var file = new File([blob], 'cropped_file.jpeg');
console.log('file', file);
data.files[0] = file;
data.originalFiles[0] = data.files[0];
data.submit();
},
'image/jpeg'
);
}
});
});
After the upload to S3 is done I am writing to image attributes to hidden field, closing the modal and destroying the cropper
done: function (e, data) {
var image = {
id: data.formData.key.match(/cache\/(.+)/)[1], // we have to remove the prefix part
storage: 'cache',
metadata: {
size: data.files[0].size,
filename: data.files[0].name.match(/[^\/\\]*$/)[0], // IE returns full path
// mime_type: data.files[0].type
mime_type: 'image/jpeg'
}
};
console.log('image', image);
$('.cached-avatar').val(JSON.stringify(image));
$('#crop_modal').modal('hide');
$('#preview_avatar').cropper('destroy');
}
An chrome everything worked fine from the very beginning, but then I figured out the safari has no toBlob functionality.
I found this one:
https://github.com/blueimp/JavaScript-Canvas-to-Blob
And toBlob is not a function error was gone..
Now I can not save the image due to some mime type related issue.
I was able to find out the exact location where it fails on safari but not chrome.
determine_mime_type.rb line 142
on line 139 in the options = {stdin_data: io.read(MAGIC_NUMBER), binmode: true}
the stdin_data is empty after the io.read
Any ideas?
Thank you!
UPDATE
I was able to figure out that the url to the cached image returned by the
$.getJSON('/images/cache/presign', options)
returns empty file when cropped and uploaded from safari.
So as I mentioned in the question safari uploaded empty file once it was cropped by cropper.js.
The problem clearly originated from this block:
if (canvas.toBlob) {
canvas.toBlob(
function (blob) {
var file = new File([blob], 'cropped_file.jpeg');
console.log('file', file);
data.files[0] = file;
data.originalFiles[0] = data.files[0];
data.submit();
},
'image/jpeg'
);
}
I found in some comment on one of the articles I read that safari does some thing like "file.toString" which in my case resulted in empty file upload.
I appended the blob directly without creating a file from it first and everything worked fine.
if (canvas.toBlob) {
canvas.toBlob(
function (blob) {
data.files[0] = blob;
data.files[0].name = 'cropped_file.jpeg';
data.files[0].type = 'image/jpeg';
data.originalFiles[0] = data.files[0];
data.submit();
},
'image/jpeg'
);
}
In one file, I have
go
In t2.html I have
<script>
document.write(window.opener);
</script>
On Safari on iOS, and on Chrome on the Mac and on pretty much every other browser, it prints out [object Window] like you'd expect.
On Chrome on iOS, I get null.
How do I get to the window that opened this window?
This code solves the problem you are talking about (specifically for issues with Chrome ios not liking "pop ups"), but in reference to Paypal Adaptive Payments where it opens a "pop up" and redirects to Paypal page for payment.
The key is that you have to:
Initiate the window.open directly from a button/link click
You must use the _blank as the window "name" (and not choose your own)
The main thing you want/need is:
var win;
//VERY IMPORTANT - You must use '_blank' and NOT name the window if you want it to work with chrome ios on iphone
//See this bug report from google explaining the issue: https://code.google.com/p/chromium/issues/detail?id=136610
win = window.open(paypalURL,'_blank');
//Initiate returnFromPayPal function if the pop up window is closed
if (win && win.closed) {
returnFromPayPal();
}
Here is the full code that you can follow (ignore anything that doesn't apply to what you are doing).
<div>
<?php $payUrl = 'https://www.paypal.com/webapps/adaptivepayment/flow/pay?expType=mini&paykey=' . $payKey ?>
<button onclick="loadPayPalPage('<?php echo $payUrl; ?>')" title="Pay online with PayPal">PayPal</button>
</div>
<script>
function loadPayPalPage(paypalURL)
{
var ua = navigator.userAgent;
var pollingInterval = 0;
var win;
// mobile device
if (ua.match(/iPhone|iPod|Android|Blackberry.*WebKit/i)) {
//VERY IMPORTANT - You must use '_blank' and NOT name the window if you want it to work with chrome ios on iphone
//See this bug report from google explaining the issue: https://code.google.com/p/chromium/issues/detail?id=136610
win = window.open(paypalURL,'_blank');
pollingInterval = setInterval(function() {
if (win && win.closed) {
clearInterval(pollingInterval);
returnFromPayPal();
}
} , 1000);
}
else
{
//Desktop device
var width = 400,
height = 550,
left,
top;
if (window.outerWidth) {
left = Math.round((window.outerWidth - width) / 2) + window.screenX;
top = Math.round((window.outerHeight - height) / 2) + window.screenY;
} else if (window.screen.width) {
left = Math.round((window.screen.width - width) / 2);
top = Math.round((window.screen.height - height) / 2);
}
//VERY IMPORTANT - You must use '_blank' and NOT name the window if you want it to work with chrome ios on iphone
//See this bug report from google explaining the issue: https://code.google.com/p/chromium/issues/detail?id=136610
win = window.open(paypalURL,'_blank','top=' + top + ', left=' + left + ', width=' + width + ', height=' + height + ', location=0, status=0, toolbar=0, menubar=0, resizable=0, scrollbars=1');
pollingInterval = setInterval(function() {
if (win && win.closed) {
clearInterval(pollingInterval);
returnFromPayPal();
}
} , 1000);
}
}
var returnFromPayPal = function()
{
location.replace("www.yourdomain.com/paypalStatusCheck.php");
// Here you would need to pass on the payKey to your server side handle (use session variable) to call the PaymentDetails API to make sure Payment has been successful
// based on the payment status- redirect to your success or cancel/failed page
}
</script>
This seems to be a bigger story. See Bugtracker here:
http://code.google.com/p/chromium/issues/detail?id=136610&q=window.opener&colspec=ID%20Pri%20Mstone%20ReleaseBlock%20OS%20Area%20Feature%20Status%20Owner%20Summary
But it seems, as if iframes could handle the parent-property, so maybe you could switch your app from using popups to using an overlay.
If you want to pass values from child to parent use the following code.
Add following code to parent page:
var hidden, state, visibilityChange;
if (typeof document.hidden !== "undefined") {
hidden = "hidden";
visibilityChange = "visibilitychange";
state = "visibilityState";
} else if (typeof document.mozHidden !== "undefined") {
hidden = "mozHidden";
visibilityChange = "mozvisibilitychange";
state = "mozVisibilityState";
} else if (typeof document.msHidden !== "undefined") {
hidden = "msHidden";
visibilityChange = "msvisibilitychange";
state = "msVisibilityState";
} else if (typeof document.webkitHidden !== "undefined") {
hidden = "webkitHidden";
visibilityChange = "webkitvisibilitychange";
state = "webkitVisibilityState";
}
// Add a listener that constantly changes the title
document.addEventListener(visibilityChange, function () {
if (localStorage.getItem("AccountName")) {
$("#txtGrower").val(localStorage.getItem("AccountName"));
}
if (localStorage.getItem("AccountID")) {
$("#hdnGrower").val(localStorage.getItem("AccountID"));
}
}, false);
Add following in child page (Any preferred event)
function CloseChildAndLoadValuesToParent() {
localStorage.setItem("AccountName", 'MyAccountName');
localStorage.setItem("AccountID", 'MyAccountID');
window.close();
}
I am writing a plugin for TinyMCE to crop images. This code works on firefox but doesn’t seem to work in other browsers.
Basically, I’m using JCrop to get the co-ordinates of the image and selected area and passing it to a server-side
method which does the cropping and returns the updated width, height and image src.
After, getting back the results. I update the image dimensions and src as follows.
tinyMCE.activeEditor.selection.getNode().src = croppedImageSource;
tinyMCE.activeEditor.selection.getNode().width = croppedImageWidth;
tinyMCE.activeEditor.selection.getNode().height = croppedImageHeight;
The Server-side method and crop co-ordinates are working as expected. While, the above code is not working well. Works on firefox but not on other browsers.
I was wondering If was updating the image selected in TinyMCE correctly?
Here is my full javascript function
function cropAndSave()
{
var imgSrc = document.getElementById('jcrop_target').src;
if(checkJcropCoords())
{
$.ajax({
async: false,
url: "/DocViewImageCrop.page",
type: 'POST',
data:
{
imgData: imgSrc,
cW: $("#w").val(),
cH: $("#h").val(),
cX: $("#x").val(),
cY: $("#y").val()
},
dataType: 'json',
complete: function(xmlRequestObject, successString)
{
var fileExists = xmlRequestObject.responseXML.getElementsByTagName("fileExists")[0].firstChild.nodeValue;
if(fileExists == undefined || fileExists == "false")
{
alert('Image not found on server. Try uploading the image, before attempting to resize');
}
else
{
tinyMCE.activeEditor.selection.getNode().src = xmlRequestObject.responseXML.getElementsByTagName("imgsrc")[0].firstChild.nodeValue;
tinyMCE.activeEditor.selection.getNode().width = xmlRequestObject.responseXML.getElementsByTagName("width")[0].firstChild.nodeValue;
tinyMCE.activeEditor.selection.getNode().height = xmlRequestObject.responseXML.getElementsByTagName("height")[0].firstChild.nodeValue;
}
}
});
}
}
You may try
tinyMCE.activeEditor.selection.getNode().setAttribute('scr', croppedImageSource);
tinyMCE.activeEditor.selection.getNode().setAttribute('width', croppedImageWidth);
tinyMCE.activeEditor.selection.getNode().setAttribute('height', croppedImageHeight);
or jQuery
var $node = $( tinyMCE.activeEditor.selection.getNode() );
$node.attr('scr', croppedImageSource);
$node.attr('width', croppedImageWidth);
$node.attr('height', croppedImageHeight);
For example, this page:
http://jqueryui.com/demos/show/
I knew that the show's prototype is: show( effect, [options], [speed], [callback] ), and it says "options:A object/hash including specific options for the effect." What are these specific options?? I can't find them :-(
You can see some examples in the source code on this page: http://jqueryui.com/demos/effect/#option-options
See this part of the code, for instance:
// most effect types need no options passed by default
var options = {};
// some effects have required parameters
if ( selectedEffect === "scale" ) {
options = { percent: 0 };
} else if ( selectedEffect === "transfer" ) {
options = { to: "#button", className: "ui-effects-transfer" };
} else if ( selectedEffect === "size" ) {
options = { to: { width: 200, height: 60 } };
}