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>
|
<h1>Path Navigator</h1>
|
||||||
|
|
||||||
<div class="file-input">
|
<div class="file-input">
|
||||||
<span>Upload file:</span>
|
<span>File:</span>
|
||||||
<input type="file" id="fileInput" accept=".json">
|
<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>
|
<span style="margin-left: 20px;">Fundamental (Hz):</span>
|
||||||
<input type="number" id="fundamentalInput" value="110" style="width: 60px;">
|
<input type="number" id="fundamentalInput" value="110" style="width: 60px;">
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -316,7 +317,7 @@
|
||||||
// Graph dimensions - will expand based on number of chords
|
// Graph dimensions - will expand based on number of chords
|
||||||
let graphWidth = 1100;
|
let graphWidth = 1100;
|
||||||
const graphHeight = 450;
|
const graphHeight = 450;
|
||||||
const chordSpacing = 350;
|
let chordSpacing = 350;
|
||||||
|
|
||||||
// Voice colors - sleek pastel scheme
|
// Voice colors - sleek pastel scheme
|
||||||
const voiceColors = ['#7eb5a6', '#c5a3ff', '#ffb3b3', '#ffd700'];
|
const voiceColors = ['#7eb5a6', '#c5a3ff', '#ffb3b3', '#ffd700'];
|
||||||
|
|
@ -374,14 +375,15 @@
|
||||||
layout: {
|
layout: {
|
||||||
name: 'preset',
|
name: 'preset',
|
||||||
},
|
},
|
||||||
minZoom: 1,
|
minZoom: 0.3,
|
||||||
maxZoom: 1,
|
maxZoom: 3,
|
||||||
zoomingEnabled: false,
|
zoomingEnabled: true,
|
||||||
panningEnabled: false,
|
panningEnabled: true,
|
||||||
|
wheelSensitivity: 0.2, // Reduce wheel zoom sensitivity
|
||||||
autounselectify: true,
|
autounselectify: true,
|
||||||
boxSelectionEnabled: false,
|
boxSelectionEnabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Lock y on drag - only allow x movement
|
// Lock y on drag - only allow x movement
|
||||||
let isDragging = false;
|
let isDragging = false;
|
||||||
let grabPosition = null;
|
let grabPosition = null;
|
||||||
|
|
@ -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
|
// Render ALL chords at once for continuous canvas
|
||||||
function renderAllChords() {
|
function renderAllChords() {
|
||||||
if (!allGraphsData || !cy) return;
|
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
|
// Fundamental input - auto-send on Enter, Up/Down arrows, or input change
|
||||||
document.getElementById("fundamentalInput").addEventListener("keydown", (e) => {
|
document.getElementById("fundamentalInput").addEventListener("keydown", (e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
|
|
@ -791,39 +646,34 @@
|
||||||
setFundamental();
|
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
|
// File input handler
|
||||||
document.getElementById("fileInput").addEventListener("change", async (e) => {
|
document.getElementById("loadFileBtn").addEventListener("click", async () => {
|
||||||
const file = e.target.files[0];
|
const filepath = document.getElementById("filepathInput").value;
|
||||||
if (!file) return;
|
if (!filepath) return;
|
||||||
|
|
||||||
const reader = new FileReader();
|
try {
|
||||||
reader.onload = (event) => {
|
const response = await fetch("/api/load-file", {
|
||||||
try {
|
method: "POST",
|
||||||
const data = JSON.parse(event.target.result);
|
headers: {"Content-Type": "application/json"},
|
||||||
loadChords(data.chords || data);
|
body: JSON.stringify({ filepath: filepath })
|
||||||
} catch (err) {
|
});
|
||||||
alert("Invalid JSON file");
|
const data = await response.json();
|
||||||
|
if (data.error) {
|
||||||
|
alert(data.error);
|
||||||
|
} else {
|
||||||
|
loadAllGraphs();
|
||||||
}
|
}
|
||||||
};
|
} catch (err) {
|
||||||
reader.readAsText(file);
|
alert("Error loading file: " + err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function loadChords(chordData) {
|
// Also allow Enter key in filepath input
|
||||||
chords = chordData;
|
document.getElementById("filepathInput").addEventListener("keydown", (e) => {
|
||||||
currentIndex = 0;
|
if (e.key === "Enter") {
|
||||||
updateDisplay();
|
document.getElementById("loadFileBtn").click();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function updateChordPanel(elementId, data) {
|
function updateChordPanel(elementId, data) {
|
||||||
const container = document.getElementById(elementId);
|
const container = document.getElementById(elementId);
|
||||||
|
|
@ -894,11 +744,6 @@
|
||||||
container.appendChild(table);
|
container.appendChild(table);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update display - now using pan navigation with all chords loaded
|
|
||||||
function updateDisplay() {
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigation with pan animation
|
// Navigation with pan animation
|
||||||
async function navigate(direction) {
|
async function navigate(direction) {
|
||||||
if (direction === 'prev' && currentIndex > 0) {
|
if (direction === 'prev' && currentIndex > 0) {
|
||||||
|
|
@ -931,6 +776,18 @@
|
||||||
navigate('prev');
|
navigate('prev');
|
||||||
} else if (e.key === "ArrowRight") {
|
} else if (e.key === "ArrowRight") {
|
||||||
navigate('next');
|
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") {
|
} else if (e.key === "k") {
|
||||||
// Soft kill - send 20 Hz to stop voices gently
|
// Soft kill - send 20 Hz to stop voices gently
|
||||||
fetch('/api/kill-siren', {
|
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__":
|
if __name__ == "__main__":
|
||||||
print("Starting Path Navigator server...")
|
print("Starting Path Navigator server...")
|
||||||
print(f"Loading chords from: {get_chords_file()}")
|
print(f"Loading chords from: {get_chords_file()}")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue