|
@@ -53,11 +53,13 @@ async function render(shareUUID: string, cx: string) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function _render(shareUUID: string, cx: string) {
|
|
async function _render(shareUUID: string, cx: string) {
|
|
|
- const [plan, buildings, recipeList, exchanges, graph]: [Plan, Building[], Recipe[], Exchange[], Graph] = await Promise.all([
|
|
|
|
|
|
|
+ const [plan, buildings, recipeList, exchanges, materialList, graph]:
|
|
|
|
|
+ [Plan, Building[], Recipe[], Exchange[], Material[], Graph] = await Promise.all([
|
|
|
cachedFetchJSON('https://api.prunplanner.org/planning/shared/' + shareUUID),
|
|
cachedFetchJSON('https://api.prunplanner.org/planning/shared/' + shareUUID),
|
|
|
cachedFetchJSON('https://api.prunplanner.org/data/buildings/'),
|
|
cachedFetchJSON('https://api.prunplanner.org/data/buildings/'),
|
|
|
cachedFetchJSON('https://api.prunplanner.org/data/recipes/'),
|
|
cachedFetchJSON('https://api.prunplanner.org/data/recipes/'),
|
|
|
cachedFetchJSON('https://api.prunplanner.org/data/exchanges/'),
|
|
cachedFetchJSON('https://api.prunplanner.org/data/exchanges/'),
|
|
|
|
|
+ cachedFetchJSON('https://api.prunplanner.org/data/materials/'),
|
|
|
cachedFetchJSON('/graph_data.json'),
|
|
cachedFetchJSON('/graph_data.json'),
|
|
|
]);
|
|
]);
|
|
|
const planet: Planet = await cachedFetchJSON(`https://api.prunplanner.org/data/planet/${plan.plan_details.planet_natural_id}/`);
|
|
const planet: Planet = await cachedFetchJSON(`https://api.prunplanner.org/data/planet/${plan.plan_details.planet_natural_id}/`);
|
|
@@ -67,8 +69,9 @@ async function _render(shareUUID: string, cx: string) {
|
|
|
const recipes = new Map<string, Recipe>(recipeList.map(r => [r.recipe_id, r]));
|
|
const recipes = new Map<string, Recipe>(recipeList.map(r => [r.recipe_id, r]));
|
|
|
const dailyTraded = new Map<string, number>(exchanges.filter((ex) => ex.exchange_code === cx)
|
|
const dailyTraded = new Map<string, number>(exchanges.filter((ex) => ex.exchange_code === cx)
|
|
|
.map((ex) => [ex.ticker, ex.sum_traded_7d / 7]));
|
|
.map((ex) => [ex.ticker, ex.sum_traded_7d / 7]));
|
|
|
|
|
+ const materials = new Map<string, Material>(materialList.map(m => [m.ticker, m]));
|
|
|
|
|
|
|
|
- travel(graph.edges, planet.system_id, cx);
|
|
|
|
|
|
|
+ const oneWayHours = travel(graph.edges, planet.system_id, cx);
|
|
|
|
|
|
|
|
const planInput = new Counter();
|
|
const planInput = new Counter();
|
|
|
const planOutput = new Counter();
|
|
const planOutput = new Counter();
|
|
@@ -108,14 +111,33 @@ async function _render(shareUUID: string, cx: string) {
|
|
|
<td style="color: ${color}">${pctFmt.format(netAmount / traded)}</td>
|
|
<td style="color: ${color}">${pctFmt.format(netAmount / traded)}</td>
|
|
|
</tr>`;
|
|
</tr>`;
|
|
|
}).join('')}
|
|
}).join('')}
|
|
|
- </table>
|
|
|
|
|
- `;
|
|
|
|
|
|
|
+ </table>`;
|
|
|
|
|
+
|
|
|
|
|
+ if (oneWayHours !== null) {
|
|
|
|
|
+ const {shippingBottleneckType, shippingBottleneckAmount} = shippingBottleneck(materials, net);
|
|
|
|
|
+ const roundTripHours = oneWayHours * 2;
|
|
|
|
|
+ const scbThroughput = 24 / roundTripHours * 500;
|
|
|
|
|
+ renderTarget.innerHTML += `
|
|
|
|
|
+ <p>
|
|
|
|
|
+ shipping bottleneck: ${formatNumber(shippingBottleneckAmount)} (${shippingBottleneckType})
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p>
|
|
|
|
|
+ one-way to/from ${cx}: ${formatNumber(oneWayHours)} hours
|
|
|
|
|
+ <br>round-trip to ${cx}: ${formatNumber(roundTripHours)} hours
|
|
|
|
|
+ <br>non-stop SCB shipping throughput per day: 24 ÷ ${formatNumber(roundTripHours)} × 500 = ${formatNumber(scbThroughput)}
|
|
|
|
|
+ <br>non-stop SCBs needed: ${formatNumber(shippingBottleneckAmount)} ÷ ${formatNumber(scbThroughput)}
|
|
|
|
|
+ = ${formatNumber(shippingBottleneckAmount / scbThroughput)}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function travel(edges: Edge[], system: string, cx: string): number {
|
|
|
|
|
|
|
+function travel(edges: Edge[], system: string, cx: string): number | null {
|
|
|
const {parsecs, jumps} = ftlDistance(edges, system, cx);
|
|
const {parsecs, jumps} = ftlDistance(edges, system, cx);
|
|
|
|
|
+ if (jumps === 0)
|
|
|
|
|
+ return null;
|
|
|
// SCB JMPs at 4pc/hr. RCT CHRGs in 7m30s. 30m DEP, 1.5hr APP+(TO/LAND)
|
|
// SCB JMPs at 4pc/hr. RCT CHRGs in 7m30s. 30m DEP, 1.5hr APP+(TO/LAND)
|
|
|
- return parsecs / 4 + (jumps - 1) * 0.125 + 2;
|
|
|
|
|
|
|
+ return Math.round(parsecs / 4 + (jumps - 1) * 0.125 + 2);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** dijkstra to find the shortest FTL route from system to CX */
|
|
/** dijkstra to find the shortest FTL route from system to CX */
|
|
@@ -205,6 +227,30 @@ function calcBuilding(recipes: Map<string, Recipe>, building: PlanBuilding, cogc
|
|
|
return {input, output};
|
|
return {input, output};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function shippingBottleneck(materials: Map<string, Material>, net: Counter):
|
|
|
|
|
+ {shippingBottleneckType: string, shippingBottleneckAmount: number} {
|
|
|
|
|
+ let importWeight = 0, importVol = 0;
|
|
|
|
|
+ let exportWeight = 0, exportVol = 0;
|
|
|
|
|
+ for (const [mat, amount] of net.entries()) {
|
|
|
|
|
+ const material = materials.get(mat)!;
|
|
|
|
|
+ if (amount < 0) {
|
|
|
|
|
+ importWeight -= amount * material.weight;
|
|
|
|
|
+ importVol -= amount * material.volume;
|
|
|
|
|
+ } else if (amount > 0) {
|
|
|
|
|
+ exportWeight += amount * material.weight;
|
|
|
|
|
+ exportVol += amount * material.volume;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const bottlenecks = [
|
|
|
|
|
+ {shippingBottleneckType: 'import weight', shippingBottleneckAmount: importWeight},
|
|
|
|
|
+ {shippingBottleneckType: 'import volume', shippingBottleneckAmount: importVol},
|
|
|
|
|
+ {shippingBottleneckType: 'export weight', shippingBottleneckAmount: exportWeight},
|
|
|
|
|
+ {shippingBottleneckType: 'export volume', shippingBottleneckAmount: exportVol},
|
|
|
|
|
+ ];
|
|
|
|
|
+ bottlenecks.sort((a, b) => b.shippingBottleneckAmount - a.shippingBottleneckAmount);
|
|
|
|
|
+ return bottlenecks[0];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const numberFmt = new Intl.NumberFormat(undefined, {maximumFractionDigits: 2});
|
|
const numberFmt = new Intl.NumberFormat(undefined, {maximumFractionDigits: 2});
|
|
|
const wholeFmt = new Intl.NumberFormat(undefined, {maximumFractionDigits: 0});
|
|
const wholeFmt = new Intl.NumberFormat(undefined, {maximumFractionDigits: 0});
|
|
|
const pctFmt = new Intl.NumberFormat(undefined, {style: 'percent', maximumFractionDigits: 2});
|
|
const pctFmt = new Intl.NumberFormat(undefined, {style: 'percent', maximumFractionDigits: 2});
|
|
@@ -260,6 +306,12 @@ interface Exchange {
|
|
|
sum_traded_7d: number // avg_traded_7d is nonsense
|
|
sum_traded_7d: number // avg_traded_7d is nonsense
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+interface Material {
|
|
|
|
|
+ ticker: string
|
|
|
|
|
+ weight: number
|
|
|
|
|
+ volume: number
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
interface Graph {
|
|
interface Graph {
|
|
|
edges: Array<Edge>
|
|
edges: Array<Edge>
|
|
|
}
|
|
}
|