elcheapogary / satisplanory

A production calculator for Satisfactory
Eclipse Public License 2.0
1 stars 1 forks source link

Build progress, planning sections or comments can't be added / tables and graphs can't be exported #6

Closed meolus closed 2 years ago

meolus commented 2 years ago

Currently there is no possibility to track build progress, plan sections of a factory by grouping them or add comments in Satisplanory. To don't build a complete spread sheet or diagramming tool into Satisplanory, I guess easiest would be to make export or copy to clipboard of the tables and graphs available.

When you ever build a self-sufficient nuclear power plant or some end game factories, you know what I am talking about ;)

Suggestion: 1.) Tables shown in Satisplanory should been able to be copy and pasted to Excel/Calc or exported as CSV or similar.

2.) Graphs shown in Satisplanory should been able to be exported to some graph tools.

2a) My personal favorite target tool so far would be yEd (https://www.yworks.com/products/yed). In the yEd's specific extended graphml format you can easily define different shapes and colors of nodes and edges, which especially is interesting for pipes since you may consider some slopes for them. Further unique advantage of yEd is, that it can group nodes (which could been collapsed/expanded), it has some powerful layout algorithms included (to arrange (grouped) nodes and also the edges), has some views to show predecessors and successors as overview, and of course has all the usual diagramming functions like coloring, add/remove/adapt nodes to highlight planned sections or mark finished things.

2b) Export to some neutral format for graphs like JSON, like it is available on https://factoriolab.github.io/satisfactory below the generated "Flows". I don't know a graph tool being directly able to use this, but it enables others to convert it to some suitable graph format being used in their graph or diagramming tool.

If you're interested I may can give you some Java code about ODS table and graphml file generation, since I also started creating some factory planning tool about a year ago. But it was never finished and especially lacks some own calculation engine and has no UI. For ODS tables I was using https://github.com/jferard/fastods and graphml files are generated by some plain text/xml template writer script.

elcheapogary commented 2 years ago

I do plan to add the ability to make comments per node in the graph view.

I also plan (and this may take a while) to be able to split parts of the graph into separate plans (like take part of the graph and isolate it to a separate factory), adding the output of the isolated part as input in it's place.

I agree about data being exportable - makes sense. For tables I would prefer clipboard and CSV.

For graphs, I briefly looked at the graphml language. I was not aware of it, and I think it is perfect for this.

I will try get these export options implemented.

elcheapogary commented 2 years ago

Tables exporting to CSV added in a6524303d99d209b76598d2771a0f5dee633a66c

Will be released in version 1.6.0

elcheapogary commented 2 years ago

Graph exporting to image and GraphML added in 1b5b61739cb3c36023e8cd95e7dbcdf8880ba369.

Will be released in version 1.6.0.

The GraphML format does not open very nicely in yed - there are no labels. However, all the data is present in the GraphML export, and could be used in a script to generate whatever custom GraphML or other format.

The GraphML exported format is more "standard graphml" than "graphml that looks pretty in yed". I am open to adding a more yed specific graphml export option. If you'd be interested in that, please have a look at a graphml export as currently implemented, and see what changes would be ideal, and post an example with current graphml export and what it would look like with desired changes.

meolus commented 2 years ago

Hi @elcheapogary, sorry for not responding so long, but I hoped you would release 1.6.0 some when, so I wouldn't need to read it from code and could simply try it. Additionally my tool is currently in a broken state so I can't generate new diagrams at the moment. Furthermore uploading some older diagrams on GitHub unfortunately fails with "Something went really wrong, and we can’t process that file. Styling with Markdown is supported", so I postponed the answer.

Now I reminisced about the topic. Hope you can take my tool's class creating the yEd graphml files as answer (see below).

Basically I just draw some diagrams with yEd and then adapted my tool to generate the particular "yEd" graphml.

Hints/remarks:

