Add pedigree API routes
This commit is contained in:
185
server/routes/pedigree.js
Normal file
185
server/routes/pedigree.js
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user