JointJS drawing multiple custom connectors to an element - javascript

I'm using my own custom connector implementation and i want to be able to consider other connectors to the same elements in order to calculate the source and target points better.
joint.connectors.custom = function (sourcePoint, targetPoint, vertices) {
// I want to calculate the "middle" point while considering other links that might interrupt
var sourceMiddleX = this.sourceBBox.x + this.sourceBBox.width / 2;
var d = ['M', sourcePoint.x, sourcePoint.y, targetPoint.x, targetPoint.y];
return d.join(' ');
};
So far i couldn't find anything helpful under the function context nor under the VElement..
Unless anyone has a better idea, i'll pass the total links per element in each model which doesn't feels right.
Thanks in advance!

Connector functions in JointJS are called with a value of this equal to a joint.dia.LinkView instance. Each linkView has access to the paper it's rendered in.
var paper = this.paper; // an instance of `joint.dia.Paper`
var graph = this.paper.model; // an instance of `joint.dia.Graph`
var link = this.model; // an instance of `joint.dia.Link`
// an instance of `joint.dia.Element` or `null`
var sourceElement = link.getSourceElement();
var sourceElementLinks = sourceElement
// an array of `joint.dia.Link` including the link
// these are connecting the same source element as the link
? graph.getConnectedLinks(sourceElement, { outbound: true })
// the link is not connected to a source element
: [];
// an instance of `joint.dia.Element` or `null`
var targetElement = link.getTargetElement();
var targetElementLinks = targetElement
// an array of `joint.dia.Link` including the link
// these are connecting the same target element as the link
? graph.getConnectedLinks(targetElement, { inbound: true })
// the link is not connected to a target element
: [];
You can also check the jumpOver connector, that implements a similar logic.

Related

Export SVG to PDF with groups/layers intact

