///|
/// A feature projection in BED/GFF-compatible coordinate conventions.
pub struct FeatureProjection {
accession : String
key : String
start : Int
end : Int
strand : String
name : String
attributes : String
} derive(Debug, Eq)
///|
/// Convert a feature to a zero-based, half-open projection for genome tools.
pub fn project_feature(record : Record, feature : Feature) -> FeatureProjection {
let summary = summarize_location(feature.location)
let name = match feature.qualifier("gene") {
Some(value) => value
None =>
match feature.qualifier("locus_tag") {
Some(value) => value
None => feature.key
}
}
let attributes = feature.qualifiers
.map(fn(item) {
let value = match item.value {
Some(text) => text
None => ""
}
item.name + "=" + value.replace_all(old=";", new=",")
})
.join(";")
{
accession: record.accession,
key: feature.key,
start: Int::max(0, summary.minimum - 1),
end: summary.maximum,
strand: if feature.location.complement {
"-"
} else {
"+"
},
name,
attributes,
}
}
///|
/// Project every local feature in source order.
pub fn project_record_features(record : Record) -> Array[FeatureProjection] {
record.features
.filter(fn(feature) { !location_is_remote(feature.location) })
.map(fn(feature) { project_feature(record, feature) })
}
///|
/// Render BED6 rows; coordinates are zero-based and end-exclusive.
pub fn record_to_bed(record : Record) -> String {
let out = StringBuilder()
for projection in project_record_features(record) {
out.write_string(projection.accession)
out.write_char('\t')
out.write_string(projection.start.to_string())
out.write_char('\t')
out.write_string(projection.end.to_string())
out.write_char('\t')
out.write_string(projection.name)
out.write_string("\t0\t")
out.write_string(projection.strand)
out.write_char('\n')
}
out.to_string()
}
///|
/// Render GFF3 rows with escaped attributes for common downstream consumers.
pub fn record_to_gff3(record : Record) -> String {
let out = StringBuilder()
out.write_string("##gff-version 3\n")
for projection in project_record_features(record) {
out.write_string(
projection.accession + "\tmoonbit-genbank\t" + projection.key + "\t",
)
out.write_string(
(projection.start + 1).to_string() + "\t" + projection.end.to_string(),
)
out.write_string("\t.\t" + projection.strand + "\t.\t")
out.write_string(
"ID=" + projection.name + ";" + projection.attributes + "\n",
)
}
out.to_string()
}
///|
/// Render an indexed FASTA header that retains accession and definition.
pub fn record_to_fasta(record : Record, line_width : Int) -> String {
let id = if record.accession == "" {
record.locus.name
} else {
record.accession
}
{ id, description: record.definition, sequence: record.origin }.to_fasta(
line_width,
)
}
///|
/// Return only projections overlapping a requested one-based interval.
pub fn project_interval(
record : Record,
start : Int,
end : Int,
) -> Array[FeatureProjection] {
if start < 1 || end < start {
return []
}
project_record_features(record).filter(fn(item) {
item.start < end && start <= item.end
})
}
///|
/// Serialize a projection for APIs that do not need the full record model.
pub fn FeatureProjection::to_json_string(self : FeatureProjection) -> String {
"{\"accession\":" +
json_string(self.accession) +
",\"key\":" +
json_string(self.key) +
",\"start\":" +
self.start.to_string() +
",\"end\":" +
self.end.to_string() +
",\"strand\":" +
json_string(self.strand) +
",\"name\":" +
json_string(self.name) +
",\"attributes\":" +
json_string(self.attributes) +
"}"
}