const express = require('express'); const router = express.Router(); const { getDatabase } = require('../db/init'); /** * calculateCOI — Wright Path Coefficient method * * Builds a full ancestor map for BOTH the sire and dam, including * themselves at generation 0, so that: * - if sire IS a parent of dam (or vice versa), it shows as a * common ancestor at gen 0 vs gen 1, giving the correct COI. * - multiple paths through the same ancestor are ALL counted. * * The Wright formula per path: (0.5)^(L+1) * (1 + F_A) * where L = number of segregation steps sire-side + dam-side. * F_A (inbreeding of common ancestor) is approximated as 0 here. * * For a direct parent-offspring pair (sire is direct parent of dam): * sireGen = 0, damGen = 1 => (0.5)^(0+1+1) = 0.25 => 25% COI */ function calculateCOI(sireId, damId, generations = 6) { const db = getDatabase(); // Returns a list of { id, name, generation } for every ancestor of dogId // INCLUDING dogId itself at generation 0. function getAncestorMap(dogId) { const visited = new Map(); // id -> [{ id, name, generation }, ...] function recurse(id, gen) { if (gen > generations) return; const dog = db.prepare('SELECT id, name FROM dogs WHERE id = ?').get(id); if (!dog) return; if (!visited.has(id)) visited.set(id, []); visited.get(id).push({ id: dog.id, name: dog.name, generation: gen }); // Only recurse into parents if we haven't already processed this id // at this generation (prevents infinite loops on circular data) if (visited.get(id).length === 1) { const parents = db.prepare(` SELECT p.parent_id, d.name FROM parents p JOIN dogs d ON p.parent_id = d.id WHERE p.dog_id = ? `).all(id); parents.forEach(p => recurse(p.parent_id, gen + 1)); } } recurse(dogId, 0); return visited; } const sireMap = getAncestorMap(sireId); const damMap = getAncestorMap(damId); // Find all IDs that appear in BOTH ancestor maps (common ancestors) // Exclude the sire and dam themselves from being listed as common ancestors // (they can't be common ancestors of the hypothetical offspring with each other) const commonIds = [...sireMap.keys()].filter( id => damMap.has(id) && id !== parseInt(sireId) && id !== parseInt(damId) ); let coi = 0; const commonAncestorList = []; const processedPaths = new Set(); commonIds.forEach(ancId => { const sireOccurrences = sireMap.get(ancId); const damOccurrences = damMap.get(ancId); // Wright: sum over every combination of paths through this ancestor sireOccurrences.forEach(sireOcc => { damOccurrences.forEach(damOcc => { const pathKey = `${ancId}-${sireOcc.generation}-${damOcc.generation}`; if (!processedPaths.has(pathKey)) { processedPaths.add(pathKey); // L = steps from sire to ancestor + steps from dam to ancestor // Wright formula: (0.5)^(L+1) where L = sireGen + damGen const L = sireOcc.generation + damOcc.generation; coi += Math.pow(0.5, L + 1); } }); }); // Build display list using closest occurrence per side const closestSire = sireOccurrences.reduce((a, b) => a.generation < b.generation ? a : b); const closestDam = damOccurrences.reduce((a, b) => a.generation < b.generation ? a : b); commonAncestorList.push({ id: ancId, name: sireOccurrences[0].name, sireGen: closestSire.generation, damGen: closestDam.generation }); }); return { coefficient: Math.round(coi * 10000) / 100, commonAncestors: commonAncestorList }; } // GET pedigree tree for a dog router.get('/:id', (req, res) => { try { const db = getDatabase(); const generations = parseInt(req.query.generations) || 5; function buildTree(dogId, currentGen = 0) { if (currentGen >= generations) return null; const dog = db.prepare('SELECT * FROM dogs WHERE id = ?').get(dogId); if (!dog) return null; dog.photo_urls = dog.photo_urls ? JSON.parse(dog.photo_urls) : []; const parents = db.prepare(` SELECT p.parent_type, p.parent_id FROM parents p WHERE p.dog_id = ? `).all(dogId); const sire = parents.find(p => p.parent_type === 'sire'); const dam = parents.find(p => p.parent_type === 'dam'); return { ...dog, generation: currentGen, sire: sire ? buildTree(sire.parent_id, currentGen + 1) : null, dam: dam ? buildTree(dam.parent_id, currentGen + 1) : null }; } const tree = buildTree(req.params.id); if (!tree) return res.status(404).json({ error: 'Dog not found' }); res.json(tree); } catch (error) { res.status(500).json({ error: error.message }); } }); // GET reverse pedigree (descendants) router.get('/:id/descendants', (req, res) => { try { const db = getDatabase(); const generations = parseInt(req.query.generations) || 3; function buildDescendantTree(dogId, currentGen = 0) { if (currentGen >= generations) return null; const dog = db.prepare('SELECT * FROM dogs WHERE id = ?').get(dogId); if (!dog) return null; dog.photo_urls = dog.photo_urls ? JSON.parse(dog.photo_urls) : []; const offspring = db.prepare(` SELECT DISTINCT d.id, d.name, d.sex, d.birth_date FROM dogs d JOIN parents p ON d.id = p.dog_id WHERE p.parent_id = ? AND d.is_active = 1 `).all(dogId); return { ...dog, generation: currentGen, offspring: offspring.map(child => buildDescendantTree(child.id, currentGen + 1)) }; } const tree = buildDescendantTree(req.params.id); if (!tree) return res.status(404).json({ error: 'Dog not found' }); res.json(tree); } catch (error) { res.status(500).json({ error: error.message }); } }); // POST calculate COI for a trial pairing router.post('/trial-pairing', (req, res) => { try { const { sire_id, dam_id } = req.body; if (!sire_id || !dam_id) { return res.status(400).json({ error: 'Both sire_id and dam_id are required' }); } const db = getDatabase(); const sire = db.prepare("SELECT * FROM dogs WHERE id = ? AND sex = 'male'").get(sire_id); const dam = db.prepare("SELECT * FROM dogs WHERE id = ? AND sex = 'female'").get(dam_id); if (!sire || !dam) { return res.status(404).json({ error: 'Invalid sire or dam' }); } const result = calculateCOI(sire_id, dam_id); res.json({ sire: { id: sire.id, name: sire.name }, dam: { id: dam.id, name: dam.name }, coi: result.coefficient, commonAncestors: result.commonAncestors, recommendation: result.coefficient < 5 ? 'Low risk' : result.coefficient < 10 ? 'Moderate risk' : 'High risk' }); } catch (error) { res.status(500).json({ error: error.message }); } }); module.exports = router;