I have an SVG object that looks like this:
Each of the inner <g> elements have <path>s in them.
I want to export this SVG to PDF so the groups translate to layers (OCGs), like this:
<ThroughCut>
<Path>
<Path>
<Path>
<Path>
<Graphics>
<Path>
<Path>
<Path>
<Path>
Yet any tool I have tried for this puts all objects in the same layer, and basically throws away information about groups.
Solutions in JavaScript or Python are preferred, but anything that executes from the command line on a UNIX machine will do.
I solved my problem as stated here, by following this PyMuPDF issue on Github.
Since I have control over the input SVG, I managed to solve the problem by parsing two SVGs to PDFs and combining them in separate layers of a new document. This is what I'm doing:
import fitz
from svglib.svglib import svg2rlg
from reportlab.graphics.renderPDF import drawToString
def svg_to_doc(path):
"""Using this function rather than `fitz`' `convertToPDF` because the latter
fills every shape with black for some reason.
"""
drawing = svg2rlg(path)
pdfbytes = drawToString(drawing)
return fitz.open("pdf", pdfbytes)
# Create a new blank document
doc = fitz.open()
page = doc.new_page()
# Create "Layer1" and "Layer2" OCGs and get their `xref`s
xref_1 = doc.add_ocg('Layer1', on=True)
xref_2 = doc.add_ocg('Layer2', on=True)
# Load "layer_1" and "layer_2" svgs and convert to pdf
doc_1 = svg_to_doc("my_layer_1.svg")
doc_2 = svg_to_doc("my_layer_2.svg")
# Set the `page` dimensions. Note: for me it makes sense to set the bounding
# box of the output to the same as `doc_1`, because I know `doc_1` contains
# `doc_2`. If that were not the case, I would set `bb` to be a new
# `fits.Rect` object that contained both `doc_1` and `doc_2`.
bb = doc_1[0].rect
page.setMediaBox(bb)
# Put the docs in their respective OCGs
page.show_pdf_page(bb, doc_1, 0, oc=xref_1)
page.show_pdf_page(bb, doc_2, 0, oc=xref_2)
# Save
doc.save("output.pdf")
If I load "output.pdf" in Adobe Acrobat the layers show. Curiously, the same is not the case for Adobe Illustrator (here they are simply "Clip Groups"). Regardless, I believe this solves the problem as stated above.
my_layer_1.svg
my_layer_2.svg
My other solution, although correct, does not produce a PDF that is compatible with Adobe standards (for example, Illustrator will not see the OCGs as legitimate layers—although strangely, Acrobat will).
In case one needs to produce a PDF that is compatible with Adobe standards, and will be loaded correctly in Illustrator, another option is to use the Illustrator scripting API.
Here's a script that one can use to convert a loaded SVG file into a PDF with the desired layer structure.
/** Convert an open file in Illustrator to PDF, after removing the first layer.
* Useful for converting SVGs into PDFs, where it is desired that the first level
* `<g>` elements are converted to layers/OCGs in the exported PDF.
*/
// Select export destination
const destFolder = Folder.selectDialog( 'Select folder for PDF files.', '~' );
// Get the PDF options to be used
const options = getOptions();
// The SVG should have a single `Layer1` top layer...
const doc = app.activeDocument;
if (doc.layers.length == 1) {
// ... remove it
removeFirstLayer(doc)
// Create a file pointer for export...
var targetFile = getTargetFile(doc.name, '.pdf', destFolder);
// ... and save save `doc` in the file pointer.
doc.saveAs(targetFile, options);
}
/* --------- */
/* Utilities */
/* --------- */
function getOptions() {
// Create PDFSaveOptions object
var pdfSaveOpts = new PDFSaveOptions();
// Set PDFSaveOptions properties (toggle these comment/uncomment)
pdfSaveOpts.acrobatLayers = true;
pdfSaveOpts.colorBars = true;
pdfSaveOpts.colorCompression = CompressionQuality.AUTOMATICJPEGHIGH;
pdfSaveOpts.compressArt = true; //default
pdfSaveOpts.embedICCProfile = true;
pdfSaveOpts.enablePlainText = true;
pdfSaveOpts.generateThumbnails = true; // default
pdfSaveOpts.optimization = true;
pdfSaveOpts.pageInformation = true;
// pdfSaveOpts.viewAfterSaving = true;
return pdfSaveOpts;
}
function removeFirstLayer(doc) {
// Get the layer to be removed
var firstLayer = doc.layers[0];
// Convert groups into new layers
for (var i=firstLayer.groupItems.length-1; i>=0; i--) {
var group = firstLayer.groupItems[i];
var newLayer = firstLayer.layers.add();
newLayer.name = group.name;
for (var j=group.pageItems.length-1; j>=0; j--)
group.pageItems[j].move(newLayer, ElementPlacement.PLACEATBEGINNING);
}
// Move new layers to the document and remove `firstLayer`
for (var i=firstLayer.layers.length-1; i>=0; i--)
firstLayer.layers[i].move(firstLayer.parent, ElementPlacement.PLACEATBEGINNING);
firstLayer.remove();
}
function getTargetFile(docName, ext, destFolder) {
var newName = "";
// Add extension is none exists
if (docName.indexOf('.') < 0)
newName = docName + ext;
else
newName += docName.substring(0, docName.lastIndexOf('.')) + ext;
// Create file pointer
var myFile = new File(destFolder + '/' + newName);
// Check that file permissions are granted
if (myFile.open("w"))
myFile.close();
else
throw new Error('Access is denied');
return myFile;
}
Put the script in a file that ends with .jsx, and place in inside your Scripts folder in Illustrator. Mine is located at /Applications/Adobe Illustrator 2021/Presets/en_US/Scripts/svgToPDF.jsx.
Restart Illustrator
Execute the script from the File > Scripts > svgToPDF menu item.
Drawbacks
Illustrator costs money.
You have to manually execute the SVG -> PDF conversion. Could be automated with AppleScript, but it's really not something you want to have running on a server.

How can I pass a geometry to the Map function in GEE?