`package com.meolus.common.production;

import com.meolus.common.gamedata.GameItem; import com.meolus.common.gamedata.GameRecipe; import com.meolus.common.util.MathHelper;

import java.awt.*; import java.io.BufferedWriter; import java.io.FileWriter; import java.util.HashMap; import java.util.Map;

class ProductionGraphExportToGraphMl<R extends GameRecipe<I, R>, I extends GameItem<R, I>> { private final ProductionGraph<R, I> _graph; private final Map<IProductionVertex, String> _vertexId = new HashMap<>();

protected ProductionGraphExportToGraphMl(ProductionGraph<R, I> graph) {
    if (graph == null) {
        throw new IllegalArgumentException("Missing graph.");
    }
    _graph = graph;

    initVertexIdMap();
}

/**
 * Create unique node ids.
 */
private void initVertexIdMap() {
    int index = 0;
    for (final var vertex : _graph.vertexSet()) {
        _vertexId.put(vertex, index++ + "_" + vertex.getId());
    }
}

protected void saveAsGraphMl(String filename) {
    final var graphMlString = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
            "<graphml xmlns=\"http://graphml.graphdrawing.org/xmlns\" xmlns:java=\"http://www.yworks.com/xml/yfiles-common/1.0/java\" xmlns:sys=\"http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0\" xmlns:x=\"http://www.yworks.com/xml/yfiles-common/markup/2.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:y=\"http://www.yworks.com/xml/graphml\" xmlns:yed=\"http://www.yworks.com/xml/yed/3\" xsi:schemaLocation=\"http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd\">\n" +
            "  <key attr.name=\"Beschreibung\" attr.type=\"string\" for=\"graph\" id=\"d0\"/>\n" +
            "  <key for=\"port\" id=\"d1\" yfiles.type=\"portgraphics\"/>\n" +
            "  <key for=\"port\" id=\"d2\" yfiles.type=\"portgeometry\"/>\n" +
            "  <key for=\"port\" id=\"d3\" yfiles.type=\"portuserdata\"/>\n" +
            "  <key attr.name=\"url\" attr.type=\"string\" for=\"node\" id=\"d4\"/>\n" +
            "  <key attr.name=\"description\" attr.type=\"string\" for=\"node\" id=\"d5\"/>\n" +
            "  <key for=\"node\" id=\"d6\" yfiles.type=\"nodegraphics\"/>\n" +
            "  <key for=\"graphml\" id=\"d7\" yfiles.type=\"resources\"/>\n" +
            "  <key attr.name=\"url\" attr.type=\"string\" for=\"edge\" id=\"d8\"/>\n" +
            "  <key attr.name=\"description\" attr.type=\"string\" for=\"edge\" id=\"d9\"/>\n" +
            "  <key for=\"edge\" id=\"d10\" yfiles.type=\"edgegraphics\"/>\n" +
            "  <graph edgedefault=\"directed\" id=\"G\">\n" +
            "    <data key=\"d0\"/>\n" +
            getNodesToGraphMlFragment() +
            getEdgesToGraphMlFragment() +
            "  </graph>\n" +
            "  <data key=\"d7\">\n" +
            "    <y:Resources/>\n" +
            "  </data>\n" +
            "</graphml>\n";

    // Save as graphml
    try {
        final var writer = new BufferedWriter(new FileWriter(filename));
        writer.write(graphMlString);

        writer.close();
    } catch(Exception e) {
        throw new IllegalStateException("Writing graphml file '" + filename + "' failed.", e);
    }
}

private String getNodesToGraphMlFragment() {
    final var result = new StringBuilder();

    for (final var vertex : _graph.vertexSet()) {
        final String shapeType;
        final Color color;
        final double width;
        final double height;

        if (vertex instanceof ProductionItemAmount) {
            shapeType = "ellipse";
            color = Color.CYAN;
            width = 240.0;
            height = 48.0;
        } else if (vertex instanceof ProductionRecipeAmount) {
            shapeType = "rectangle";
            color = Color.ORANGE;
            width = 240.0;
            height = 48.0;
        } else if (vertex instanceof ProductionIntersectionVertex) {
            shapeType = "rectangle";
            color = Color.WHITE;
            width = 32.0;
            height = 32.0;
        } else {
            throw new IllegalStateException("Unexpected vertex type: " + vertex.getClass().getSimpleName());
        }

        final var nodeId = _vertexId.get(vertex);
        result.append("    <node id=\"").append(nodeId).append("\">\n")
                .append("      <data key=\"d5\"/>\n")
                .append("      <data key=\"d6\">\n")
                .append("        <y:ShapeNode>\n")
                .append("          <y:Geometry height=\"").append(height).append("\" width=\"").append(width).append("\" x=\"0.0\" y=\"0.0\"/>\n")
                .append("          <y:Fill color=\"#").append(Integer.toHexString(color.getRGB()).substring(2)).append("\" transparent=\"false\"/>\n")
                .append("          <y:BorderStyle color=\"#000000\" raised=\"false\" type=\"line\" width=\"1.0\"/>\n")
                .append("          <y:NodeLabel alignment=\"center\" autoSizePolicy=\"content\" fontFamily=\"Dialog\" fontSize=\"12\" fontStyle=\"plain\" hasBackgroundColor=\"false\" hasLineColor=\"false\" height=\"0.0\" horizontalTextPosition=\"center\" iconTextGap=\"4\" modelName=\"custom\" textColor=\"#000000\" verticalTextPosition=\"bottom\" visible=\"true\" width=\"0.0\" x=\"6.6630859375\" y=\"5.6494140625\">").append(getNodeLabelText(vertex)).append("<y:LabelModel>\n")
                .append("              <y:SmartNodeLabelModel distance=\"4.0\"/>\n")
                .append("            </y:LabelModel>\n")
                .append("            <y:ModelParameter>\n")
                .append("              <y:SmartNodeLabelModelParameter labelRatioX=\"0.0\" labelRatioY=\"0.0\" nodeRatioX=\"0.0\" nodeRatioY=\"0.0\" offsetX=\"0.0\" offsetY=\"0.0\" upX=\"0.0\" upY=\"-1.0\"/>\n")
                .append("            </y:ModelParameter>\n")
                .append("          </y:NodeLabel>\n")
                .append("          <y:Shape type=\"").append(shapeType).append("\"/>\n")
                .append("        </y:ShapeNode>\n")
                .append("      </data>\n")
                .append("    </node>\n");
    }

    return result.toString();
}

private String getNodeLabelText(IProductionVertex vertex) {
    final String text;
    if (vertex instanceof final ProductionItemAmount itemAmount) {
        text = itemAmount.getName() + "\n(" + MathHelper.roundTo2Digits(itemAmount.getAmount()) + " / min)";
    } else if (vertex instanceof final ProductionRecipeAmount recipeAmount) {
        text = recipeAmount.getName() + "\n(" + MathHelper.roundTo2Digits(recipeAmount.getAmount()) + "x " + recipeAmount.getRecipe().getPreferredManufacturedIn().getName() + " @" + recipeAmount.getWorkloadInPercentRoundedUp() + "%)";
    } else if (vertex instanceof final ProductionIntersectionVertex intersectionVertex) {
        text = intersectionVertex.getName();
    } else {
        throw new IllegalStateException("Unexpected type of vertex: " + vertex.getClass().getTypeName());
    }

    return escapeText(text);
}

private String getEdgesToGraphMlFragment() {
    final var result = new StringBuilder();

    for (final var edge : _graph.edgeSet()) {
        final var source = _graph.getEdgeSource(edge);
        final var target = _graph.getEdgeTarget(edge);

        final var sourceNodeId = _vertexId.get(source);
        final var targetNodeId = _vertexId.get(target);
        result.append("    <edge id=\"").append(sourceNodeId).append(targetNodeId).append("\" source=\"").append(sourceNodeId).append("\" target=\"").append(targetNodeId).append("\">\n")
                .append("      <data key=\"d9\"/>\n")
                .append("      <data key=\"d10\">\n")
                .append(getStyledEdge(edge))
                .append("      </data>\n")
                .append("    </edge>\n");
    }

    return result.toString();
}

private String getStyledEdge(IProductionEdge<I, R> edge) {
    final var item = edge.getItem();
    if (!item.isConstruction() && (item.isLiquid() || item.isGas())) {
        final var colorString = "#" + String.format("%06x", 0xFFFFFF & item.getColor().getRGB());

        return
                "        <y:GenericEdge configuration=\"com.yworks.edge.framed\">\n" +
                "          <y:Path sx=\"0.0\" sy=\"0.0\" tx=\"0.0\" ty=\"0.0\"/>\n" +
                "          <y:LineStyle color=\"#000000\" type=\"line\" width=\"5.0\"/>\n" +
                "          <y:Arrows source=\"none\" target=\"standard\"/>\n" +
                getEdgeLabel(edge) +
                "          <y:StyleProperties>\n" +
                "            <y:Property class=\"java.awt.Color\" name=\"FramedEdgePainter.fillColor\" value=\"" + colorString + "\"/>\n" +
                "          </y:StyleProperties>\n" +
                "        </y:GenericEdge>\n";
    } else {
        return
                "        <y:PolyLineEdge>\n" +
                "          <y:Path sx=\"0.0\" sy=\"0.0\" tx=\"0.0\" ty=\"0.0\"/>\n" +
                "          <y:LineStyle color=\"#000000\" type=\"line\" width=\"1.0\"/>\n" +
                "          <y:Arrows source=\"none\" target=\"standard\"/>\n" +
                getEdgeLabel(edge) +
                "          <y:BendStyle smoothed=\"false\"/>\n" +
                "        </y:PolyLineEdge>\n";
    }
}

private String getEdgeLabel(IProductionEdge<I, R> edge) {
    return
            "          <y:EdgeLabel alignment=\"center\" configuration=\"AutoFlippingLabel\" distance=\"2.0\" fontFamily=\"Dialog\" fontSize=\"12\" fontStyle=\"plain\" hasBackgroundColor=\"false\" hasLineColor=\"false\" height=\"18.701171875\" horizontalTextPosition=\"center\" iconTextGap=\"4\" modelName=\"custom\" preferredPlacement=\"anywhere\" ratio=\"0.5\" textColor=\"#000000\" verticalTextPosition=\"bottom\" visible=\"true\" width=\"29.353515625\" x=\"-95.50797076767427\" y=\"14.846472323293739\">" + getEdgeLabelText(edge) + "<y:LabelModel>\n" +
            "              <y:SmartEdgeLabelModel autoRotationEnabled=\"false\" defaultAngle=\"0.0\" defaultDistance=\"10.0\"/>\n" +
            "            </y:LabelModel>\n" +
            "            <y:ModelParameter>\n" +
            "              <y:SmartEdgeLabelModelParameter angle=\"0.0\" distance=\"30.0\" distanceToCenter=\"true\" position=\"right\" ratio=\"0.5\" segment=\"0\"/>\n" +
            "            </y:ModelParameter>\n" +
            "            <y:PreferredPlacementDescriptor angle=\"0.0\" angleOffsetOnRightSide=\"0\" angleReference=\"absolute\" angleRotationOnRightSide=\"co\" distance=\"-1.0\" frozen=\"true\" placement=\"anywhere\" side=\"anywhere\" sideReference=\"relative_to_edge_flow\"/>\n" +
            "          </y:EdgeLabel>\n";
}

private String getEdgeLabelText(IProductionEdge<I, R> edge) {
    final var text = edge.getItem().getName() + "\n" + MathHelper.roundTo2Digits(edge.getAmount()) + " / min";
    return escapeText(text);
}

private String escapeText(String inputText) {
    return inputText
            .replace("<", "&lt;")
            .replace(">", "&gt;");
}

} `

meolus commented 2 years ago

Thanks for adding the export of tables to CSV and of graphs to graphml and pngs in v1.6.0. Looks very useful, even if I need to create some scripts first to use it in yEd or similar.

On checking it, I made some "experiences":

  1. I got a lot of "Unnamed Factory" Production Plans, until I found that I can rename it via double click on opened tabs. First I was searching inside the Configuration tab below Unnamed Factory and in the list of Production Plans. So that could be a little bit more intuitive ;)

  2. In the Graph tab. I don't found a way to deselect nodes, so I can get a nice png export without having anything highlighted (without recalculating it). Would be nice if clicking on background could deselect all nodes or so.

  3. In the Graph tab, the Export image (as png) works good, but has some unexpected behaviors:

Furthermore I am really looking forward to see further extensions of Satisplanory in the future, like you mentioned in previous comments:

Maybe you are also interested in graph formats, I mentioned in previous comments, like:

I will close this issue, since original issue was solved. I see Satisplanory is still unter development, so I think it does not makes sense to keep this issues open until all ideas from the comments were totally implemented ;)