diff --git a/server/routes/pedigree.js b/server/routes/pedigree.js new file mode 100644 index 0000000..7855ec9 --- /dev/null +++ b/server/routes/pedigree.js @@ -0,0 +1,185 @@ +const express = require('express'); +const router = express.Router(); +const { getDatabase } = require('../db/init'); + +// Helper function to calculate inbreeding coefficient +function calculateCOI(sireId, damId, generations = 5) { + const db = getDatabase(); + + // Get all ancestors for both parents + function getAncestors(dogId, currentGen = 0, maxGen = generations) { + if (currentGen >= maxGen) return []; + + const parents = db.prepare(` + SELECT p.parent_type, p.parent_id, d.name + FROM parents p + JOIN dogs d ON p.parent_id = d.id + WHERE p.dog_id = ? + `).all(dogId); + + const ancestors = parents.map(p => ({ + id: p.parent_id, + name: p.name, + type: p.parent_type, + generation: currentGen + 1 + })); + + parents.forEach(p => { + ancestors.push(...getAncestors(p.parent_id, currentGen + 1, maxGen)); + }); + + return ancestors; + } + + const sireAncestors = getAncestors(sireId); + const damAncestors = getAncestors(damId); + + // Find common ancestors + const commonAncestors = []; + sireAncestors.forEach(sireAnc => { + damAncestors.forEach(damAnc => { + if (sireAnc.id === damAnc.id) { + commonAncestors.push({ + id: sireAnc.id, + name: sireAnc.name, + sireGen: sireAnc.generation, + damGen: damAnc.generation + }); + } + }); + }); + + // Calculate COI using path coefficient method + let coi = 0; + const processed = new Set(); + + commonAncestors.forEach(anc => { + const key = `${anc.id}-${anc.sireGen}-${anc.damGen}`; + if (!processed.has(key)) { + processed.add(key); + const pathLength = anc.sireGen + anc.damGen; + coi += Math.pow(0.5, pathLength); + } + }); + + return { + coefficient: Math.round(coi * 10000) / 100, // Percentage with 2 decimals + commonAncestors: [...new Map(commonAncestors.map(a => [a.id, a])).values()] + }; +} + +// 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; \ No newline at end of file