UNPKG

gpml2pvjson

Version:
343 lines (336 loc) 25.7 kB
import { curry, findIndex, flow, get, isEmpty, isNaN, isString, kebabCase, map } from "lodash/fp"; const RGBColor = require("rgbcolor"); import * as VError from "verror"; import { decode } from "he"; import { generatePublicationXrefId } from "../gpml-utilities"; import { normalize, radiansToDegrees } from "../spinoffs/Angle"; import { isDefinedCXML } from "../gpml-utilities"; // TODO are these ever used? PathVisio-Java // does not accept them as inputs in the // Rotation input field in the UI. // TODO if they are used, are the notes in // the XSD correct, or would "Right" actually // be 0 radians? const GPML_ROTATION_SIDE_TO_RAD = { Top: 0, Right: 0.5 * Math.PI, Bottom: Math.PI, Left: (3 / 2) * Math.PI }; function decodeIfNotEmpty(input) { return isEmpty(input) ? input : decode(input); } function parseAsNonNaNNumber(i) { const parsed = Number(i); if (isNaN(parsed)) { throw new Error('Cannot parse "' + String(i) + '" as non-NaN number'); } return parsed; } //***************** // Value Converters //***************** // NOTE: we use He.decode for many of these // because at some point some GPML files were // processed w/out using UTF-8, leaving some // strings garbled, such as author names. // TODO backpageHead could be further processed to yield displayName and standardName export function ID(gpmlElement) { if (gpmlElement.hasOwnProperty("ID")) { const { ID } = gpmlElement; return isString(ID) ? ID : ID.content; } else { return gpmlElement.Xref.ID; } } // GPML2013-ish incorrectly used "rdf:id" where it was intented // to use "rdf:ID". We corrected that error before processing, // but CXML turns "rdf:ID" into "ID", and since we already have // a property "ID" on the element, CXML uses "$ID". export const $ID = flow(get("$ID"), generatePublicationXrefId); export const DB = flow(get("DB.content"), decodeIfNotEmpty); export const TITLE = flow(get("TITLE.content"), decodeIfNotEmpty); export const SOURCE = flow(get("SOURCE.content"), decodeIfNotEmpty); // TODO: why doesn't TypeScript like the following? //export const YEAR = get("YEAR.content"); export const YEAR = function (x) { return x.YEAR.content; //return get("YEAR.content"); }; export const AUTHORS = flow(get("AUTHORS"), map(flow(get("content"), decodeIfNotEmpty))); export const BiopaxRef = flow(get("BiopaxRef"), map(generatePublicationXrefId)); /* Meanings of Width ----------------- In PathVisio-Java, GPML Width/Height for GPML Shapes is inconsistent when zoomed in vs. when at default zoom level. When zoomed in, GPML Width/Height refers to the distance from center of stroke (border) one one edge to center of stroke (border) on the opposite edge, meaning that shapes that run up to the edge are cropped. When at default zoom level, GPML Width/Height refers to the distance from outer edge of stroke (border) to outer edge of stroke (border) with no cropping. Because of this, LineThickness is also inconsistent. When zoomed in: approx. one half of specified LineThickness. When at default zoom level: approx. full specified LineThickness. For double lines, LineThickness refers to the the stroke (border) width of each line and the space between each line, meaning the stroke (border) width for the double line as a whole will be three times the listed LineThickness. For pvjs, we define GPML Width/Height to be from outer edge of stroke (border) on one side to outer edge of stroke (border) on the opposite site, meaning visible width/height may not exactly match between pvjs and PathVisio. See issue https://github.com/PathVisio/pathvisio/issues/59 * DOM box model - box-sizing: border-box visible width = width (width means border + padding + width of the content) (see https://css-tricks.com/international-box-sizing-awareness-day/) - box-sizing: content-box visible width = width + border + padding (width means width of the content) * PathVisio-Java - Zoomed in - LineStyle NOT Double visible width ≈ GPMLWidth visible height ≈ GPMLHeight (matches box-sizing: border-box) - LineStyle Double visible width ≈ Width + 1.5 * LineThickness visible height ≈ Height + 1.5 * LineThickness - Zoomed out - LineStyle NOT Double visible width ≈ GPMLWidth + LineThickness visible height ≈ GPMLHeight + LineThickness (matches box-sizing: border-box) (one half LineThickness on either side yields a full LineThickness to add to width/height). - LineStyle Double visible width = Width + 3 * LineThickness visible height = Height + 3 * LineThickness * SVG: visible width = width + stroke-width * kaavio/pvjs: same as DOM box model with box-sizing: border-box //*/ const getDimension = curry(function (dimensionName, gpmlElement) { const dimension = gpmlElement.Graphics[dimensionName]; if (findIndex(function ({ Key, Value }) { return Key === "org.pathvisio.DoubleLineProperty"; }, gpmlElement.Attribute) > -1) { return dimension + LineThickness(gpmlElement); } else { return dimension; } }); export const Height = getDimension("Height"); export const Width = getDimension("Width"); export function CenterX(gpmlElement) { const { CenterX } = gpmlElement.Graphics; return CenterX - Width(gpmlElement) / 2; } export function CenterY(gpmlElement) { const { CenterY } = gpmlElement.Graphics; return CenterY - Height(gpmlElement) / 2; } export function Rotation(gpmlElement) { // NOTE: the rotation input field in the PathVisio-Java UI expects degrees, // but GPML expresses rotation in radians. The XSD indicates GPML can also // use directional strings, although I haven't seen one used in actual GPML. // For both the PathVisio-Java UI and GPML, a positive value means clockwise // rotation. // NOTE: GPML can hold a rotation value for State elements in an element // named "Attribute" like this: // Key="org.pathvisio.core.StateRotation" // From discussion with AP and KH, we've decided to ignore this value, // because we don't actually want States to be rotated. const { Graphics } = gpmlElement; const Rotation = !isDefinedCXML(Graphics.Rotation) ? 0 : Graphics.Rotation; // NOTE: Output is in degrees, because that's what the SVG transform // attribute accepts. Don't get confused, because we use radians in // the edge processing. // // NOTE: to make it as simple as possible for users to work with pvjson, // we're normalizing these rotation values so they are always positive values // between 0 and 2 * Math.PI, e.g., // (3/2) * Math.PI, not -1 * Math.PI/2 or (7/3) * Math.PI return radiansToDegrees(normalize(GPML_ROTATION_SIDE_TO_RAD.hasOwnProperty(Rotation) ? GPML_ROTATION_SIDE_TO_RAD[Rotation] : parseAsNonNaNNumber(Rotation))); } export function LineStyle(gpmlElement) { const { LineStyle } = gpmlElement.Graphics; // TODO hard-coding this here is not the most maintainable if (LineStyle === "Solid") { // this gets converted to strokeDasharray, // and we don't need this value when it's // solid, so we return undefined, because // then this won't be included. return; } else if (LineStyle === "Broken") { return "5,3"; } else { throw new Error(`Unrecognized LineStyle: ${LineStyle}`); } } export const Author = flow(get("Author"), decodeIfNotEmpty); export const DataSource = flow(get("Data-Source"), decodeIfNotEmpty); export const Email = flow(get("Email"), decodeIfNotEmpty); export const Maintainer = flow(get("Maintainer"), decodeIfNotEmpty); export const Name = flow(get("Name"), decodeIfNotEmpty); export const TextLabel = flow(get("TextLabel"), decodeIfNotEmpty); // TODO is this ever used? // The only way I see to create underlined text in PathVisio-Java // is to create a Label and fill in the Link field. // But the resulting GPML does not have a FontDecoration attribute. export function getTextDecorationFromGPMLElement(gpmlElement) { const { FontDecoration, FontStrikethru } = gpmlElement.Graphics; let outputChunks = []; const fontDecorationDefined = isDefinedCXML(FontDecoration) && FontDecoration === "Underline"; const fontStrikethruDefined = isDefinedCXML(FontStrikethru) && FontStrikethru === "Strikethru"; if (fontDecorationDefined || fontStrikethruDefined) { if (fontDecorationDefined) { outputChunks.push("underline"); } if (fontStrikethruDefined) { outputChunks.push("line-through"); } } else { outputChunks.push("none"); } return outputChunks.join(" "); } export const Align = flow(get("Graphics.Align"), kebabCase); export const FontDecoration = getTextDecorationFromGPMLElement; export const FontStrikethru = getTextDecorationFromGPMLElement; export const FontStyle = flow(get("Graphics.FontStyle"), kebabCase); export const FontWeight = flow(get("Graphics.FontWeight"), kebabCase); export const Valign = flow(get("Graphics.Valign"), kebabCase); export const Href = flow(get("Href"), decodeIfNotEmpty, encodeURI); export function gpmlColorToCssColor(colorValue) { const colorValueLowerCased = colorValue.toLowerCase(); if (["transparent", "none"].indexOf(colorValueLowerCased) > -1) { return colorValueLowerCased; } else { let color = new RGBColor(colorValue); if (!color.ok) { throw new VError(` Failed to get a valid CSS color for gpmlColorToCssColor(${colorValue}) Is there an invalid Color or FillColor in the GPML? `); // TODO should we use this? // return "#c0c0c0"; } return color.toHex(); } } export const Color = flow(get("Graphics.Color"), gpmlColorToCssColor); export function FillColor(gpmlElement) { const { FillColor, ShapeType } = gpmlElement.Graphics; // If it's a GPML Group, DataNode, Shape, Label or State, it needs a // ShapeType in order for it to have a FillColor, but a // GPML Interaction or GraphicalLine can have a FillColor // without having a ShapeType. return (!!ShapeType && ShapeType.toLowerCase() !== "none") || gpmlElement.Graphics.hasOwnProperty("Point") ? gpmlColorToCssColor(FillColor) : "transparent"; } export function LineThickness(gpmlElement) { const { LineThickness, ShapeType } = gpmlElement.Graphics; // See note near Height converter regarding LineThickness. // If it's a GPML Group, DataNode, Shape, Label or State, it needs a // ShapeType in order for it to have a LineThickness > 0, but a // GPML Interaction or GraphicalLine can have a LineThickness > 0 // without having a ShapeType. if (!isDefinedCXML(LineThickness)) { return 0; } else if (isDefinedCXML(ShapeType) && ShapeType.toLowerCase() !== "none") { /* return findIndex(function({ Key, Value }) { return Key === "org.pathvisio.DoubleLineProperty"; }, gpmlElement.Attribute) > -1 ? LineThickness * 3 : LineThickness; //*/ /* return findIndex(function({ Key, Value }) { return Key === "org.pathvisio.DoubleLineProperty"; }, gpmlElement.Attribute) > -1 ? LineThickness : LineThickness * 2; //*/ //return LineThickness * 2; return LineThickness; } else if (gpmlElement.Graphics.hasOwnProperty("Point")) { return LineThickness; } else { return 0; } } export function ConnectorType(gpmlElement) { const { ConnectorType } = gpmlElement.Graphics; return ConnectorType + "Line"; } // We return a partial attachmentDisplay, because it's // merged with the other items as we come across them. export function Position(gpmlElement) { const { Position } = gpmlElement; return { position: [Position, 0], // a GPML Anchor never has an offset offset: [0, 0] }; } /** * getPositionAndRelativeOffsetScalarsAlongAxis * * @param relValue {number} * @return {OffsetOrientationAndPositionScalarsAlongAxis} */ function getPositionAndRelativeOffsetScalarsAlongAxis(relValue) { let relativeOffsetScalar; let positionScalar; const relativeToUpperLeftCorner = (relValue + 1) / 2; if (relativeToUpperLeftCorner < 0 || relativeToUpperLeftCorner > 1) { if (relativeToUpperLeftCorner < 0) { positionScalar = 0; relativeOffsetScalar = relativeToUpperLeftCorner; } else { positionScalar = 1; relativeOffsetScalar = relativeToUpperLeftCorner - 1; } } else { positionScalar = relativeToUpperLeftCorner; relativeOffsetScalar = 0; } if (!isFinite(positionScalar) || !isFinite(relativeOffsetScalar)) { throw new Error(`Expected finite values for positionScalar ${positionScalar} and relativeOffsetScalar ${relativeOffsetScalar}`); } return { relativeOffsetScalar, positionScalar }; } // We actually handle both RelX and RelY together // when we hit RelX and then ignoring when we // hit RelY. // We return a partial attachmentDisplay, because it's // merged with the other items as we come across them. export function RelX(gpmlElement) { // first is for a State (?), second is for a Point const RelXRelYContainer = isDefinedCXML(gpmlElement.Graphics) ? gpmlElement.Graphics : gpmlElement; const { RelX, RelY } = RelXRelYContainer; const { relativeOffsetScalar: relativeOffsetScalarX, positionScalar: positionScalarX } = getPositionAndRelativeOffsetScalarsAlongAxis(RelX); const { relativeOffsetScalar: relativeOffsetScalarY, positionScalar: positionScalarY } = getPositionAndRelativeOffsetScalarsAlongAxis(RelY); return { position: [positionScalarX, positionScalarY], // we can't calculate absolute offset until we get the // referenced element width/height offset: [], relativeOffset: [relativeOffsetScalarX, relativeOffsetScalarY] }; } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiVmFsdWVDb252ZXJ0ZXJzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjLzIwMTNhL1ZhbHVlQ29udmVydGVycy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQ0wsS0FBSyxFQUNMLFNBQVMsRUFDVCxJQUFJLEVBQ0osR0FBRyxFQUNILE9BQU8sRUFDUCxLQUFLLEVBQ0wsUUFBUSxFQUNSLFNBQVMsRUFDVCxHQUFHLEVBQ0osTUFBTSxXQUFXLENBQUM7QUFDbkIsTUFBTSxRQUFRLEdBQUcsT0FBTyxDQUFDLFVBQVUsQ0FBQyxDQUFDO0FBQ3JDLE9BQU8sS0FBSyxNQUFNLE1BQU0sUUFBUSxDQUFDO0FBQ2pDLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxJQUFJLENBQUM7QUFFNUIsT0FBTyxFQUNMLHlCQUF5QixFQUcxQixNQUFNLG1CQUFtQixDQUFDO0FBQzNCLE9BQU8sRUFBRSxTQUFTLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUNoRSxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFHbEQsMkNBQTJDO0FBQzNDLHdDQUF3QztBQUN4QyxrQ0FBa0M7QUFDbEMsMENBQTBDO0FBQzFDLDZDQUE2QztBQUM3QyxnQkFBZ0I7QUFDaEIsTUFBTSx5QkFBeUIsR0FBRztJQUNoQyxHQUFHLEVBQUUsQ0FBQztJQUNOLEtBQUssRUFBRSxHQUFHLEdBQUcsSUFBSSxDQUFDLEVBQUU7SUFDcEIsTUFBTSxFQUFFLElBQUksQ0FBQyxFQUFFO0lBQ2YsSUFBSSxFQUFFLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxHQUFHLElBQUksQ0FBQyxFQUFFO0NBQ3hCLENBQUM7QUFFRixTQUFTLGdCQUFnQixDQUFDLEtBQUs7SUFDN0IsT0FBTyxPQUFPLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDO0FB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