1 | import { curry, findIndex, flow, get, isEmpty, isNaN, isString, kebabCase, map } from "lodash/fp";
|
2 | const RGBColor = require("rgbcolor");
|
3 | import * as VError from "verror";
|
4 | import { decode } from "he";
|
5 | import { generatePublicationXrefId } from "../gpml-utilities";
|
6 | import { normalize, radiansToDegrees } from "../spinoffs/Angle";
|
7 | import { 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?
|
14 | const 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 | };
|
20 | function decodeIfNotEmpty(input) {
|
21 | return isEmpty(input) ? input : decode(input);
|
22 | }
|
23 | function 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
|
38 | export 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".
|
51 | export const $ID = flow(get("$ID"), generatePublicationXrefId);
|
52 | export const DB = flow(get("DB.content"), decodeIfNotEmpty);
|
53 | export const TITLE = flow(get("TITLE.content"), decodeIfNotEmpty);
|
54 | export const SOURCE = flow(get("SOURCE.content"), decodeIfNotEmpty);
|
55 | export const YEAR = get("YEAR.content");
|
56 | export const AUTHORS = flow(get("AUTHORS"), map(flow(get("content"), decodeIfNotEmpty)));
|
57 | export const BiopaxRef = flow(get("BiopaxRef"), map(generatePublicationXrefId));
|
58 | /*
|
59 | Meanings of Width
|
60 | -----------------
|
61 |
|
62 | In PathVisio-Java, GPML Width/Height for GPML Shapes is
|
63 | inconsistent when zoomed in vs. when at default zoom level.
|
64 |
|
65 | When zoomed in, GPML Width/Height refers to the distance from center of stroke (border)
|
66 | one one edge to center of stroke (border) on the opposite edge, meaning that shapes that
|
67 | run up to the edge are cropped.
|
68 |
|
69 | When at default zoom level, GPML Width/Height refers to the distance from outer edge of
|
70 | stroke (border) to outer edge of stroke (border) with no cropping.
|
71 |
|
72 | Because of this, LineThickness is also inconsistent.
|
73 | When zoomed in: approx. one half of specified LineThickness.
|
74 | When at default zoom level: approx. full specified LineThickness.
|
75 |
|
76 | For double lines, LineThickness refers to the the stroke (border) width of each line and
|
77 | the space between each line, meaning the stroke (border) width
|
78 | for the double line as a whole will be three times the listed LineThickness.
|
79 |
|
80 | For pvjs, we define GPML Width/Height to be from outer edge of stroke (border) on one
|
81 | side to outer edge of stroke (border) on the opposite site, meaning visible width/height
|
82 | may not exactly match between pvjs and PathVisio.
|
83 | See 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 | //*/
|
115 | const 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 | });
|
126 | export const Height = getDimension("Height");
|
127 | export const Width = getDimension("Width");
|
128 | export function CenterX(gpmlElement) {
|
129 | const { CenterX } = gpmlElement.Graphics;
|
130 | return CenterX - Width(gpmlElement) / 2;
|
131 | }
|
132 | export function CenterY(gpmlElement) {
|
133 | const { CenterY } = gpmlElement.Graphics;
|
134 | return CenterY - Height(gpmlElement) / 2;
|
135 | }
|
136 | export 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 | }
|
161 | export 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 | }
|
178 | export const Author = flow(get("Author"), decodeIfNotEmpty);
|
179 | export const DataSource = flow(get("Data-Source"), decodeIfNotEmpty);
|
180 | export const Email = flow(get("Email"), decodeIfNotEmpty);
|
181 | export const Maintainer = flow(get("Maintainer"), decodeIfNotEmpty);
|
182 | export const Name = flow(get("Name"), decodeIfNotEmpty);
|
183 | export 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.
|
188 | export 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 | }
|
206 | export const Align = flow(get("Graphics.Align"), kebabCase);
|
207 | export const FontDecoration = getTextDecorationFromGPMLElement;
|
208 | export const FontStrikethru = getTextDecorationFromGPMLElement;
|
209 | export const FontStyle = flow(get("Graphics.FontStyle"), kebabCase);
|
210 | export const FontWeight = flow(get("Graphics.FontWeight"), kebabCase);
|
211 | export const Valign = flow(get("Graphics.Valign"), kebabCase);
|
212 | export const Href = flow(get("Href"), decodeIfNotEmpty, encodeURI);
|
213 | export 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 | }
|
231 | export const Color = flow(get("Graphics.Color"), gpmlColorToCssColor);
|
232 | export 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 | }
|
243 | export 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 | }
|
274 | export 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.
|
280 | export 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 | */
|
294 | function 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.
|
322 | export 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 |