I am trying to use the Map function in Google Earth Engine to clip an ImageCollection to a geometry. I have multiple areas of interest (AOIs) and thus would like to apply a generic clip function multiple times (for each AOI). However, when I create a function with two parameters (the image and the geometry) to map over, I get the error image.clip is not a function. When using the function with just one parameter (the image), it works just fine, but then I need to write two (or possible more) functions to do exactly the same task (i.e. clipping an image to a certain geometry).
I have tried the solutions in this post, but they do not work.
Any ideas on how to solve this issue?
Code:
// Get NTL data
var ntl = ee.ImageCollection("NOAA/VIIRS/DNB/MONTHLY_V1/VCMSLCFG");
// Define start and end year
var startYear = 2015;
var endYear = 2018;
// Filter montly and select 'avg_rad' band
var ntlMonthly = ntl.filter(ee.Filter.calendarRange(startYear, endYear, 'year'))
.filter(ee.Filter.calendarRange(1,12,'month'))
.select(['avg_rad']);
// Create geometries of AOIs
// -- Create a geometry of Venezuela
var geomVenezuela = ee.Geometry.Rectangle([-73.341258, 13.363291, -59.637555, -0.372893]);
// -- Create a geometry of Caracas (Venezuela's capital)
var geomCaracas = ee.Geometry.Rectangle([-67.062383, 10.558489, -66.667078, 10.364908]);
// Functions to crop to Venezuela (nationwide) and Caracas (local) resp.
var clipImageToGeometry = function(image, geom) {
return image.clip(geom);
}
// Apply crop function to the ImageCollection
var ntlMonthly_Venezuela = ntlMonthly.map(clipImageToGeometry.bind(null, geomVenezuela));
var ntlMonthly_Caracas = ntlMonthly.map(clipImageToCaracas.bind(null, geomCaracas));
// Convert ImageCollection to single Image (for exporting to Drive)
var ntlMonthly_Venezuela_image = ntlMonthly_Venezuela.toBands();
var ntlMonthly_Caracas_image = ntlMonthly_Caracas.toBands();
// Check geometry in map
Map.addLayer(geomCaracas, {color: 'red'}, 'planar polygon');
Map.addLayer(ntlMonthly_Caracas_image);
// Store scale (m. per pixel) in variable
var VenezuelaScale = 1000;
var CaracasScale = 100;
// Export the image, specifying scale and region.
Export.image.toDrive({
image: ntlMonthly_Caracas_image,
description: 'ntlMonthly_Caracas_'.concat(startYear, "_to_", endYear),
folder: 'GeoScripting',
scale: CaracasScale,
fileFormat: 'GeoTIFF',
maxPixels: 1e9
});
If I understood your question correctly:
If you want to crop each image in the ImageCollection to a geometry, you could just do
var ntlMonthly_Venezuela = ntlMonthly.map(function(image){return ee.Image(image).clip(geomVenezuela)})
And just repeat for other AOIs.
If you wan to cast it into a function:
var clipImageCollection = function(ic, geom){
return ic.map(function(image){return ee.Image(image).clip(geom)})
}
// Apply crop function to the ImageCollection
var ntlMonthly_Venezuela = clipImageCollection(ntlMonthly, geomVenezuela);
var ntlMonthly_Caracas = clipImageCollection(ntlMonthly, geomCaracas);

What is the correct way of writing a binding for D3.js animations?

