on a397efd help sign in
astrolabe/ui/src/lib/graph.ts
375 lines 12 kB history raw
1import Graph from 'graphology';
2import * as colors from './colors';
4const example = '[0[3216291:t;9219219;4608:s[156546;1612196;219621:~;321965149:4608;]1280[4289594624;4278191360;426672256;4260168960;4260824320;4246668544;4246144256;4253746432;4235396352;4230415616;4225697024;4227007744;4219667712;4222616832;4224648448;4214490368;4215997696;4219143424;4237296896;]]]'
6export interface ChartPoint {
7 patp: string,
8 dominion: ('l1' | 'l2' | 'spawn'),
9 sponsor?: string,
10 parent?: string,
11 children?: ChartPoint[],
12 sponsees?: ChartPoint[],
15function parseMetaString(metaString: string): ChartPoint {
16 let idString, subMetaString = '';
17 const colIndex = metaString.indexOf(':');
18 if (colIndex === -1) {
19 idString = metaString;
20 } else {
21 idString = metaString.slice(0, colIndex);
22 subMetaString = metaString.slice(colIndex + 1);
23 }
24 const meta: ChartPoint = {
25 patp: '~' + idString,
26 dominion: 'l1',
27 }
28 if (subMetaString) {
29 const firstChar = subMetaString[0];
30 if (firstChar === 't') {
31 meta.dominion = 'l2';
32 subMetaString = subMetaString.slice(1);
33 } else if (firstChar === 's') {
34 meta.dominion = 'spawn';
35 subMetaString = subMetaString.slice(1);
36 }
37 if (subMetaString) {
38 if (subMetaString[0] === '~') {
39 meta.sponsor = null;
40 } else {
41 let [sponsor, parent] = subMetaString.slice(1).split('!');
42 meta.sponsor = '~' + sponsor;
43 meta.parent = '~' + parent;
44 }
45 }
46 }
47 return meta;
50function extractMeta(data: string): [ChartPoint, string] {
51 const index = data.search(/[[;]/);
52 if (index === -1) {
53 throw new Error(`No point meta present in ${data}`);
54 }
55 const metaString = data.slice(0, index);
56 data = data.slice(index);
57 return [parseMetaString(metaString), data];
60function extractChildren(data: string, sup: string = undefined): [ChartPoint[], ChartPoint[], string] {
61 if (data[0] === ';') return [[], [], data.slice(1)];
62 if (data[0] !== '[') throw new Error('Invalid children in data: ' + data.slice(0, 20));
64 let endFound = false;
65 let counter = 0;
66 data = data.slice(1);
67 const children = [];
68 const sponsees = [];
69 while (!endFound && data.length && counter < 20000) {
70 if (data[0] === ']') {
71 endFound = true;
72 data = data.slice(1);
73 } else {
74 let sub;
75 [sub, data] = extractChartPoint(data);
76 if (!sub.parent || sub.parent === sup)
77 children.push(sub);
78 if (sub.sponsor === undefined || sub.sponsor === sup)
79 sponsees.push(sub);
80 }
81 counter++;
82 }
83 return [children, sponsees, data];
86function extractChartPoint(data: string): [ChartPoint, string] {
87 let point, children, sponsees;
88 [point, data] = extractMeta(data);
89 [children, sponsees, data] = extractChildren(data, point.patp);
90 point.children = children;
91 point.sponsees = sponsees;
92 return [point, data];
95function linkChartData(points: ChartPoint[], store: any): ChartPoint[] {
96 if (!points) return points;
97 return points.map(point => {
98 if (!store[point.patp]) return point;
99 if (!point.children?.length) {
100 point.children = linkChartData(store[point.patp].children, store);
101 }
102 if (!point.sponsees?.length) {
103 point.sponsees = linkChartData(store[point.patp].sponsees, store);
104 }
105 return point;
106 });
109function _parseChartData(str: string): ChartPoint[] {
110 if (!str) return [];
111 let [baseChildren, _] = extractChildren(str);
112 let store = {};
113 baseChildren.forEach(child => {
114 store[child.patp] = {
115 children: child.children,
116 sponsees: child.sponsees,
117 };
118 });
120 return linkChartData(store['~base'].children, store);
123export function parseChartData(data: string): ChartPoint[] {
124 let start = Date.now();
125 let parsed = _parseChartData(data);
126 // console.log(`finished parsing chart data to js object in ${Date.now() - start} ms`);
127 return parsed;
130const circle = 2 * Math.PI;
131function chordLength(radius, radians) {
132 return 2 * radius * Math.sin(radians / 2);
134function distancePolar(r1, th1, r2, th2) {
135 return Math.sqrt((r1 * r1) + (r2 * r2) - (2 * r1 * r2 * Math.cos(th2 - th1)));
137export function arrangePointsInSpirals(
138 numPoints,
139 {
140 maxDistance, minDistance,
141 startX = 0, startY = 0,
142 maxArms = 6, maxPerArm = 45,
143 makeSingleArmCircle = false,
144 }): { coords: any[], numArms: number, minDistanceBetween: number } {
145 const numSmallEnoughArms = Math.ceil(numPoints / maxPerArm);
146 let eRootNumArms = Math.max(1, Math.floor(Math.pow(numPoints, 1 / Math.E)));
147 const numArms = Math.max(numSmallEnoughArms, eRootNumArms);
148 const singleArmCircle = makeSingleArmCircle && numArms === 1;
149 const numPerArm = numPoints / numArms;
150 let armThetaRange = numPerArm * Math.min(Math.PI / 12, 0.25 * circle / numArms);
151 if (singleArmCircle) {
152 minDistance = maxDistance;
153 armThetaRange = circle;
154 }
155 const distanceRange = maxDistance - minDistance;
156 const pointWeight = 1 / numPerArm;
157 let minDistanceBetween = distancePolar(minDistance, 0,
158 minDistance + distanceRange * pointWeight, armThetaRange * pointWeight);
159 if (numPoints <= 3) minDistanceBetween = maxDistance * 0.75;
160 const coords = [];
161 let lastArm = null;
162 let indexInArm = null;
163 for (let i = 0; i < numPoints; i++) {
164 const overallProgress = i / numPoints;
165 const arm = Math.floor(numArms * overallProgress);
166 if (lastArm === arm) {
167 indexInArm++;
168 } else {
169 indexInArm = 0;
170 }
171 const interArmProgress = arm / numArms;
172 const beginningOfNextArm = Math.floor(numPerArm * (arm + 1));
173 const numInThisArm = beginningOfNextArm - (i - indexInArm);
174 const intraArmProgress = indexInArm / numInThisArm;
175 const theta = circle * interArmProgress + armThetaRange * intraArmProgress;
176 let line = (minDistance + distanceRange * intraArmProgress);
177 let x = 0 + startX + line * Math.cos(theta);
178 let y = 0 + startY + line * Math.sin(theta);
179 coords.push({
180 coords: [x, y],
181 arm: singleArmCircle ? i : arm,
182 });
183 lastArm = arm;
184 };
185 return {
186 coords,
187 numArms,
188 minDistanceBetween,
189 };
192export function populateGraph(g: Graph, chartData: ChartPoint[], bySponsor: boolean, callback?): Graph {
193 if (!(chartData instanceof Array)) return;
194 if (!g) {
195 g = new Graph();
196 } else {
197 g.clear();
198 }
200 let subsKey = bySponsor ? 'sponsees' : 'children';
201 let sponsorEdges = [];
203 let offsetX = 0;
204 let offsetY = 0;
206 let a = 120000000;
207 let b = 9000000;
208 let theta = 0;
210 chartData.forEach((galaxy, gi) => {
211 offsetX = 0 + (a + b * theta) * Math.cos(theta);
212 offsetY = 0 + (a + b * theta) * Math.sin(theta);
214 theta = 0.15 * (gi+1) * (1 - .15 * (gi / 256));
215 const gPatp = galaxy.patp;
216 const itsYou = gPatp === '~' + window.ship;
217 g.addNode(gPatp, {
218 label: gPatp,
219 size: 3,
220 color: itsYou ? colors.YOU : colors.WHITE,
221 x: offsetX,
222 y: offsetY,
223 zIndex: 10,
224 });
226 const stars = galaxy[subsKey] || [];
227 const numStars = stars.length;
228 const maxStarDistance = 60000 * 128;
229 const minStarDistance = maxStarDistance / 4;
230 const starSize = 1.2 / Math.pow(numStars, 1 / 7);
231 const {
232 coords: starCoords,
233 numArms: numStarArms,
234 minDistanceBetween: minDistanceBetweenStars,
235 } = arrangePointsInSpirals(numStars,
236 {
237 maxDistance: maxStarDistance,
238 minDistance: minStarDistance,
239 startX: offsetX,
240 startY: offsetY,
241 makeSingleArmCircle: true,
242 });
243 let lastStarArm = null;
244 let lastStarPatp = null;
245 stars.forEach((star, si) => {
246 const sPatp = star.patp;
248 const [starX, starY] = starCoords[si].coords;
249 const arm = starCoords[si].arm;
251 const itsYou = sPatp === '~' + window.ship;
252 if (itsYou) {
253 g.setNodeAttribute(gPatp, 'color', colors.YOUR_GAL);
254 }
255 try {
256 g.addNode(sPatp, {
257 label: sPatp,
258 size: starSize,
259 color: itsYou ? colors.YOU : colors.GOLDT2,
260 x: starX,
261 y: starY,
262 zIndex: 8,
263 });
264 if (lastStarArm == arm) {
265 g.addEdge(sPatp, lastStarPatp, {
266 color: colors.LGREY,
267 size: 0.1,
268 zIndex: 7,
269 });
270 } else {
271 g.addEdge(sPatp, gPatp, {
272 color: colors.LGREY,
273 size: 0.2,
274 zIndex: 9,
275 });
276 }
277 lastStarArm = arm;
278 lastStarPatp = sPatp;
279 if (star.sponsor) {
280 sponsorEdges.push([sPatp, star.sponsor, {
281 color: colors.GREEN,
282 size: 0.2,
283 zIndex: 11,
284 }]);
285 }
287 const planets = star[subsKey] || [];
288 const numPlanets = planets.length;
289 // const maxPlanetDistance = starSize * 1.6 * 4000 * 64;
290 const maxPlanetDistance = minDistanceBetweenStars * 0.45;
291 const minPlanetDistance = maxPlanetDistance / 4;
292 const {
293 coords: planetCoords,
294 numArms: numPlanetArms,
295 minDistanceBetween: minDistanceBetweenPlanets,
296 } = arrangePointsInSpirals(numPlanets,
297 {
298 maxDistance: maxPlanetDistance,
299 minDistance: minPlanetDistance,
300 startX: starX,
301 startY: starY,
302 maxArms: 30,
303 makeSingleArmCircle: true,
304 });
305 let lastPlanetArm = null;
306 let lastPlanetPatp = null;
307 planets.forEach((planet, pi) => {
308 const pPatp = planet.patp;
309 const [planetX, planetY] = planetCoords[pi].coords;
310 const arm = planetCoords[pi].arm;
311 const planetSize = Math.sqrt(Math.min(minPlanetDistance, minDistanceBetweenPlanets)) / 1300;
312 const planetEdgeSize = planetSize / 3;
313 const itsYou = pPatp === '~' + window.ship;
314 if (itsYou) {
315 g.setNodeAttribute(sPatp, 'color', colors.YOUR_STAR);
316 g.setNodeAttribute(gPatp, 'color', colors.YOUR_GAL);
317 }
318 try {
319 g.addNode(pPatp, {
320 label: pPatp,
321 size: planetSize,
322 color: itsYou ? colors.YOU : (planet.dominion == 'l2' ? colors.GREEN : colors.BLUE),
323 x: planetX,
324 y: planetY,
325 zIndex: 6,
326 });
327 if (lastPlanetArm == arm) {
328 g.addEdge(pPatp, lastPlanetPatp, {
329 color: colors.DGREY,
330 size: planetEdgeSize,
331 zIndex: 5,
332 });
333 } else {
334 if (numPlanetArms == 1) {
335 g.addEdge(pPatp, sPatp, {
336 color: colors.DGREY,
337 size: planetEdgeSize,
338 zIndex: 5,
339 });
340 }
341 }
342 if (planet.sponsor) {
343 sponsorEdges.push([pPatp, planet.sponsor, {
344 color: colors.GREEN,
345 size: planetEdgeSize,
346 zIndex: 11,
347 }]);
348 }
349 lastPlanetArm = arm;
350 lastPlanetPatp = pPatp;
351 } catch (e) {
352 let msg = `Couldn't add ${pPatp} to the graph`;
353 console.error(msg, e);
354 alertMe(msg + '\n' + e.message);
355 }
356 });
357 } catch (e) {
358 let msg = `Couldn't add ${sPatp} to the graph`;
359 console.error(msg, e);
360 alertMe(msg + '\n' + e.message);
361 }
362 });
363 });
364 sponsorEdges.forEach(([point, sponsor, attrs]) => {
365 // g.addEdge(point, sponsor, attrs);
366 });
367 if (callback) {
368 setTimeout(callback, 0);
369 }
370 return g;
373function alertMe(msg: string) {
374 alert('Encountered an error, please tell ~midlev-mindyr: ' + msg);