479 lines
16 KiB
HTML
479 lines
16 KiB
HTML
<!-- SPDX-License-Identifier: Beerware -->
|
|
<!-- Wolfgang Kroener wrote this file. As long as you retain this notice -->
|
|
<!-- you can do whatever you want with this stuff. If we meet some day, -->
|
|
<!-- and you think this stuff is worth it, you can buy me a beer in return -->
|
|
|
|
<!doctype html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>band plan</title>
|
|
<style>
|
|
body>div {
|
|
padding-bottom: 1em;
|
|
}
|
|
div#togglebuttons>button {
|
|
margin: 0 0.1em;
|
|
}
|
|
div>label {
|
|
padding-right: 1em;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<link href="https://unpkg.com/tabulator-tables/dist/css/tabulator.min.css" rel="stylesheet">
|
|
<script src="https://unpkg.com/browser-cjs/require.min.js"></script>
|
|
<script src="https://unpkg.com/tabulator-tables/dist/js/tabulator.min.js"></script>
|
|
<script src="https://unpkg.com/js-quantities/build/quantities.js"></script>
|
|
<!--
|
|
for pdf saving
|
|
<script src="https://unpkg.com/jspdf/dist/jspdf.umd.min.js"></script>
|
|
<script src="https://unpkg.com/dompurify/dist/purify.min.js"></script>
|
|
<script src="https://unpkg.com/html2canvas-pro/dist/html2canvas-pro.min.js"></script>
|
|
-->
|
|
<script>
|
|
// require in browser-cjs
|
|
// eslint-disable-next-line no-undef
|
|
const yaml = require("https://unpkg.com/js-yaml/dist/js-yaml.min.js");
|
|
// eslint-disable-next-line no-undef
|
|
const path = require("https://unpkg.com/path-browserify/index.js");
|
|
|
|
function create_element_link(element, href) {
|
|
element.addEventListener("click", () => {
|
|
let lnk = document.createElement("a");
|
|
|
|
if (typeof href === "function") {
|
|
lnk.href = href();
|
|
} else {
|
|
lnk.href = href;
|
|
}
|
|
|
|
if (document.createEvent) {
|
|
let e = document.createEvent("MouseEvents");
|
|
e.initMouseEvent("click", true, true, window,
|
|
0, 0, 0, 0, 0, false, false, false,
|
|
false, 0, null);
|
|
lnk.dispatchEvent(e);
|
|
} else if (lnk.fireEvent) {
|
|
lnk.fireEvent("onclick");
|
|
}
|
|
});
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
location.search.substr(1).split("&").forEach((param) => {
|
|
let p = param.split("=");
|
|
let key = p[0];
|
|
["base", "regulation", "css"].forEach((id) => {
|
|
if (key === id) {
|
|
document.getElementById(id + "_input").value = decodeURIComponent(p[1]);
|
|
}
|
|
});
|
|
});
|
|
update_inputs();
|
|
|
|
["base", "regulation", "css"].forEach((id) => {
|
|
// download buttons
|
|
create_element_link(document.getElementById("download_" + id), document.getElementById(id + "_input").value);
|
|
// enter on input fields
|
|
document.getElementById(id + "_input").addEventListener("keydown", (event) => {
|
|
if (event.key === "Enter") {
|
|
update_inputs()
|
|
}
|
|
});
|
|
});
|
|
|
|
create_element_link(document.getElementById("create_link"), function() {
|
|
return window.location.href.split("?")[0] + "?"
|
|
+ "base=" + document.getElementById("base_input").value
|
|
+ "®ulation=" + document.getElementById("regulation_input").value
|
|
+ "&css=" + document.getElementById("css_input").value;
|
|
});
|
|
});
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
function filter_with_not(headerValue, rowValue, rowData, filterParams){
|
|
if (headerValue.startsWith("!")) {
|
|
// filter not
|
|
const str = headerValue.slice(1);
|
|
if ((str.length > 0) && (typeof rowValue !== "undefined")) {
|
|
return !rowValue.toString().includes(str);
|
|
} else {
|
|
return true;
|
|
}
|
|
} else {
|
|
// filter normal
|
|
if (typeof rowValue !== "undefined") {
|
|
return rowValue.toString().includes(headerValue);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
function display_error(e) {
|
|
let t = document.getElementById("data-table");
|
|
t.innerHTML = "";
|
|
t.removeAttribute("class");
|
|
console.log(e);
|
|
document.getElementById("error").innerText = e;
|
|
}
|
|
|
|
function isInRange(number, min, max) {
|
|
return Number(number) >= Number(min) && Number(number) <= Number(max);
|
|
}
|
|
|
|
function add_column(row, cols, header, minimum_fraction_digits = 0) {
|
|
const formatter = new Intl.NumberFormat("en-US", {
|
|
minimumFractionDigits: minimum_fraction_digits,
|
|
maximumFractionDigits: 100,
|
|
useGrouping: false,
|
|
});
|
|
header.columns.forEach((col) => {
|
|
if (header.columns.includes(col)) {
|
|
if (typeof cols[col] === "object") {
|
|
for (const [key, value] of Object.entries(cols[col])) {
|
|
if ((key === "frequency") && (typeof value === "number")) {
|
|
row[col + "_" + key] = formatter.format(value);
|
|
} else {
|
|
row[col + "_" + key] = value;
|
|
}
|
|
}
|
|
} else {
|
|
if ((col === "frequency") && (typeof cols[col] === "number")) {
|
|
row[col] = formatter.format(cols[col]);
|
|
} else {
|
|
row[col] = cols[col];
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function fill_comments(header) {
|
|
for (const [key, value] of Object.entries(header)) {
|
|
if (typeof value.comments !== "undefined") {
|
|
let s = "";
|
|
value.comments.forEach((comment) => {
|
|
for (const [ckey, cvalue] of Object.entries(comment)) {
|
|
s += ckey + ": " + cvalue + "<br>";
|
|
}
|
|
});
|
|
document.getElementById("comments_" + key).innerHTML = s;
|
|
} else {
|
|
document.getElementById("comments_" + key).innerHTML = "";
|
|
}
|
|
};
|
|
}
|
|
|
|
function update_table(bases, regulations) {
|
|
let tdata = [];
|
|
let header = {};
|
|
|
|
// first, fill base_header
|
|
bases.forEach((base) => {
|
|
if ((typeof base.header !== "undefined") && base.header) {
|
|
header["base"] = structuredClone(base);
|
|
}
|
|
});
|
|
|
|
// then fill tdata
|
|
bases.forEach((base) => {
|
|
if ((typeof base.header === "undefined") || (base.header == false)) {
|
|
let row = {};
|
|
|
|
// first, fill reg_header
|
|
regulations.forEach((reg) => {
|
|
if ((typeof reg.header !== "undefined") && reg.header) {
|
|
header["regulation"] = structuredClone(reg);
|
|
}
|
|
});
|
|
|
|
fill_comments(header);
|
|
|
|
// then fill tdata
|
|
regulations.forEach((reg) => {
|
|
if ((typeof reg.header === "undefined") || (reg.header == false)) {
|
|
add_column(row, base, header["base"], header["base"].frequency_fraction_digits);
|
|
|
|
let [reg_start, reg_end] = reg.frequency.split("-");
|
|
reg_start = Qty(Number(reg_start), header["regulation"].frequency_unit).to(header["base"].frequency_unit).scalar;
|
|
reg_end = Qty(Number(reg_end), header["regulation"].frequency_unit).to(header["base"].frequency_unit).scalar;
|
|
if (typeof base.frequency === "number") {
|
|
// at single frequency
|
|
if (isInRange(base.frequency, reg_start, reg_end)) {
|
|
add_column(row, reg, header["regulation"], header["base"].frequency_fraction_digits);
|
|
tdata.push(row);
|
|
}
|
|
} else {
|
|
// frequeny range
|
|
let [base_start, base_end] = base.frequency.split("-");
|
|
if (isInRange(base_start, reg_start, reg_end) && isInRange(base_end, reg_start, reg_end)) {
|
|
// base range is inside of reg range or the same
|
|
add_column(row, reg, header["regulation"]);
|
|
tdata.push(row);
|
|
} else {
|
|
// base range is split by reg range
|
|
if ((base_end > reg_start) && isInRange(reg_start, base_start, base_end)) {
|
|
// base range starts below reg range
|
|
let r = structuredClone(row);
|
|
let start = reg_start;
|
|
let end = base_end < reg_end ? base_end : reg_end;
|
|
r["frequency"] = start + "-" + end;
|
|
add_column(r, reg, header["regulation"]);
|
|
tdata.push(r);
|
|
} else if ((reg_end > base_start) && isInRange(reg_end, base_start, base_end)) {
|
|
// reg range starts below base range
|
|
let r = structuredClone(row);
|
|
let start = base_start;
|
|
let end = reg_end;
|
|
r["frequency"] = start + "-" + end;
|
|
add_column(r, reg, header["regulation"]);
|
|
tdata.push(r);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// require in tabular-tables
|
|
// eslint-disable-next-line no-undef
|
|
let table = new Tabulator("#data-table", {
|
|
layout: "fitDataTable",
|
|
initialSort: [
|
|
{column: "frequency", dir: "asc"},
|
|
{column: "band", dir: "desc"},
|
|
],
|
|
selectableRows: true,
|
|
selectableRowsRangeMode: "click",
|
|
data: tdata,
|
|
autoColumns: "full",
|
|
downloadEncoder: function(fileContents, mimeType){
|
|
// add header and footer for a complete html file
|
|
let header = "<!doctype html><html><head><meta charset=\"utf-8\"><style>";
|
|
for (let i = 0; i < document.styleSheets.length; i++) {
|
|
let css = document.styleSheets[i];
|
|
if (css.ownerNode.id === "link_css") {
|
|
for (let j = 0; j < css.cssRules.length; j++) {
|
|
header += css.cssRules[j].cssText + "\n";
|
|
}
|
|
}
|
|
}
|
|
header += "</style></head><body>";
|
|
let footer = "</body></html>";
|
|
|
|
// add footer for horizontal line
|
|
let t = document.createElement("div");
|
|
t.insertAdjacentHTML("beforeend", fileContents);
|
|
t.firstChild.insertAdjacentHTML("beforeend", "<tfoot><tr><td colspan=\"100%\"></td></tr></tfoot>");
|
|
fileContents = t.innerHTML;
|
|
|
|
fileContents = fileContents.replaceAll(">undefined<", "><");
|
|
|
|
// also save as pdf
|
|
// currently scaling is wrong, no idea how to fix
|
|
//const doc = new jspdf.jsPDF("p", "mm", "a4");
|
|
//doc.html(header + fileContents + footer, {
|
|
// callback: (doc) => {
|
|
// doc.save();
|
|
// },
|
|
// filename: path.basename(document.getElementById("base_input").value, ".yml") + ".pdf",
|
|
// autoPaging: "text",
|
|
// html2canvas: {
|
|
// scale: 0.5,
|
|
// },
|
|
//});
|
|
return new Blob([header, fileContents, footer], {type:mimeType});
|
|
},
|
|
|
|
// add filter for all columns
|
|
autoColumnsDefinitions: function(definitions){
|
|
definitions.forEach((column) => {
|
|
if (column.field == "band") {
|
|
column.sorter = function(a, b) {
|
|
return Qty(a).sub(Qty(b)).toBase().scalar;
|
|
}
|
|
}
|
|
|
|
if (column.field == "frequency") {
|
|
column.sorter = "number";
|
|
}
|
|
|
|
column.headerFilter = "input";
|
|
column.headerFilterFunc = filter_with_not;
|
|
if (typeof header["base"].titles[column.field] !== "undefined") {
|
|
column.title = header["base"].titles[column.field];
|
|
}
|
|
if (typeof header["regulation"].titles[column.field] !== "undefined") {
|
|
column.title = header["regulation"].titles[column.field];
|
|
}
|
|
|
|
// add css styles for formatting based on cell values
|
|
// e. g. cell-bandwidth-2700
|
|
// list like cw, narrow will be split to two entries
|
|
column.formatter = function(cell) {
|
|
let f = String(cell.getField()).toLowerCase().replace(/[\W]+/g, " ").replaceAll(" ", "_").replace(/__+/g, "_");
|
|
let v = String(cell.getValue()).toLowerCase().replaceAll("+", "_plus_").replace(/[^\w,-]+/g, " ").replaceAll(" ", "_").replace(/__+/g, "_");
|
|
v.split(",").forEach((s) => {
|
|
cell.getElement().classList.add("cell-" + f + "-" + s.replace(/^_+|_+$/g, ""));
|
|
});
|
|
cell.getElement().classList.add("cell-" + f);
|
|
if ((f === "frequency") && (v.indexOf("-") > -1)) {
|
|
cell.getElement().classList.add("cell-" + f + "-" + "is_range");
|
|
}
|
|
return cell.getValue();
|
|
}
|
|
});
|
|
|
|
return definitions;
|
|
},
|
|
// add css styles for formatting based on cell data
|
|
// e. g. row-mode-cw, row-bandwidth-200
|
|
// list like cw, narrow will be split to two entries
|
|
rowFormatter: function(row){
|
|
for (const [key, value] of Object.entries(row.getData())) {
|
|
let k = String(key).toLowerCase().replace(/[\W]+/g, " ").replaceAll(" ", "_").replace(/__+/g, "_");
|
|
let v = String(value).toLowerCase().replaceAll("+", "_plus_").replace(/[^\w,-]+/g, " ").replaceAll(" ", "_").replace(/__+/g, "_");
|
|
v.split(",").forEach((s) => {
|
|
row.getElement().classList.add("row-" + k + "-" + s.replace(/^_+|_+$/g, ""));
|
|
});
|
|
if ((k === "frequency") && (v.indexOf("-") > -1)) {
|
|
row.getElement().classList.add("row-" + k + "-" + "is_range");
|
|
}
|
|
}
|
|
},
|
|
});
|
|
|
|
document.getElementById("download").addEventListener("click", () => {
|
|
let download_range = "active";
|
|
if (table.getSelectedRows().length > 0) {
|
|
download_range = "selected";
|
|
}
|
|
|
|
table.downloadToTab(
|
|
"html",
|
|
path.basename(document.getElementById("base_input").value, ".yml") + ".html",
|
|
{ },
|
|
download_range,
|
|
);
|
|
});
|
|
|
|
table.on("tableBuilt", () => {
|
|
let div_togglebuttons = document.getElementById("togglebuttons");
|
|
div_togglebuttons.innerHTML = "<span>Toggle column:</span>";
|
|
table.getColumns().forEach((col) => {
|
|
let col_name = col.getField();
|
|
let b = document.createElement("button");
|
|
b.innerText = col_name;
|
|
b.addEventListener("click", () => {
|
|
table.toggleColumn(col_name);
|
|
});
|
|
div_togglebuttons.appendChild(b);
|
|
});
|
|
});
|
|
document.getElementById("error").innerText = "";
|
|
}
|
|
|
|
function update_inputs() {
|
|
let urls = [
|
|
fetch(document.getElementById("base_input").value, { mode: "cors" }),
|
|
];
|
|
let reg = document.getElementById("regulation_input").value;
|
|
if (reg !== "") {
|
|
urls.push(fetch(reg, { mode: "cors" }));
|
|
}
|
|
|
|
Promise.all(urls).then((res) => {
|
|
res.forEach((r) => {
|
|
if (!r.ok) {
|
|
throw new Error(r.status);
|
|
}
|
|
});
|
|
let ymls = [res[0].text()];
|
|
if (reg !== "") {
|
|
ymls.push(res[1].text());
|
|
} else {
|
|
// make dummy yml file with one big frequency range
|
|
ymls.push("- header: true\n columns: []\n titles: []\n frequency_unit: GHz\n- frequency: 0-" + 9007199254740991);
|
|
}
|
|
Promise.all(ymls).then((d) => {
|
|
try {
|
|
update_table(yaml.load(d[0]), yaml.load(d[1]));
|
|
} catch (e) {
|
|
display_error(e);
|
|
}
|
|
}).catch((e) => {
|
|
display_error(e);
|
|
});
|
|
}).catch((e) => {
|
|
display_error(e);
|
|
});
|
|
|
|
// add stylesheet, clear before adding
|
|
let el = document.getElementById("link_css");
|
|
if (el !== null) {
|
|
el.remove();
|
|
}
|
|
let href = document.getElementById("css_input").value;
|
|
if (href !== "") {
|
|
let link = document.createElement("link");
|
|
link.type = "text/css";
|
|
link.rel = "stylesheet";
|
|
link.href = href;
|
|
link.id = "link_css";
|
|
link.setAttribute("crossorigin", "anonymous");
|
|
document.head.appendChild(link);
|
|
}
|
|
}
|
|
|
|
// used in button
|
|
// eslint-disable-next-line no-unused-vars
|
|
function help() {
|
|
alert(
|
|
"Base plan, regulation and css: use your own files for the band plan and css, URLs are possible, CORS needs to be allowed for the files, download standard files for examples\n"
|
|
+ "When Regulation is empty, all frequencies from base plan will be included\n"
|
|
+ "When CSS is empty or URL not available, colors, etc. will be gone\n"
|
|
+ "Filter rows with text in column headings, use filter starting with \"!\" as exclusion\n"
|
|
+ "Download list: saves .pdf of the current list (selection of rows apply)\n"
|
|
+ "Selection of rows with mouse possible\n"
|
|
+ "Download plans/css: get the yml and css files\n"
|
|
+ "Create link: get link for currently selected plans and css for bookmarking\n"
|
|
);
|
|
}
|
|
</script>
|
|
<div>
|
|
<label>Base Plan:
|
|
<input id="base_input" type="text" value="band-plan-iaru_r1_hf.yml">
|
|
</label>
|
|
<label>Regulation:
|
|
<input id="regulation_input" type="text" value="regulations-de.yml">
|
|
</label>
|
|
<label>CSS:
|
|
<input id="css_input" type="text" value="standard.css">
|
|
</label>
|
|
<button onclick="update_inputs()">Update</button>
|
|
</div>
|
|
<div id="togglebuttons"></div>
|
|
<div>
|
|
<button id="download">Download list</button>
|
|
<button id="download_base">Download base plan</button>
|
|
<button id="download_regulation">Download regulation</button>
|
|
<button id="download_css">Download css file</button>
|
|
<button id="create_link">Create link</button>
|
|
<button onclick="help()">Help</button>
|
|
</div>
|
|
<div id="data-table"></div>
|
|
<div id="error"></div>
|
|
<div id="comments_base"></div>
|
|
<div id="comments_regulation"></div>
|
|
<div>
|
|
<p>This information is supplied without liability.</p>
|
|
<p>Source is at <a href="https://src.dm5wk.de/dm5wk/band-plan-web/">https://src.dm5wk.de/dm5wk/band-plan-web/</a></p>
|
|
</div>
|
|
</body>
|
|
</html>
|