I wonder how to create a d3js binding to simplify animations. I trying to use something similar to a dynamic version of TikZ coordinates, where you simply define a 2d coordinate and refer to it for positioning elements. Here I simply want to define an object M that encapsulate one value and some bindings to objects-attributes, e.g., radius of circle, x-position, y-position, ... . Every change of the value of M should animate the objects-attributes of attached objects at the same time.
I created a jsFiddle:
http://jsfiddle.net/66qxjze9/1/
In this example:
test.addBinding(line,["x1","x2"],[]);
test.addBinding(dot,["cx1"],[]);
test.addBinding(dot2,["cy1"],[function(v){return 2*v;}]);
I wanted to animate the x1, x2 attribute of a line, the cx1 attribute of a dot and the cy1 of dot2 when I call test.setValue(30);.
Basically I thought it should work like
// on value changes
this.setValue = function(x){
// update the value itself
this.v = x;
// update each binded-object
for(var i=0;i<this.entry.length;i++){
var bindObject = this.entry[i];
// update each binded-attribute of this object
for(var j=0;j<bindObject.property.length;j++){
// how to update?
// identify
var modifier = function(x){return x;};
// or a custom function ?
if(typeof bindObject.function[j] !== "undefined"){
modifier = bindObject.function[j];
}
var to = modifier(x);
var attrName = bindObject.property[j];
// update
bindObject.handle
.transition()
.duration(2000)
.attr(attrName, to);
}
}
};
My main idea is to use a wrapper for this value which knows which svg-attributes have to be changed.
I would like to use
// bind some svg elements
var test = new d3jsbinding();
test.addBinding(line,["x1","x2"],[]);
test.addBinding(dot,["cx1"],[]);
test.addBinding(dot2,["cx1"],[function(v){return 2*v;}]);
// update them (at the same time)
test.setValue(30);
Unfortunately I found no way to enqueue the attributes-updates (or better assign the new values of each attribute) before calling .transition().duration(2000)
Have a look at my bl.ocks
Essentially, you need to write the loop into:
d3.transition()
.duration(parent.duration)
.ease(parent.ease)
.each(function() {
... HERE ...
});
disclaimer:

NVD3.js Given X-Axis Value, Get all Lines' Y-Axis Values

I'm using the line-with-focus chart ( View Finder ) example in nvd3. That means there's 3 or 4 lines ( series ) being drawn on the graph. When i hover over any of the lines I want to get back all the y-values for all lines of that given x-axis position ( for the most part these will be interpolated y-values per line ).
I see in the nv.models.lineWithFocusChart source code that using a callback for the elementMouseover.tooltip event I can get my data's x-value back for the data points on the line.
The closest part of the source code that does what i want is with the interactiveGuideline code for the lineChart examples. However, i don't want to create a <rect> overlay with elementMousemove interaction. I think i can modify this code to filter my data and get each line's y-value, but I'm sure there's an easier way I'm not seeing.
I think I'm on the right track, but just wondering if someone had this need before and found a quicker route than the rabbit hole I'm about jump in.
Thanks for feedback
This is the basic functionality you're looking for, it still needs a bit of finesse and styling of the tooltips. (Right now the tooltip blocks the view of the points...)
Key code to call after the drawing the chart in (for example, within the nv.addGraph function on the NVD3 live code site):
d3.selectAll("g.nv-focus g.nv-point-paths")
.on("mouseover.mine", function(dataset){
//console.log("Data: ", dataset);
var singlePoint, pointIndex, pointXLocation, allData = [];
var lines = chart.lines;
var xScale = chart.xAxis.scale();
var yScale = chart.yAxis.scale();
var mouseCoords = d3.mouse(this);
var pointXValue = xScale.invert(mouseCoords[0]);
dataset
.filter(function(series, i) {
series.seriesIndex = i;
return !series.disabled;
})
.forEach(function(series,i) {
pointIndex = nv.interactiveBisect(series.values, pointXValue, lines.x());
lines.highlightPoint(i, pointIndex, true);
var point = series.values[pointIndex];
if (typeof point === 'undefined') return;
if (typeof singlePoint === 'undefined') singlePoint = point;
if (typeof pointXLocation === 'undefined')
pointXLocation = xScale(lines.x()(point,pointIndex));
allData.push({
key: series.key,
value: lines.y()(point, pointIndex),
color: lines.color()(series,series.seriesIndex)
});
});
/*
Returns the index in the array "values" that is closest to searchVal.
Only returns an index if searchVal is within some "threshold".
Otherwise, returns null.
*/
nv.nearestValueIndex = function (values, searchVal, threshold) {
"use strict";
var yDistMax = Infinity, indexToHighlight = null;
values.forEach(function(d,i) {
var delta = Math.abs(searchVal - d);
if ( delta <= yDistMax && delta < threshold) {
yDistMax = delta;
indexToHighlight = i;
}
});
return indexToHighlight;
};
//Determine which line the mouse is closest to.
if (allData.length > 2) {
var yValue = yScale.invert( mouseCoords[1] );
var domainExtent = Math.abs(yScale.domain()[0] - yScale.domain()[1]);
var threshold = 0.03 * domainExtent;
var indexToHighlight = nv.nearestValueIndex(
allData.map(function(d){ return d.value}), yValue, threshold
);
if (indexToHighlight !== null)
allData[indexToHighlight].highlight = true;
//set a flag you can use when styling the tooltip
}
//console.log("Points for all series", allData);
var xValue = chart.xAxis.tickFormat()( lines.x()(singlePoint,pointIndex) );
d3.select("div.nvtooltip:last-of-type")
.html(
"Point: " + xValue + "<br/>" +
allData.map(function(point){
return "<span style='color:" + point.color +
(point.highlight? ";font-weight:bold" : "") + "'>" +
point.key + ": " +
chart.yAxis.tickFormat()(point.value) +
"</span>";
}).join("<br/><hr/>")
);
}).on("mouseout.mine", function(d,i){
//select all the visible circles and remove the hover class
d3.selectAll("g.nv-focus circle.hover").classed("hover", false);
});
The first thing to figure out was which objects should I bind the events to? The logical choice was the Voronoi path elements, but even when I namespaced the event names to avoid conflict the internal event handlers nothing was triggering my event handling function. It seems that a parent <g> event captures the mouse events before they can reach the individual <path> elements. However, it works just fine if instead I bind the events to the <g> element that contains the Voronoi paths, and it has the added benefit of giving me direct access to the entire dataset as the data object passed to my function. That means that even if the data is later updated, the function is still using the active data.
The rest of the code is based on the Interactive Guideline code for the NVD3 line graphs, but I had to make a couple important changes:
Their code is inside the closure of the chart function and can access private variables, I can't. Also the context+focus graph has slightly different names/functionality for accessing chart components, because it is made up of two charts. Because of that:
chart in the internal code is chart.lines externally,
xScale and yScale have to be accessed from the chart axes,
the color scale and the x and y accessor functions are accessible within lines,
I have to select the tooltip instead of having it in a variable
Their function is called with custom event as the e parameter that has already had the mouse coordinates calculated, I have to calculate them myself.
One of their calculations uses a function (nv.nearestValueIndex) which is only initialized if you create an interactive layer, so I had to copy that function definition into mine.
I think that about covers it. If there's anything else you can't follow, leave a comment.

