Add filepath input for loading any JSON file via API
This commit is contained in:
parent
a93ade34b4
commit
79e1259f5b
|
|
@ -271,8 +271,9 @@
|
|||
<h1>Path Navigator</h1>
|
||||
|
||||
<div class="file-input">
|
||||
<span>Upload file:</span>
|
||||
<input type="file" id="fileInput" accept=".json">
|
||||
<span>File:</span>
|
||||
<input type="text" id="filepathInput" value="output/output_chords.json" style="width: 250px;">
|
||||
<button id="loadFileBtn">Load</button>
|
||||
<span style="margin-left: 20px;">Fundamental (Hz):</span>
|
||||
<input type="number" id="fundamentalInput" value="110" style="width: 60px;">
|
||||
</div>
|
||||
|
|
@ -316,7 +317,7 @@
|
|||
// Graph dimensions - will expand based on number of chords
|
||||
let graphWidth = 1100;
|
||||
const graphHeight = 450;
|
||||
const chordSpacing = 350;
|
||||
let chordSpacing = 350;
|
||||
|
||||
// Voice colors - sleek pastel scheme
|
||||
const voiceColors = ['#7eb5a6', '#c5a3ff', '#ffb3b3', '#ffd700'];
|
||||
|
|
@ -374,10 +375,11 @@
|
|||
layout: {
|
||||
name: 'preset',
|
||||
},
|
||||
minZoom: 1,
|
||||
maxZoom: 1,
|
||||
zoomingEnabled: false,
|
||||
panningEnabled: false,
|
||||
minZoom: 0.3,
|
||||
maxZoom: 3,
|
||||
zoomingEnabled: true,
|
||||
panningEnabled: true,
|
||||
wheelSensitivity: 0.2, // Reduce wheel zoom sensitivity
|
||||
autounselectify: true,
|
||||
boxSelectionEnabled: false,
|
||||
});
|
||||
|
|
@ -450,102 +452,6 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Render all three chords using Cytoscape (legacy - not used with all-chords approach)
|
||||
function renderAllThree(prevData, currentData, nextData) {
|
||||
if (!cy) initCytoscapeAll(3);
|
||||
|
||||
// Clear previous elements
|
||||
cy.elements().remove();
|
||||
|
||||
// Build elements array
|
||||
let elements = [];
|
||||
|
||||
// Collect all chords
|
||||
const allChords = [];
|
||||
if (prevData && prevData.nodes) allChords.push({data: prevData, xPos: graphWidth * 0.2, label: "prev"});
|
||||
if (currentData && currentData.nodes) allChords.push({data: currentData, xPos: graphWidth * 0.5, label: "current"});
|
||||
if (nextData && nextData.nodes) allChords.push({data: nextData, xPos: graphWidth * 0.8, label: "next"});
|
||||
|
||||
// Collect all cents from all chords for global scale
|
||||
let allCents = [];
|
||||
allChords.forEach(c => {
|
||||
allCents = allCents.concat(c.data.nodes.map(n => n.cents));
|
||||
});
|
||||
const globalMinCents = Math.min(...allCents);
|
||||
const globalMaxCents = Math.max(...allCents);
|
||||
const globalCentsRange = globalMaxCents - globalMinCents || 1;
|
||||
const ySpread = graphHeight * 0.8;
|
||||
const yBase = graphHeight * 0.1;
|
||||
|
||||
// Add nodes for each chord
|
||||
let nodeOffset = 0;
|
||||
allChords.forEach(({data, xPos, label: chordLabel}) => {
|
||||
const nodes = data.nodes;
|
||||
const edges = data.edges || [];
|
||||
const idMap = {};
|
||||
|
||||
nodes.forEach((n, i) => {
|
||||
const globalId = nodeOffset + n.id;
|
||||
idMap[n.id] = globalId;
|
||||
|
||||
// Wider initial spread: 25% of graphWidth
|
||||
const x = xPos + (graphWidth * 0.25) * (i / (nodes.length - 1 || 1));
|
||||
const y = yBase + ySpread - ((n.cents - globalMinCents) / globalCentsRange) * ySpread;
|
||||
|
||||
elements.push({
|
||||
group: 'nodes',
|
||||
data: {
|
||||
id: String(globalId),
|
||||
localId: n.id,
|
||||
cents: n.cents,
|
||||
fraction: n.fraction,
|
||||
chordLabel: chordLabel,
|
||||
color: voiceColors[n.id % voiceColors.length],
|
||||
},
|
||||
position: { x: x, y: y },
|
||||
});
|
||||
});
|
||||
|
||||
// Add edges
|
||||
edges.forEach(e => {
|
||||
elements.push({
|
||||
group: 'edges',
|
||||
data: {
|
||||
source: String(idMap[e.source]),
|
||||
target: String(idMap[e.target]),
|
||||
ratio: e.ratio,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
nodeOffset += nodes.length;
|
||||
});
|
||||
|
||||
if (elements.length === 0) return;
|
||||
|
||||
// Add elements to Cytoscape
|
||||
cy.add(elements);
|
||||
|
||||
// Run xforce layout to optimize x positions while keeping y fixed
|
||||
// Bounds: each chord stays in its third of the container
|
||||
cy.layout({
|
||||
name: 'xforce',
|
||||
linkDistance: 60,
|
||||
linkStrength: 0.1,
|
||||
charge: -60,
|
||||
collisionDistance: 35,
|
||||
damping: 0.7,
|
||||
iterations: 150,
|
||||
bounds: {
|
||||
'prev': { min: 50, max: 400 },
|
||||
'current': { min: 350, max: 750 },
|
||||
'next': { min: 700, max: 1050 },
|
||||
},
|
||||
}).run();
|
||||
|
||||
cy.fit();
|
||||
}
|
||||
|
||||
// Render ALL chords at once for continuous canvas
|
||||
function renderAllChords() {
|
||||
if (!allGraphsData || !cy) return;
|
||||
|
|
@ -717,57 +623,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Load from Flask API - get computed graph from Python (legacy)
|
||||
async function loadFromAPI() {
|
||||
try {
|
||||
const response = await fetch(`/api/graph/${currentIndex}`);
|
||||
if (!response.ok) throw new Error("API not available");
|
||||
const data = await response.json();
|
||||
|
||||
currentIndex = data.index;
|
||||
totalSteps = data.total - 1;
|
||||
hasPrev = data.has_prev;
|
||||
hasNext = data.has_next;
|
||||
|
||||
// Render all three graphs in single panel
|
||||
renderAllThree(data.prev, data.current, data.next);
|
||||
|
||||
// Update displays
|
||||
document.getElementById("currentIndex").textContent = currentIndex;
|
||||
document.getElementById("totalSteps").textContent = totalSteps;
|
||||
document.getElementById("prevBtn").disabled = !hasPrev;
|
||||
document.getElementById("nextBtn").disabled = !hasNext;
|
||||
|
||||
// Update chord panels
|
||||
updateChordPanel("prevPitches", data.prev ? data.prev.nodes : null);
|
||||
updateChordPanel("currentPitches", data.current ? data.current.nodes : null);
|
||||
updateChordPanel("nextPitches", data.next ? data.next.nodes : null);
|
||||
|
||||
} catch (e) {
|
||||
console.log("API not available, trying local file", e);
|
||||
loadFromLocalFile();
|
||||
}
|
||||
}
|
||||
|
||||
async function setFundamental() {
|
||||
const input = document.getElementById("fundamentalInput");
|
||||
const fundamental = parseFloat(input.value);
|
||||
if (!fundamental || fundamental <= 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch("/api/set-fundamental", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({ fundamental: fundamental })
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log("Fundamental set to:", data.fundamental, "Hz");
|
||||
} catch (e) {
|
||||
console.log("Error setting fundamental", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fundamental input - auto-send on Enter, Up/Down arrows, or input change
|
||||
document.getElementById("fundamentalInput").addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
|
|
@ -791,39 +646,34 @@
|
|||
setFundamental();
|
||||
});
|
||||
|
||||
async function loadFromLocalFile() {
|
||||
try {
|
||||
const response = await fetch("output/output_chords.json");
|
||||
if (!response.ok) throw new Error("File not found");
|
||||
const data = await response.json();
|
||||
loadChords(data.chords);
|
||||
} catch (e) {
|
||||
console.log("No default file, waiting for user upload");
|
||||
}
|
||||
}
|
||||
|
||||
// File input handler
|
||||
document.getElementById("fileInput").addEventListener("change", async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
document.getElementById("loadFileBtn").addEventListener("click", async () => {
|
||||
const filepath = document.getElementById("filepathInput").value;
|
||||
if (!filepath) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.target.result);
|
||||
loadChords(data.chords || data);
|
||||
} catch (err) {
|
||||
alert("Invalid JSON file");
|
||||
try {
|
||||
const response = await fetch("/api/load-file", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({ filepath: filepath })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
loadAllGraphs();
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
} catch (err) {
|
||||
alert("Error loading file: " + err);
|
||||
}
|
||||
});
|
||||
|
||||
function loadChords(chordData) {
|
||||
chords = chordData;
|
||||
currentIndex = 0;
|
||||
updateDisplay();
|
||||
}
|
||||
// Also allow Enter key in filepath input
|
||||
document.getElementById("filepathInput").addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
document.getElementById("loadFileBtn").click();
|
||||
}
|
||||
});
|
||||
|
||||
function updateChordPanel(elementId, data) {
|
||||
const container = document.getElementById(elementId);
|
||||
|
|
@ -894,11 +744,6 @@
|
|||
container.appendChild(table);
|
||||
}
|
||||
|
||||
// Update display - now using pan navigation with all chords loaded
|
||||
function updateDisplay() {
|
||||
updateUI();
|
||||
}
|
||||
|
||||
// Navigation with pan animation
|
||||
async function navigate(direction) {
|
||||
if (direction === 'prev' && currentIndex > 0) {
|
||||
|
|
@ -931,6 +776,18 @@
|
|||
navigate('prev');
|
||||
} else if (e.key === "ArrowRight") {
|
||||
navigate('next');
|
||||
} else if (e.key === "+" || e.key === "=") {
|
||||
// Zoom in
|
||||
if (cy) {
|
||||
const zoom = cy.zoom();
|
||||
cy.zoom({ zoomLevel: Math.min(3, zoom * 1.1), renderedPosition: { x: cy.width()/2, y: cy.height()/2 } });
|
||||
}
|
||||
} else if (e.key === "-") {
|
||||
// Zoom out
|
||||
if (cy) {
|
||||
const zoom = cy.zoom();
|
||||
cy.zoom({ zoomLevel: Math.max(0.3, zoom / 1.1), renderedPosition: { x: cy.width()/2, y: cy.height()/2 } });
|
||||
}
|
||||
} else if (e.key === "k") {
|
||||
// Soft kill - send 20 Hz to stop voices gently
|
||||
fetch('/api/kill-siren', {
|
||||
|
|
|
|||
|
|
@ -385,6 +385,27 @@ def kill_siren():
|
|||
)
|
||||
|
||||
|
||||
@app.route("/api/load-file", methods=["POST"])
|
||||
def load_file():
|
||||
global current_index, chords
|
||||
data = request.json
|
||||
filepath = data.get("filepath")
|
||||
|
||||
if not filepath:
|
||||
return jsonify({"error": "No filepath provided"}), 400
|
||||
|
||||
try:
|
||||
with open(filepath) as f:
|
||||
chords_data = json.load(f)
|
||||
chords = chords_data.get("chords", [])
|
||||
current_index = 0
|
||||
return jsonify({"loaded": len(chords), "filepath": filepath})
|
||||
except FileNotFoundError:
|
||||
return jsonify({"error": f"File not found: {filepath}"}), 404
|
||||
except json.JSONDecodeError:
|
||||
return jsonify({"error": f"Invalid JSON in: {filepath}"}), 400
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Starting Path Navigator server...")
|
||||
print(f"Loading chords from: {get_chords_file()}")
|
||||
|
|
|
|||
Loading…
Reference in a new issue