UNPKG

26.1 kBJavaScriptView Raw
1import { curry, findIndex, flow, get, isEmpty, isNaN, isString, kebabCase, map } from "lodash/fp";
2const RGBColor = require("rgbcolor");
3import * as VError from "verror";
4import { decode } from "he";
5import { generatePublicationXrefId } from "../gpml-utilities";
6import { normalize, radiansToDegrees } from "../spinoffs/Angle";
7import { isDefinedCXML } from "../gpml-utilities";
8// TODO are these ever used? PathVisio-Java
9// does not accept them as inputs in the
10// Rotation input field in the UI.
11// TODO if they are used, are the notes in
12// the XSD correct, or would "Right" actually
13// be 0 radians?
14const GPML_ROTATION_SIDE_TO_RAD = {
15 Top: 0,
16 Right: 0.5 * Math.PI,
17 Bottom: Math.PI,
18 Left: 3 / 2 * Math.PI
19};
20function decodeIfNotEmpty(input) {
21 return isEmpty(input) ? input : decode(input);
22}
23function parseAsNonNaNNumber(i) {
24 const parsed = Number(i);
25 if (isNaN(parsed)) {
26 throw new Error('Cannot parse "' + String(i) + '" as non-NaN number');
27 }
28 return parsed;
29}
30//*****************
31// Value Converters
32//*****************
33// NOTE: we use He.decode for many of these
34// because at some point some GPML files were
35// processed w/out using UTF-8, leaving some
36// strings garbled, such as author names.
37// TODO backpageHead could be further processed to yield displayName and standardName
38export function ID(gpmlElement) {
39 if (gpmlElement.hasOwnProperty("ID")) {
40 const { ID } = gpmlElement;
41 return isString(ID) ? ID : ID.content;
42 }
43 else {
44 return gpmlElement.Xref.ID;
45 }
46}
47// GPML2013-ish incorrectly used "rdf:id" where it was intented
48// to use "rdf:ID". We corrected that error before processing,
49// but CXML turns "rdf:ID" into "ID", and since we already have
50// a property "ID" on the element, CXML uses "$ID".
51export const $ID = flow(get("$ID"), generatePublicationXrefId);
52export const DB = flow(get("DB.content"), decodeIfNotEmpty);
53export const TITLE = flow(get("TITLE.content"), decodeIfNotEmpty);
54export const SOURCE = flow(get("SOURCE.content"), decodeIfNotEmpty);
55export const YEAR = get("YEAR.content");
56export const AUTHORS = flow(get("AUTHORS"), map(flow(get("content"), decodeIfNotEmpty)));
57export const BiopaxRef = flow(get("BiopaxRef"), map(generatePublicationXrefId));
58/*
59Meanings of Width
60-----------------
61
62In PathVisio-Java, GPML Width/Height for GPML Shapes is
63inconsistent when zoomed in vs. when at default zoom level.
64
65When zoomed in, GPML Width/Height refers to the distance from center of stroke (border)
66one one edge to center of stroke (border) on the opposite edge, meaning that shapes that
67run up to the edge are cropped.
68
69When at default zoom level, GPML Width/Height refers to the distance from outer edge of
70stroke (border) to outer edge of stroke (border) with no cropping.
71
72Because of this, LineThickness is also inconsistent.
73When zoomed in: approx. one half of specified LineThickness.
74When at default zoom level: approx. full specified LineThickness.
75
76For double lines, LineThickness refers to the the stroke (border) width of each line and
77the space between each line, meaning the stroke (border) width
78for the double line as a whole will be three times the listed LineThickness.
79
80For pvjs, we define GPML Width/Height to be from outer edge of stroke (border) on one
81side to outer edge of stroke (border) on the opposite site, meaning visible width/height
82may not exactly match between pvjs and PathVisio.
83See issue https://github.com/PathVisio/pathvisio/issues/59
84
85* DOM box model
86 - box-sizing: border-box
87 visible width = width
88 (width means border + padding + width of the content)
89 (see https://css-tricks.com/international-box-sizing-awareness-day/)
90 - box-sizing: content-box
91 visible width = width + border + padding
92 (width means width of the content)
93* PathVisio-Java
94 - Zoomed in
95 - LineStyle NOT Double
96 visible width ≈ GPMLWidth
97 visible height ≈ GPMLHeight
98 (matches box-sizing: border-box)
99 - LineStyle Double
100 visible width ≈ Width + 1.5 * LineThickness
101 visible height ≈ Height + 1.5 * LineThickness
102 - Zoomed out
103 - LineStyle NOT Double
104 visible width ≈ GPMLWidth + LineThickness
105 visible height ≈ GPMLHeight + LineThickness
106 (matches box-sizing: border-box)
107 (one half LineThickness on either side yields a full LineThickness to add
108 to width/height).
109 - LineStyle Double
110 visible width = Width + 3 * LineThickness
111 visible height = Height + 3 * LineThickness
112* SVG: visible width = width + stroke-width
113* kaavio/pvjs: same as DOM box model with box-sizing: border-box
114//*/
115const getDimension = curry(function (dimensionName, gpmlElement) {
116 const dimension = gpmlElement.Graphics[dimensionName];
117 if (findIndex(function ({ Key, Value }) {
118 return Key === "org.pathvisio.DoubleLineProperty";
119 }, gpmlElement.Attribute) > -1) {
120 return dimension + LineThickness(gpmlElement);
121 }
122 else {
123 return dimension;
124 }
125});
126export const Height = getDimension("Height");
127export const Width = getDimension("Width");
128export function CenterX(gpmlElement) {
129 const { CenterX } = gpmlElement.Graphics;
130 return CenterX - Width(gpmlElement) / 2;
131}
132export function CenterY(gpmlElement) {
133 const { CenterY } = gpmlElement.Graphics;
134 return CenterY - Height(gpmlElement) / 2;
135}
136export function Rotation(gpmlElement) {
137 // NOTE: the rotation input field in the PathVisio-Java UI expects degrees,
138 // but GPML expresses rotation in radians. The XSD indicates GPML can also
139 // use directional strings, although I haven't seen one used in actual GPML.
140 // For both the PathVisio-Java UI and GPML, a positive value means clockwise
141 // rotation.
142 // NOTE: GPML can hold a rotation value for State elements in an element
143 // named "Attribute" like this:
144 // Key="org.pathvisio.core.StateRotation"
145 // From discussion with AP and KH, we've decided to ignore this value,
146 // because we don't actually want States to be rotated.
147 const { Graphics } = gpmlElement;
148 const Rotation = !isDefinedCXML(Graphics.Rotation) ? 0 : Graphics.Rotation;
149 // NOTE: Output is in degrees, because that's what the SVG transform
150 // attribute accepts. Don't get confused, because we use radians in
151 // the edge processing.
152 //
153 // NOTE: to make it as simple as possible for users to work with pvjson,
154 // we're normalizing these rotation values so they are always positive values
155 // between 0 and 2 * Math.PI, e.g.,
156 // (3/2) * Math.PI, not -1 * Math.PI/2 or (7/3) * Math.PI
157 return radiansToDegrees(normalize(GPML_ROTATION_SIDE_TO_RAD.hasOwnProperty(Rotation)
158 ? GPML_ROTATION_SIDE_TO_RAD[Rotation]
159 : parseAsNonNaNNumber(Rotation)));
160}
161export function LineStyle(gpmlElement) {
162 const { LineStyle } = gpmlElement.Graphics;
163 // TODO hard-coding this here is not the most maintainable
164 if (LineStyle === "Solid") {
165 // this gets converted to strokeDasharray,
166 // and we don't need this value when it's
167 // solid, so we return undefined, because
168 // then this won't be included.
169 return;
170 }
171 else if (LineStyle === "Broken") {
172 return "5,3";
173 }
174 else {
175 throw new Error(`Unrecognized LineStyle: ${LineStyle}`);
176 }
177}
178export const Author = flow(get("Author"), decodeIfNotEmpty);
179export const DataSource = flow(get("Data-Source"), decodeIfNotEmpty);
180export const Email = flow(get("Email"), decodeIfNotEmpty);
181export const Maintainer = flow(get("Maintainer"), decodeIfNotEmpty);
182export const Name = flow(get("Name"), decodeIfNotEmpty);
183export const TextLabel = flow(get("TextLabel"), decodeIfNotEmpty);
184// TODO is this ever used?
185// The only way I see to create underlined text in PathVisio-Java
186// is to create a Label and fill in the Link field.
187// But the resulting GPML does not have a FontDecoration attribute.
188export function getTextDecorationFromGPMLElement(gpmlElement) {
189 const { FontDecoration, FontStrikethru } = gpmlElement.Graphics;
190 let outputChunks = [];
191 const fontDecorationDefined = isDefinedCXML(FontDecoration) && FontDecoration === "Underline";
192 const fontStrikethruDefined = isDefinedCXML(FontStrikethru) && FontStrikethru === "Strikethru";
193 if (fontDecorationDefined || fontStrikethruDefined) {
194 if (fontDecorationDefined) {
195 outputChunks.push("underline");
196 }
197 if (fontStrikethruDefined) {
198 outputChunks.push("line-through");
199 }
200 }
201 else {
202 outputChunks.push("none");
203 }
204 return outputChunks.join(" ");
205}
206export const Align = flow(get("Graphics.Align"), kebabCase);
207export const FontDecoration = getTextDecorationFromGPMLElement;
208export const FontStrikethru = getTextDecorationFromGPMLElement;
209export const FontStyle = flow(get("Graphics.FontStyle"), kebabCase);
210export const FontWeight = flow(get("Graphics.FontWeight"), kebabCase);
211export const Valign = flow(get("Graphics.Valign"), kebabCase);
212export const Href = flow(get("Href"), decodeIfNotEmpty, encodeURI);
213export function gpmlColorToCssColor(colorValue) {
214 const colorValueLowerCased = colorValue.toLowerCase();
215 if (["transparent", "none"].indexOf(colorValueLowerCased) > -1) {
216 return colorValueLowerCased;
217 }
218 else {
219 let color = new RGBColor(colorValue);
220 if (!color.ok) {
221 throw new VError(`
222 Failed to get a valid CSS color for gpmlColorToCssColor(${colorValue})
223 Is there an invalid Color or FillColor in the GPML?
224 `);
225 // TODO should we use this?
226 // return "#c0c0c0";
227 }
228 return color.toHex();
229 }
230}
231export const Color = flow(get("Graphics.Color"), gpmlColorToCssColor);
232export function FillColor(gpmlElement) {
233 const { FillColor, ShapeType } = gpmlElement.Graphics;
234 // If it's a GPML Group, DataNode, Shape, Label or State, it needs a
235 // ShapeType in order for it to have a FillColor, but a
236 // GPML Interaction or GraphicalLine can have a FillColor
237 // without having a ShapeType.
238 return (!!ShapeType && ShapeType.toLowerCase() !== "none") ||
239 gpmlElement.Graphics.hasOwnProperty("Point")
240 ? gpmlColorToCssColor(FillColor)
241 : "transparent";
242}
243export function LineThickness(gpmlElement) {
244 const { LineThickness, ShapeType } = gpmlElement.Graphics;
245 // See note near Height converter regarding LineThickness.
246 // If it's a GPML Group, DataNode, Shape, Label or State, it needs a
247 // ShapeType in order for it to have a LineThickness > 0, but a
248 // GPML Interaction or GraphicalLine can have a LineThickness > 0
249 // without having a ShapeType.
250 if (!isDefinedCXML(LineThickness)) {
251 return 0;
252 }
253 else if (isDefinedCXML(ShapeType) && ShapeType.toLowerCase() !== "none") {
254 /*
255 return findIndex(function({ Key, Value }) {
256 return Key === "org.pathvisio.DoubleLineProperty";
257 }, gpmlElement.Attribute) > -1 ? LineThickness * 3 : LineThickness;
258 //*/
259 /*
260 return findIndex(function({ Key, Value }) {
261 return Key === "org.pathvisio.DoubleLineProperty";
262 }, gpmlElement.Attribute) > -1 ? LineThickness : LineThickness * 2;
263 //*/
264 //return LineThickness * 2;
265 return LineThickness;
266 }
267 else if (gpmlElement.Graphics.hasOwnProperty("Point")) {
268 return LineThickness;
269 }
270 else {
271 return 0;
272 }
273}
274export function ConnectorType(gpmlElement) {
275 const { ConnectorType } = gpmlElement.Graphics;
276 return ConnectorType + "Line";
277}
278// We return a partial attachmentDisplay, because it's
279// merged with the other items as we come across them.
280export function Position(gpmlElement) {
281 const { Position } = gpmlElement;
282 return {
283 position: [Position, 0],
284 // a GPML Anchor never has an offset
285 offset: [0, 0]
286 };
287}
288/**
289 * getPositionAndRelativeOffsetScalarsAlongAxis
290 *
291 * @param relValue {number}
292 * @return {OffsetOrientationAndPositionScalarsAlongAxis}
293 */
294function getPositionAndRelativeOffsetScalarsAlongAxis(relValue) {
295 let relativeOffsetScalar;
296 let positionScalar;
297 const relativeToUpperLeftCorner = (relValue + 1) / 2;
298 if (relativeToUpperLeftCorner < 0 || relativeToUpperLeftCorner > 1) {
299 if (relativeToUpperLeftCorner < 0) {
300 positionScalar = 0;
301 relativeOffsetScalar = relativeToUpperLeftCorner;
302 }
303 else {
304 positionScalar = 1;
305 relativeOffsetScalar = relativeToUpperLeftCorner - 1;
306 }
307 }
308 else {
309 positionScalar = relativeToUpperLeftCorner;
310 relativeOffsetScalar = 0;
311 }
312 if (!isFinite(positionScalar) || !isFinite(relativeOffsetScalar)) {
313 throw new Error(`Expected finite values for positionScalar ${positionScalar} and relativeOffsetScalar ${relativeOffsetScalar}`);
314 }
315 return { relativeOffsetScalar, positionScalar };
316}
317// We actually handle both RelX and RelY together
318// when we hit RelX and then ignoring when we
319// hit RelY.
320// We return a partial attachmentDisplay, because it's
321// merged with the other items as we come across them.
322export function RelX(gpmlElement) {
323 // first is for a State (?), second is for a Point
324 const RelXRelYContainer = isDefinedCXML(gpmlElement.Graphics)
325 ? gpmlElement.Graphics
326 : gpmlElement;
327 const { RelX, RelY } = RelXRelYContainer;
328 const { relativeOffsetScalar: relativeOffsetScalarX, positionScalar: positionScalarX } = getPositionAndRelativeOffsetScalarsAlongAxis(RelX);
329 const { relativeOffsetScalar: relativeOffsetScalarY, positionScalar: positionScalarY } = getPositionAndRelativeOffsetScalarsAlongAxis(RelY);
330 return {
331 position: [positionScalarX, positionScalarY],
332 // we can't calculate absolute offset until we get the
333 // referenced element width/height
334 offset: [],
335 relativeOffset: [relativeOffsetScalarX, relativeOffsetScalarY]
336 };
337}
338//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiVmFsdWVDb252ZXJ0ZXJzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjLzIwMTNhL1ZhbHVlQ29udmVydGVycy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQ0wsS0FBSyxFQUNMLFNBQVMsRUFDVCxJQUFJLEVBQ0osR0FBRyxFQUNILE9BQU8sRUFDUCxLQUFLLEVBQ0wsUUFBUSxFQUNSLFNBQVMsRUFDVCxHQUFHLEVBQ0osTUFBTSxXQUFXLENBQUM7QUFDbkIsTUFBTSxRQUFRLEdBQUcsT0FBTyxDQUFDLFVBQVUsQ0FBQyxDQUFDO0FBQ3JDLE9BQU8sS0FBSyxNQUFNLE1BQU0sUUFBUSxDQUFDO0FBQ2pDLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxJQUFJLENBQUM7QUFFNUIsT0FBTyxFQUNMLHlCQUF5QixFQUcxQixNQUFNLG1CQUFtQixDQUFDO0FBQzNCLE9BQU8sRUFBRSxTQUFTLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUNoRSxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFHbEQsMkNBQTJDO0FBQzNDLHdDQUF3QztBQUN4QyxrQ0FBa0M7QUFDbEMsMENBQTBDO0FBQzFDLDZDQUE2QztBQUM3QyxnQkFBZ0I7QUFDaEIsTUFBTSx5QkFBeUIsR0FBRztJQUNoQyxHQUFHLEVBQUUsQ0FBQztJQUNOLEtBQUssRUFBRSxHQUFHLEdBQUcsSUFBSSxDQUFDLEVBQUU7SUFDcEIsTUFBTSxFQUFFLElBQUksQ0FBQyxFQUFFO0lBQ2YsSUFBSSxFQUFFLENBQUMsR0FBRyxDQUFDLEdBQUcsSUFBSSxDQUFDLEVBQUU7Q0FDdEIsQ0FBQztBQUVGLDBCQUEwQixLQUFLO0lBQzdCLE1BQU0sQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDO0FBQ2hELENBQUM7QUFFRCw2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
\No newline at end of file