Draw2D: How to use a port as source/target for more than one connection?

So, I'm trying to build as a personal project my course curriculum and I decided to use Draw2D because I think it's pretty complete. I'm representing the courses as rectangles and setting connections between them to show which ones are pre-requisites for other courses, something like this:
The problem I'm having is that when trying to make the same port the source for two connections it just takes one and ignores the other. Any ideas? Below is a quick sample:
$(window).load(function () {
// Create the paint area. The id in the constructor must be
// an existing DIV
var canvas = new draw2d.Canvas("gfx_holder");
// create and add two nodes which contains Ports (In and OUT)
var start = new draw2d.shape.node.Hub();
var startLocator = new draw2d.layout.locator.BottomLocator(start);
var startLocator2 = new draw2d.layout.locator.BottomLocator(start);
var startPort = start.createPort("output", startLocator);
var end = new draw2d.shape.node.End();
var end2 = new draw2d.shape.node.End();
canvas.addFigure( start, 400,100);
canvas.addFigure( end, 200,150);
canvas.addFigure( end2, 600,150);
var c = new draw2d.Connection();
c.setTargetDecorator(new draw2d.decoration.connection.ArrowDecorator());
c.setSource(startPort);
c.setTarget(end.getInputPort(0));
canvas.addFigure(c);
var c2 = new draw2d.Connection();
c2.setTargetDecorator(new draw2d.decoration.connection.ArrowDecorator());
c2.setSource(startLocator2);
c2.setTarget(end2.getInputPort(0));
canvas.addFigure(c2);
});
I would suspect your problem lies in the setSource function call.
c.setSource(startPort);
c2.setSource(startLocator2);
One seems to specify a port, and the other a Locator.

Categories

Resources