mirror of
https://github.com/m3y54m/Embedded-Engineering-Roadmap.git
synced 2026-10-10 08:18:16 +03:00
Static site in site/ (vanilla ES modules, no build step) that reads README.md at runtime and draws the roadmap map. - Map view: Software, Software ∩ Hardware, Hardware and Soft skills areas, importance colors, area/importance filters, connections between topics, pan/zoom, A4 print stylesheet - Outline view, search and a details panel with descriptions and learning resources - site/map.json is the editable source of the map (bands, clusters, importance); layout.js places it on an A4 page with a minimum spacing - Credits with creator logo, CC BY-SA badge and build revision - Sofia Sans Semi Condensed bundled under the OFL - Tests for the README parser, map mapping and layout
145 lines
5.8 KiB
JavaScript
145 lines
5.8 KiB
JavaScript
// Links the topics drawn on the map (map.json) to README topics, so every topic knows
|
|
// which areas (Software, Hardware, Soft skills) it sits in and how important the map marks it.
|
|
|
|
// Map labels whose README heading is worded differently.
|
|
const ALIASES = {
|
|
'ADC / DAC': ['ADC', 'DAC'],
|
|
'Buildroot / Yocto': ['Buildroot', 'Yocto'],
|
|
'TDD & Unit Testing': ['Test Driven Development (TDD)', 'Unit Testing'],
|
|
'Threading / Parallelism': ['Multithreading & Parallel Processing'],
|
|
'Device Drivers': ['Linux Device Drivers'],
|
|
'Real-Time OS': ['Real-Time Operating Systems'],
|
|
'Interfaces & Protocols': ['Interfaces, Protocols & Communication Technologies'],
|
|
Basic: ['Basic Protocols'],
|
|
'High-Speed': ['High-Speed Protocols'],
|
|
Wireless: ['Wireless Protocols'],
|
|
Industrial: ['Industrial Protocols'],
|
|
Automotive: ['Automotive Protocols'],
|
|
Network: ['Network Protocols / Socket Programming'],
|
|
'TCP/IP': ['Network Protocols / Socket Programming'],
|
|
UDP: ['Network Protocols / Socket Programming'],
|
|
Cellular: ['Cellular Communication'],
|
|
MQTT: ['CoAP & MQTT'],
|
|
CoAP: ['CoAP & MQTT'],
|
|
'LTE-M / 5G': ['LTE-M & NB-IoT'],
|
|
'NB-IoT': ['LTE-M & NB-IoT'],
|
|
'Basic Math & Calculus': ['Basic Calculus'],
|
|
'SDLC Models': ['Software Development Life Cycle (SDLC) Models'],
|
|
'Version Control': ['Version Control Systems'],
|
|
AUTOSAR: ['AUTOSAR Architecture'],
|
|
// Drawn on the map but without their own README section: open the protocol family instead.
|
|
Profinet: ['Industrial Protocols'],
|
|
LIN: ['Automotive Protocols'],
|
|
MOST: ['Automotive Protocols'],
|
|
FlexRay: ['Automotive Protocols'],
|
|
UWB: ['Wireless Protocols'],
|
|
};
|
|
|
|
export const AREA_ORDER = ['SOFTWARE', 'HARDWARE', 'SOFT SKILLS'];
|
|
export const IMPORTANCE_LEVELS = ['required', 'recommended', 'possible'];
|
|
const RANK = { required: 3, recommended: 2, possible: 1 };
|
|
|
|
const key = (text) => text.normalize('NFKC').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
|
|
function topicIndex(topics) {
|
|
const index = new Map();
|
|
const add = (k, topic) => {
|
|
if (k && !index.has(k)) index.set(k, topic);
|
|
};
|
|
const byDepth = [...topics].sort((a, b) => a.depth - b.depth);
|
|
for (const t of byDepth) add(key(t.title), t);
|
|
for (const t of byDepth) {
|
|
const base = t.title.replace(/\s*\([^)]*\)\s*/g, ' ').trim();
|
|
add(key(base), t);
|
|
const inner = t.title.match(/\(([^)]+)\)/);
|
|
if (inner && /^[A-Z0-9-]{2,6}$/.test(inner[1])) add(key(inner[1]), t);
|
|
add(key(base.split(' / ')[0]), t);
|
|
}
|
|
return index;
|
|
}
|
|
|
|
export function linkDiagram(map, data) {
|
|
const index = topicIndex(data.topics);
|
|
const softSkills = data.topics.find((t) => t.depth === 1 && key(t.title) === 'soft skills');
|
|
const resolve = (text, areas = []) => {
|
|
if (ALIASES[text]) return ALIASES[text].map((title) => index.get(key(title))).filter(Boolean);
|
|
const topic = index.get(key(text));
|
|
if (topic) return [topic];
|
|
if (softSkills && areas.includes('SOFT SKILLS')) return [softSkills];
|
|
return [];
|
|
};
|
|
|
|
// A cluster's areas are those of the band it sits in; a group's are the areas all its clusters share.
|
|
const clusterAreas = new Map();
|
|
const clusters = {};
|
|
const boxes = [];
|
|
for (const band of map.bands) {
|
|
for (const [cluster, spec] of Object.entries(band.clusters)) {
|
|
clusterAreas.set(cluster, band.areas);
|
|
clusters[cluster] = { header: spec.header, caption: spec.caption, per: spec.per };
|
|
if (spec.header) boxes.push({ text: spec.header, importance: null, areas: band.areas, cluster, header: true });
|
|
for (const [text, importance] of spec.topics) boxes.push({ text, importance, areas: band.areas, cluster, header: false });
|
|
}
|
|
}
|
|
boxes.forEach((b, i) => Object.assign(b, { id: `box-${i}`, topics: resolve(b.text, b.areas) }));
|
|
const regions = Object.entries(map.groups).map(([name, clusters]) => {
|
|
const areas = AREA_ORDER.filter((a) => clusters.every((c) => (clusterAreas.get(c) || []).includes(a)));
|
|
return { name, clusters, areas, topics: resolve(name) };
|
|
});
|
|
|
|
const boxOf = new Map();
|
|
for (const t of data.topics) {
|
|
t.areas = [];
|
|
t.importance = null;
|
|
t.boxes = [];
|
|
}
|
|
for (const box of boxes) {
|
|
for (const t of box.topics) {
|
|
t.boxes.push(box);
|
|
if (!boxOf.has(t)) boxOf.set(t, box);
|
|
for (const area of box.areas) if (!t.areas.includes(area)) t.areas.push(area);
|
|
if (box.importance && (!t.importance || RANK[box.importance] > RANK[t.importance])) t.importance = box.importance;
|
|
}
|
|
}
|
|
for (const region of regions) {
|
|
for (const t of region.topics) {
|
|
for (const area of region.areas) if (!t.areas.includes(area)) t.areas.push(area);
|
|
}
|
|
}
|
|
// Subtopics that are not drawn in the diagram inherit the areas of their closest drawn ancestor.
|
|
for (const t of data.topics) {
|
|
if (!t.areas.length) for (let p = t.parent; p && !t.areas.length; p = p.parent) t.areas = [...(p.areas || [])];
|
|
t.areas.sort((a, b) => AREA_ORDER.indexOf(a) - AREA_ORDER.indexOf(b));
|
|
}
|
|
|
|
// The box that stands for a topic on the map: its own, or the closest ancestor's.
|
|
const boxFor = (topic) => {
|
|
for (let t = topic; t; t = t.parent) if (boxOf.has(t)) return boxOf.get(t);
|
|
return null;
|
|
};
|
|
|
|
const pairs = new Map();
|
|
for (const link of data.links) {
|
|
const a = boxFor(link.source);
|
|
const b = boxFor(link.target);
|
|
if (!a || !b || a === b) continue;
|
|
const [p, q] = a.id < b.id ? [a, b] : [b, a];
|
|
const id = `${p.id}|${q.id}`;
|
|
if (!pairs.has(id)) pairs.set(id, { a: p, b: q, weight: 0, shared: 0, links: [] });
|
|
const pair = pairs.get(id);
|
|
pair.weight += link.weight;
|
|
pair.shared += link.shared.length;
|
|
pair.links.push(link);
|
|
}
|
|
|
|
return {
|
|
regions,
|
|
clusters,
|
|
boxes,
|
|
boxFor,
|
|
connections: [...pairs.values()],
|
|
unmatched: boxes.filter((b) => !b.header && !b.topics.length).map((b) => b.text),
|
|
invalid: boxes.filter((b) => !b.header && !IMPORTANCE_LEVELS.includes(b.importance)).map((b) => `${b.text}: ${b.importance}`),
|
|
};
|
|
}
|