From 6adec0c02271f52d1bf51fbdcf44743a6ce65a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilhem=20Semp=C3=A9r=C3=A9?= Date: Mon, 23 Feb 2026 16:53:48 +0100 Subject: [PATCH 01/10] Minor change --- src/main/webapp/js/charts.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/webapp/js/charts.js b/src/main/webapp/js/charts.js index aeeb334c..36ac78cb 100644 --- a/src/main/webapp/js/charts.js +++ b/src/main/webapp/js/charts.js @@ -207,7 +207,7 @@ function initializeChartDisplay() { displayName: "MAF distribution", queryURLFunction: "getChartMafDataURL", title: "MAF values for {{displayedVariantType}} variants on sequence {{displayedSequence}}", - subtitle: "MAF values averaged in an interval of size {{intervalSize}} around each point (excluding missing and multi-allelic variants)", + subtitle: "MAF values averaged in an interval of size {{intervalSize}} around each point (only accounting for bi-allelic variants)", xAxisTitle: "Positions on selected sequence", series: [{ name: "MAF * 100", @@ -242,7 +242,7 @@ function initializeChartDisplay() { displayName: "Missing data distribution", queryURLFunction: "getChartMissingDataURL", title: "Missing data percentage for {{displayedVariantType}} variants on sequence {{displayedSequence}}", - subtitle: "Missing data percentage averaged in an interval of size {{intervalSize}} around each point (excluding missing and multi-allelic variants)", + subtitle: "Missing data percentage averaged in an interval of size {{intervalSize}} around each point", xAxisTitle: "Positions on selected sequence", series: [{ name: "Missing data percentage", @@ -277,7 +277,7 @@ function initializeChartDisplay() { displayName: "Heterozygosity distribution", queryURLFunction: "getChartHeterozygosityDataURL", title: "Heterozygosity percentage for {{displayedVariantType}} variants on sequence {{displayedSequence}}", - subtitle: "Heterozygosity percentage averaged in an interval of size {{intervalSize}} around each point (excluding missing and multi-allelic variants)", + subtitle: "Heterozygosity percentage averaged in an interval of size {{intervalSize}} around each point", xAxisTitle: "Positions on selected sequence", series: [{ name: "Heterozygosity percentage", @@ -312,7 +312,7 @@ function initializeChartDisplay() { displayName: "Fst", queryURLFunction: "getChartFstDataURL", title: "Fst value for {{displayedVariantType}} variants on sequence {{displayedSequence}}", - subtitle: "Weir and Cockerham Fst estimate calculated between selected groups in an interval of size {{intervalSize}} around each point", + subtitle: "Weir and Cockerham Fst estimate calculated between selected groups in an interval of size {{intervalSize}} around each point (only accounting for bi-allelic variants)", xAxisTitle: "Positions on selected sequence", series: [{ name: "Fst estimate", @@ -353,7 +353,7 @@ function initializeChartDisplay() { displayName: "Tajima's D", queryURLFunction: "getChartTajimaDDataURL", title: "Tajima's D values for {{displayedVariantType}} variants on sequence {{displayedSequence}}", - subtitle: "Tajima's D values averaged in an interval of size {{intervalSize}} around each point (excluding missing and multi-allelic variants)", + subtitle: "Tajima's D values averaged in an interval of size {{intervalSize}} around each point (only accounting for bi-allelic variants)", xAxisTitle: "Positions on selected sequence", series: [{ name: "Tajima's D", From 3dff6cc34d98f595d5db8cf33d3e49e4c992025b Mon Sep 17 00:00:00 2001 From: droc Date: Wed, 11 Mar 2026 10:03:42 +0100 Subject: [PATCH 02/10] Update genomes.json (#173) --- src/main/webapp/res/genomes.json | 149 ++++++++++++++++--------------- 1 file changed, 75 insertions(+), 74 deletions(-) diff --git a/src/main/webapp/res/genomes.json b/src/main/webapp/res/genomes.json index 4fee7dcc..22fbdebc 100644 --- a/src/main/webapp/res/genomes.json +++ b/src/main/webapp/res/genomes.json @@ -1,17 +1,17 @@ [ { - "id": "Oryza_sativa_japonica_Nipponbare", + "id": "oryza_sativa_japonica_nipponbare_v7", "name": "Oryza sativa Nipponbare (v7.0)", - "fastaURL": "https://jbrowse.southgreen.fr/Oryza_sativa_japonica_Nipponbare/seq/Oryza_sativa_japonica_Nipponbare.assembly.fna", - "indexURL": "https://jbrowse.southgreen.fr/Oryza_sativa_japonica_Nipponbare/seq/Oryza_sativa_japonica_Nipponbare.assembly.fna.fai", + "fastaURL": "https://jbrowse.southgreen.fr/oryza_sativa_japonica_nipponbare_v7/seq/Oryza_sativa_japonica_Nipponbare.assembly.fna", + "indexURL": "https://jbrowse.southgreen.fr/oryza_sativa_japonica_nipponbare_v7/seq/Oryza_sativa_japonica_Nipponbare.assembly.fna.fai", "tracks": [ { "name": "Genes (RAPDB)", "type": "annotation", "format": "gff3", "sourceType": "file", - "url": "https://jbrowse.southgreen.fr/Oryza_sativa_japonica_Nipponbare/gff3/Oryza_sativa_japonica_Nipponbare.gff3.gz", - "indexURL": "https://jbrowse.southgreen.fr/Oryza_sativa_japonica_Nipponbare/gff3/Oryza_sativa_japonica_Nipponbare.gff3.gz.tbi", + "url": "https://jbrowse.southgreen.fr/oryza_sativa_japonica_nipponbare_v7/gff3/Oryza_sativa_japonica_Nipponbare.gff3.gz", + "indexURL": "https://jbrowse.southgreen.fr/oryza_sativa_japonica_nipponbare_v7/gff3/Oryza_sativa_japonica_Nipponbare.gff3.gz.tbi", "order": 0 }, { @@ -19,8 +19,8 @@ "type": "annotation", "format": "gff3", "sourceType": "file", - "url": "https://jbrowse.southgreen.fr/Oryza_sativa_japonica_Nipponbare/gff3/MSU.gff3.gz", - "indexURL": "https://jbrowse.southgreen.fr/Oryza_sativa_japonica_Nipponbare/gff3/MSU.gff3.gz.tbi", + "url": "https://jbrowse.southgreen.fr/oryza_sativa_japonica_nipponbare_v7/gff3/MSU.gff3.gz", + "indexURL": "https://jbrowse.southgreen.fr/oryza_sativa_japonica_nipponbare_v7/gff3/MSU.gff3.gz.tbi", "order": 0 }, { @@ -28,93 +28,93 @@ "type": "annotation", "format": "gff3", "sourceType": "file", - "url": "https://jbrowse.southgreen.fr/Oryza_sativa_japonica_Nipponbare/gff3/LRR_Nipponbare.gff3.gz", - "indexURL": "https://jbrowse.southgreen.fr/Oryza_sativa_japonica_Nipponbare/gff3/LRR_Nipponbare.gff3.gz.tbi", + "url": "https://jbrowse.southgreen.fr/oryza_sativa_japonica_nipponbare_v7/gff3/LRR_Nipponbare.gff3.gz", + "indexURL": "https://jbrowse.southgreen.fr/oryza_sativa_japonica_nipponbare_v7/gff3/LRR_Nipponbare.gff3.gz.tbi", "order": 0 } ] }, { - "id": "Oryza_glaberrima_AGI1", + "id": "oryza_glaberrima_v2", "name": "Oryza glaberrima", - "fastaURL": "https://jbrowse.southgreen.fr/Oryza_glaberrima_AGI1/seq/Oryza_glaberrima.AGI1.1.39.assembly.fa", - "indexURL": "https://jbrowse.southgreen.fr/Oryza_glaberrima_AGI1/seq/Oryza_glaberrima.AGI1.1.39.assembly.fa.fai", + "fastaURL": "https://jbrowse.southgreen.fr/oryza_glaberrima_v2/seq/Oryza_glaberrima.assembly.fna", + "indexURL": "https://jbrowse.southgreen.fr/oryza_glaberrima_v2/seq/Oryza_glaberrima.assembly.fna.fai", "tracks": [ { "name": "Locus", "type": "annotation", "format": "gff3", "sourceType": "file", - "url": "https://jbrowse.southgreen.fr/Oryza_glaberrima_AGI1/gff3/Oryza_glaberrima.AGI1.1.39.gff3.gz", - "indexURL": "https://jbrowse.southgreen.fr/Oryza_glaberrima_AGI1/gff3/Oryza_glaberrima.AGI1.1.39.gff3.gz.tbi", + "url": "https://jbrowse.southgreen.fr/oryza_glaberrima_v2/gff3/Oryza_glaberrima.gff3.gz", + "indexURL": "https://jbrowse.southgreen.fr/oryza_glaberrima_v2/gff3/Oryza_glaberrima.gff3.gz.tbi", "order": 0 } ] }, { - "id": "musa_acuminata_pahang_v2", + "id": "musa_acuminata_subsp_malaccensis_v2", "name": "Musa acuminata DH Pahang (v2.0)", - "fastaURL": "https://jbrowse.southgreen.fr/musa_acuminata_pahang_v2/seq/musa_acuminata_v2_pseudochromosome.fna", - "indexURL": "https://jbrowse.southgreen.fr/musa_acuminata_pahang_v2/seq/musa_acuminata_v2_pseudochromosome.fna.fai", + "fastaURL": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v2/seq/musa_acuminata_v2_pseudochromosome.fna", + "indexURL": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v2/seq/musa_acuminata_v2_pseudochromosome.fna.fai", "tracks": [ { "name": "Locus", "type": "annotation", "format": "gff3", "sourceType": "file", - "url": "https://jbrowse.southgreen.fr/musa_acuminata_pahang_v2/gff3/musa_acuminata_v2.gff3.gz", - "indexURL": "https://jbrowse.southgreen.fr/musa_acuminata_pahang_v2/gff3/musa_acuminata_v2.gff3.gz.tbi", + "url": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v2/gff3/musa_acuminata_v2.gff3.gz", + "indexURL": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v2/gff3/musa_acuminata_v2.gff3.gz.tbi", "order": 0 } ] }, { - "id": "musa_acuminata_pahang_v4", + "id": "musa_acuminata_subsp_malaccensis_v2", "name": "Musa acuminata DH Pahang (v4.3)", - "fastaURL": "https://jbrowse.southgreen.fr/musa_acuminata_pahang_v4/seq/Musa_acuminata_pahang_v4.fasta", - "indexURL": "https://jbrowse.southgreen.fr/musa_acuminata_pahang_v4/seq/Musa_acuminata_pahang_v4.fasta.fai", + "fastaURL": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v2/seq/Musa_acuminata_subsp_malaccensis.assembly.fna", + "indexURL": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v4/seq/Musa_acuminata_subsp_malaccensis.assembly.fna.fai", "tracks": [ { "name": "Locus", "type": "annotation", "format": "gff3", "sourceType": "file", - "url": "https://jbrowse.southgreen.fr/musa_acuminata_pahang_v4/gff3/Musa_acuminata_pahang_v4.gff3.gz", - "indexURL": "https://jbrowse.southgreen.fr/musa_acuminata_pahang_v4/gff3/Musa_acuminata_pahang_v4.gff3.gz.tbi", + "url": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v4/gff3/Musa_acuminata_subsp_malaccensis.gff3.gz", + "indexURL": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v4/gff3/Musa_acuminata_subsp_malaccensis.gff3.gz.tbi", "order": 0 } ] }, { - "id": "musa_balbisiana_dhpkw_v1.1", + "id": "musa_balbisiana_v1", "name": "Musa balbisiana DH PKW (v1.3)", - "fastaURL": "https://jbrowse.southgreen.fr/musa_balbisiana_v1.1/seq/Musa_balbisiana_assembly.fna", - "indexURL": "https://jbrowse.southgreen.fr/musa_balbisiana_v1.1/seq/Musa_balbisiana_assembly.fna.fai", + "fastaURL": "https://jbrowse.southgreen.fr/musa_balbisiana_v1/seq/Musa_balbisiana_assembly.fna", + "indexURL": "https://jbrowse.southgreen.fr/musa_balbisiana_v1/seq/Musa_balbisiana_assembly.fna.fai", "tracks": [ { "name": "Locus", "type": "annotation", "format": "gff3", "sourceType": "file", - "url": "https://jbrowse.southgreen.fr/musa_balbisiana_v1.1/gff3/Musa_balbisiana.gff3.gz", - "indexURL": "https://jbrowse.southgreen.fr/musa_balbisiana_v1.1/gff3/Musa_balbisiana.gff3.gz.tbi", + "url": "https://jbrowse.southgreen.fr/musa_balbisiana_v1/gff3/Musa_balbisiana.gff3.gz", + "indexURL": "https://jbrowse.southgreen.fr/musa_balbisiana_v1/gff3/Musa_balbisiana.gff3.gz.tbi", "order": 0 } ] }, { - "id": "sorghum_bicolor_bt623_v3", + "id": "sorghum_bicolor_cv_btx623_v3", "name": "Sorghum bicolor BTx623 (v3.1)", - "fastaURL": "https://jbrowse.southgreen.fr/sorghum_bicolor_BTx623_v3.1/seq/Sbicolor_313_v3.1.assembly.fna", - "indexURL": "https://jbrowse.southgreen.fr/sorghum_bicolor_BTx623_v3.1/seq/Sbicolor_313_v3.1.assembly.fna.fai", + "fastaURL": "https://jbrowse.southgreen.fr/sorghum_bicolor_cv_btx623_v3/seq/Sorghum_bicolor_cv_BTx623.assembly.fna", + "indexURL": "https://jbrowse.southgreen.fr/sorghum_bicolor_cv_btx623_v3/seq/Sorghum_bicolor_cv_BTx623.assembly.fna.fai", "tracks": [ { "name": "Locus", "type": "annotation", "format": "gff3", "sourceType": "file", - "url": "https://jbrowse.southgreen.fr/sorghum_bicolor_BTx623_v3.1/gff3/Sbicolor_313_v3.1.gff3.gz", - "indexURL": "https://jbrowse.southgreen.fr/sorghum_bicolor_BTx623_v3.1/gff3/Sbicolor_313_v3.1.gff3.gz.tbi", + "url": "https://jbrowse.southgreen.fr/sorghum_bicolor_BTx623_v3.1/gff3/Sorghum_bicolor_cv_BTx623.gff3.gz", + "indexURL": "https://jbrowse.southgreen.fr/sorghum_bicolor_BTx623_v3.1/gff3/Sorghum_bicolor_cv_BTx623.gff3.gz.tbi", "order": 0 } ] @@ -137,139 +137,140 @@ ] }, { - "id": "musa_schizocarpa_v1", + "id": "musa_schizocarpa_v2", "name": "Musa schizocarpa", - "fastaURL": "https://jbrowse.southgreen.fr/musa_schizocarpa_v1/seq/Mschizocarpa_chromosomes.fasta", - "indexURL": "https://jbrowse.southgreen.fr/musa_schizocarpa_v1/seq/Mschizocarpa_chromosomes.fasta.fai", + "fastaURL": "https://jbrowse.southgreen.fr/musa_schizocarpa_v2/seq/Musa_schizocarpa.assembly.fna", + "indexURL": "https://jbrowse.southgreen.fr/musa_schizocarpa_v2/seq/Musa_schizocarpa.assembly.fna.fai", "tracks": [ { "name": "Locus", "type": "annotation", "format": "gff3", "sourceType": "file", - "url": "https://jbrowse.southgreen.fr/musa_schizocarpa_v1/gff3/Mschizocarpa_annotation.gff3.gz", - "indexURL": "https://jbrowse.southgreen.fr/musa_schizocarpa_v1/gff3/Mschizocarpa_annotation.gff3.gz.tbi", + "url": "https://jbrowse.southgreen.fr/musa_schizocarpa_v2/gff3/Musa_schizocarpa.gff3.gz", + "indexURL": "https://jbrowse.southgreen.fr/musa_schizocarpa_v2/gff3/Musa_schizocarpa.gff3.gz.tbi", "order": 0 } ] }, { - "id": "coffea_canephora", + "id": "coffea_canephora_v1", "name": "Coffea canephora", - "fastaURL": "https://jbrowse.southgreen.fr/coffea_canephora/seq/coffea_canephora.assembly.fna", - "indexURL": "https://jbrowse.southgreen.fr/coffea_canephora/seq/coffea_canephora.assembly.fna.fai", + "fastaURL": "https://jbrowse.southgreen.fr/coffea_canephora_v1/seq/Coffea_canephora.assembly.fna", + "indexURL": "https://jbrowse.southgreen.fr/coffea_canephora_v1/seq/Coffea_canephora.assembly.fna.fai", "tracks": [ { "name": "Locus", "type": "annotation", "format": "gff3", "sourceType": "file", - "url": "https://jbrowse.southgreen.fr/coffea_canephora/gff3/coffea_canephora.gff3.gz", - "indexURL": "https://jbrowse.southgreen.fr/coffea_canephora/gff3/coffea_canephora.gff3.gz.tbi", + "url": "https://jbrowse.southgreen.fr/coffea_canephora_v1/gff3/Coffea_canephora.gff3.gz", + "indexURL": "https://jbrowse.southgreen.fr/coffea_canephora_v1/gff3/Coffea_canephora.gff3.gz.csi", "order": 0 } ] }, { - "id": "citrus_clementina", + "id": "citrus_clementina_v1", "name": "Citrus clementina", - "fastaURL": "https://jbrowse.southgreen.fr/citrus_clementina/seq/Cclementina_182_v1.fa", - "indexURL": "https://jbrowse.southgreen.fr/citrus_clementina/seq/Cclementina_182_v1.fa.fai", + "fastaURL": "https://jbrowse.southgreen.fr/citrus_clementina_v1/seq/Citrus_clementina.assembly.fna", + "indexURL": "https://jbrowse.southgreen.fr/citrus_clementina_v1/seq/Citrus_clementina.assembly.fna.fai", "tracks": [ { "name": "Locus", "type": "annotation", "format": "gff3", "sourceType": "file", - "url": "https://jbrowse.southgreen.fr/citrus_clementina/gff3/CITCL.gff3.gz", - "indexURL": "https://jbrowse.southgreen.fr/citrus_clementina/gff3/CITCL.gff3.gz.tbi", + "url": "https://jbrowse.southgreen.fr/citrus_clementina_v1/gff3/Citrus_clementina.gff3.gz", + "indexURL": "https://jbrowse.southgreen.fr/citrus_clementina_v1/gff3/Citrus_clementina.gff3.gz.tbi", "order": 0 } ] }, { - "id": "citrus_medica", + "id": "citrus_medica_v1", "name": "Citrus medica", - "fastaURL": "https://jbrowse.southgreen.fr/citrus_medica/seq/CITME.fasta", - "indexURL": "https://jbrowse.southgreen.fr/citrus_medica/seq/CITME.fasta.fai", + "fastaURL": "https://jbrowse.southgreen.fr/citrus_medica_v1/seq/Citrus_medica.assembly.fna", + "indexURL": "https://jbrowse.southgreen.fr/citrus_medica_v1/seq/Citrus_medica.assembly.fna.fai", "tracks": [ { "name": "Locus", "type": "annotation", "format": "gff3", "sourceType": "file", - "url": "https://jbrowse.southgreen.fr/citrus_medica/gff3/CITME.gff3.gz", - "indexURL": "https://jbrowse.southgreen.fr/citrus_medica/gff3/CITME.gff3.gz.tbi", + "url": "https://jbrowse.southgreen.fr/citrus_medica_v1/gff3/Citrus_medica.gff3.gz", + "indexURL": "https://jbrowse.southgreen.fr/citrus_medica_v1/gff3/Citrus_medica.gff3.gz.tbi", "order": 0 } ] }, { - "id": "citrus_reticulata", + "id": "citrus_reticulata_v1", "name": "Citrus reticulata", - "fastaURL": "https://jbrowse.southgreen.fr/citrus_reticulata/seq/CITRE.fasta", - "indexURL": "https://jbrowse.southgreen.fr/citrus_reticulata/seq/CITRE.fasta.fai", + "fastaURL": "https://jbrowse.southgreen.fr/citrus_reticulata_v1/seq/Citrus_reticulata.assembly.fna", + "indexURL": "https://jbrowse.southgreen.fr/citrus_reticulata_v1/seq/Citrus_reticulata.assembly.fna.fai", "tracks": [ { "name": "Locus", "type": "annotation", "format": "gff3", "sourceType": "file", - "url": "https://jbrowse.southgreen.fr/citrus_reticulata/gff3/CITRE.gff3.gz", - "indexURL": "https://jbrowse.southgreen.fr/citrus_reticulata/gff3/CITRE.gff3.gz.tbi", + "url": "https://jbrowse.southgreen.fr/citrus_reticulata_v1/gff3/Citrus_reticulata.gff3.gz", + "indexURL": "https://jbrowse.southgreen.fr/citrus_reticulata_v1/gff3/Citrus_reticulata.gff3.gz.tbi", "order": 0 } ] }, { - "id": "citrus_micrantha", + "id": "citrus_micrantha_v1", "name": "Citrus micrantha", - "fastaURL": "https://jbrowse.southgreen.fr/citrus_micrantha/seq/CITMI.fasta", - "indexURL": "https://jbrowse.southgreen.fr/citrus_micrantha/seq/CITMI.fasta.fai", + "fastaURL": "https://jbrowse.southgreen.fr/citrus_micrantha_v1/seq/Citrus_micrantha.assembly.fna", + "indexURL": "https://jbrowse.southgreen.fr/citrus_micrantha_v1/seq/Citrus_micrantha.assembly.fna.fai", "tracks": [ { "name": "Locus", "type": "annotation", "format": "gff3", "sourceType": "file", - "url": "https://jbrowse.southgreen.fr/citrus_micrantha/gff3/CITMI.gff3.gz", - "indexURL": "https://jbrowse.southgreen.fr/citrus_micrantha/gff3/CITMI.gff3.gz.tbi", + "url": "https://jbrowse.southgreen.fr/citrus_micrantha_v1/gff3/Citrus_micrantha.gff3.gz", + "indexURL": "https://jbrowse.southgreen.fr/citrus_micrantha_v1/gff3/Citrus_micrantha.gff3.gz.tbi", "order": 0 } ] }, { - "id": "olea_europaea_leccino", + "id": "olea_europaea_cv_leccino_v1", "name": "Olea europaea var Leccino", - "fastaURL": "https://jbrowse.southgreen.fr/olea_europaea_leccino/seq/Olea_europaea_leccino.assembly.fasta", - "indexURL": "https://jbrowse.southgreen.fr/olea_europaea_leccino/seq/Olea_europaea_leccino.assembly.fasta.fai", + "fastaURL": "https://jbrowse.southgreen.fr/olea_europaea_cv_leccino_v1/seq/Olea_europaea_cv_Leccino.assembly.fna", + "indexURL": "https://jbrowse.southgreen.fr/olea_euolea_europaea_cv_leccino_v1ropaea_cv_leccino_v1/seq/Olea_europaea_cv_Leccino.assembly.fna.fai", "tracks": [ { "name": "Locus", "type": "annotation", "format": "gff3", "sourceType": "file", - "url": "https://jbrowse.southgreen.fr/olea_europaea_leccino/gff3/Olea_europaea_leccino.gff3.gz", - "indexURL": "https://jbrowse.southgreen.fr/olea_europaea_leccino/gff3/Olea_europaea_leccino.gff3.gz.tbi", + "url": "https://jbrowse.southgreen.fr/olea_europaea_cv_leccino_v1/gff3/Olea_europaea_cv_Leccino.gff3.gz", + "indexURL": "https://jbrowse.southgreen.fr/olea_europaea_cv_leccino_v1/gff3/Olea_europaea_cv_Leccino.gff3.gz.tbi", "order": 0 } ] }, { - "id": "citrus_maxima", + "id": "citrus_maxima_cv_wanbaiyou_v1", "name": "Citrus maxima", - "fastaURL": "https://jbrowse.southgreen.fr/citrus_maxima/seq/CITMA.fasta", - "indexURL": "https://jbrowse.southgreen.fr/citrus_maxima/seq/CITMA.fasta.fai", + "fastaURL": "https://jbrowse.southgreen.fr/citrus_maxima_cv_wanbaiyou_v1/seq/Citrus_maxima_cv_Wanbaiyou.assembly.fna", + "indexURL": "https://jbrowse.southgreen.fr/citrus_maxima_cv_wanbaiyou_v1/seq/Citrus_maxima_cv_Wanbaiyou.assembly.fna.fai", "tracks": [ { "name": "Locus", "type": "annotation", "format": "gff3", "sourceType": "file", - "url": "https://jbrowse.southgreen.fr/citrus_maxima/gff3/CITMA.gff3.gz", - "indexURL": "https://jbrowse.southgreen.fr/citrus_maxima/gff3/CITMA.gff3.gz.tbi", + "url": "https://jbrowse.southgreen.fr/citrus_maxima_cv_wanbaiyou_v1/gff3/Citrus_maxima_cv_Wanbaiyou.gff3.gz", + "indexURL": "https://jbrowse.southgreen.fr/citrus_maxima_cv_wanbaiyou_v1/gff3/Citrus_maxima_cv_Wanbaiyou.gff3.gz.tbi", "order": 0 } ] } ] + From 939dbcafc2303b351616b89985579711cac81530 Mon Sep 17 00:00:00 2001 From: GuilhemSempere Date: Fri, 13 Mar 2026 09:17:36 +0100 Subject: [PATCH 03/10] Staging (#175) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * SnpEff integration prototyping * Semi-functional prototype snpEff interface * SnpEff prototype — annotation successful * Reorganization * Switch to a serious implementation * Add an interface for SnpEff annotation * Fix annotation config and interface * Add proper SnpEff configuration * Beqin SnpEff database management * Start snpEff database download * Add automatic genome download * Add snpEff genome installation * Add online building of SnpEff database * Minor change * fix: default genome download seems ok but 1sec download genome can crash * fix: snpeff upload from files seems ok * fix: snpeff import ok + start seems ok * Improvements on SnpEff integration * Minor fix * Split up shared code from main.js into common.js (#131) * Embed Oracle JDK instead of Zulu JRE into bundles * Now averaging cumulated VCF field values on #variants per interval * Finalized calculation of VCF numeric field series as averaged ... rather than cumulated, as it used to be. * Fixed VCF field series removal ... also set MongoDB password via placeholder in applicationContext-data.xml, to avoiding pushing passwords to source control * Merge from master (#139) * Update build.yml * Reverted back to original build.sh * Added missing file (possibly forgot to commmit/push it) (#136) * added python build file to get dynamically current pom.xml dependencies, add build.sh deprecated and improve current github workflow (#138) Co-authored-by: Dorian Grasset --------- Co-authored-by: Dorian Grasset * Added support for user email addresses * Displaying imports in admin process list (#140) * feat(execution-message): update GigwaModuleManager * Finalized inclusion of import processes in admin-process list * Finalized display of imports in admin process list --------- Co-authored-by: Dorian Grasset * Feat/change password (#141) * feat(change-password): added a menu for logged in users to change password * feat(change-password): send mail code * feat(change-password): add new services * feat(change-password): feature implemented * feat(change-password): handle password > 20 characters error and update email message * feat(change-password): put reset code logic in session * feat(change-password): add enforcedWebapRootUrl var * Sorted out a few details on password override functionality --------- Co-authored-by: Dorian Grasset * Merged password recovery feature * Fixed default JavaMailSenderImpl config: no credentials if auth disabled * Made dumpFolder checking more robust (10sec timeout) * Minor fixes / changes * Suggest user to enter an e-mail address if reset functionality is active * POM.xml version update * Added HTML version to password reset e-mail (+ skipped for SSO users) * Better handle security exceptions * Simplified XML context file loading + enabled global-method-security * Update build-staging.yml * Added forgotten class * Made online output tools available for async-watched exports * Fixed login issue (redirection to administration was remaining active) * Minor fix * add function to check if the user can edit calls and display snpclust button also for reader role * Fixed import process cleanup * Merge from master (#145) * Update build.yml * Reverted back to original build.sh * Added missing file (possibly forgot to commmit/push it) (#136) * added python build file to get dynamically current pom.xml dependencies, add build.sh deprecated and improve current github workflow (#138) Co-authored-by: Dorian Grasset * Update genomes.json Add 4 new genomes for Citrus medica, C. micrantha, C. maxima and C. reticulata --------- Co-authored-by: Dorian Grasset Co-authored-by: droc * Export optim (#146) * DDL is now fake (deleting file after 1 hour) This is a counterpart for removing the need for very long timeout settings * Made IGV data retrieval compatible with multithreaded export * Parallelized export functional IGV display * Minor change * IGV export is now handled via HapmapExportHandler * Minor changes * Made export file cleanup happen periodically * Minor changes * Minor change * Fixed slight interface issues * Enabled AutoUnzipFilter for DDL exports * Enhanced export flexibility regarding ability to push to external tools * Changed project version * Dynamically accounting for changes in custom output tool configuration * Minor UI fix * Minor UI changes * Fixed bug in reloading saved trait settings * Worked around filtering by IDs disabling variant type filter This is causing issues for exporting into formats that don't support all variant types * Reverted accidental commit * Provided means to rearrange metadata columns' order * Merge from master (#150) * Update build.yml * Reverted back to original build.sh * Added missing file (possibly forgot to commmit/push it) (#136) * added python build file to get dynamically current pom.xml dependencies, add build.sh deprecated and improve current github workflow (#138) Co-authored-by: Dorian Grasset * Update genomes.json Add 4 new genomes for Citrus medica, C. micrantha, C. maxima and C. reticulata --------- Co-authored-by: Dorian Grasset Co-authored-by: droc * Corrected css file contents * Provided means to set DB taxonomy thru back-office * Fixed bug in switching from species to another level * Minor fix * Merged Staging & Custom look (#155) * Added means to customize look * Merge from master (#114) * Moved standalone IGV download link to "Online output tools" dialog * added OAuth token compatibility for API requests, based on an OIDC discovery file (#112) Co-authored-by: GuilhemSempere * Revert "added OAuth token compatibility for API requests, based on an OIDC discovery file (#112)" This reverts commit a899c7fe718bea4ddd5507136b87c5ab53e840bf. --------- Co-authored-by: Peter Selby <32845555+BrapiCoordinatorSelby@users.noreply.github.com> * Merge from master (#119) * Moved standalone IGV download link to "Online output tools" dialog * added OAuth token compatibility for API requests, based on an OIDC discovery file (#112) Co-authored-by: GuilhemSempere * Revert "added OAuth token compatibility for API requests, based on an OIDC discovery file (#112)" This reverts commit a899c7fe718bea4ddd5507136b87c5ab53e840bf. * Caching unzoomed results for fast zoom resets ... also reduced dot size and set default number of intervals to 1000 * Fixed dialog height (set to maximum for all) --------- Co-authored-by: Peter Selby <32845555+BrapiCoordinatorSelby@users.noreply.github.com> --------- Co-authored-by: Peter Selby <32845555+BrapiCoordinatorSelby@users.noreply.github.com> * Revert "Merged Staging & Custom look (#155)" This reverts commit 984577452c1057abeda80ff3d7a2e6836285768a. * override listReadablesDb * Fixed position range paste issue (stripping out non-digits) * POM version update * Fixed minor UI issue * Custom look (#156) * Provided means to customize the UI (Windows update script not tested yet) * Fixed Windows update script * Fixed export file cleanup rate * Fixed missing header in IGV data table when no markers in displayed area * Fixed scrollbar bugs, optimized initial genotype loading, ... * Minor UI fix * Handle uploaded files while HTTP request is valid * Minor UI fix * Accounted for newly added role, allowing to create DBs * Removed forbidMongoDiskUse config parameter * Moved VisualizationService class from Gigwa2ServiceImpl to Mgdb2Export * Minor UI fix * Minor UI improvements * Minor UI improvements * Fixed startup bug occurring when no trait data available * Minor change * Sorted out dodgy import UI behaviour when typing an existing DB's name * Only check authorities if not anonymous * Documentation update for v2.9 * Added screenshot to documentation * Update README.md (#158) (#159) * Skipping Unit Tests whein building staging vesion * Persist skip-monomorphic-variant-import-box state into localStorage * Made it possible to push to Galaxy even when running on localhost * Changed version * Re-commitiing omitted file * Keeping vcf.gz compressed on Galaxy side when not running on localhost * Merge from master (#161) * Update README.md (#158) * Update genomes.json add Leccino genome (#160) --------- Co-authored-by: droc * Making UI able to work with samples instead of individuals * Removed accidentally added parameters * Moving towards making UI & exports support working with samples * Fixed 2 small startup bugs * Made VizService compatible with detached samples * Made IGV data export compatible with detached samples * Minor fix * Only showing "Work on samples" checkbox when appropriate * Persisted workWithSamples status in localStorage * Made password reset work without using the HttpSession * Minor change * Import refactoring and Split Sample into 2 new collections : IndSample and CallSets * Remove pj and rn fields from GenotypingSample and update queries to get callsets from individuals * Add workWithSample param in getQuery, and bookmark * Add missing update from previous commit * Implementing multi-project UI + addition of callset entity * Further integrating multi-projet UI + callSet level addition * Fixed some issues in displaying genotypes by individual or sample * Minor fix * Made BrAPI v1 import compatible with Gigwa CallSets * Minor fix * Minor change * Update Terminology correspondence table for APIs * Minor fix * CallSets now embedded in Samples * Fixing errors due to addition of CallSet level * add config parameters for allelematrixSearch limit pageSize * Ready for testing * Minor changes * Removed unused parameter * Minor changes * Fixed chart sequence list * fix getting metadata from several projects * fix tests error when cleaning after testing * fix call distinctSampleMetadata with list of projects * Fixed remaining issues in handling multiple project IDs * Sort genotype table by individual, then project, then run * Implemented fix for accidentally published "internal" config properties * Corrected "internal" trackerUrl value * Minor fix * Added a 5 sec timeout to the attempts of listing DB dump files * Minor change * Set a limit (10) to number of simultaneously displayed metadata fields * Save workWithSample in localStorage depending on DB * Show also individuals metadata when loading samples metadata * Minor change * Minor change * Account for metadate field value selection only when closing dropdown * Implemented example MD file download, supporting mandatory fields * Minor change * Mentioning mandatory fields in example metadata files (+ minor fix) * Added support for ignoring comment lines in metadata files * Minor fixes * new: mandatoryIndividualMetadata-DB_NAME mandatorySampleMetadata-DB_NAME * Now supporting global (not only per-DB) mandatory metadata fields * Minor change * Updated documentation to match v2.10 features * Minor change * Fixed filter object structure for sample's individual metadata in graphs * Minor change * Added support for description for mandatory metadata fields * Updated documentation to mention mandataory metadata fields * Minor change * Minor change * Sped up metadata distinct values loading when used as Fst groups * Updated new param description & example values * Minor fixes * Provided means to ask for confirmation before switching module to public Also made dirtiness handling more clever * Added termsToAcceptToMakeDatabasePublic config parameter * For mandatory metadata fields prefixed with *, require non-blank values * Reduced project info box z-index * Reworked Terms Of Use dialog display - cookie duration now configurable - cookie expiration is no more pushed back while the system is being used (delay counting now strictly starts when the agreement button is clicked) - dialog can be displayed explicitly via a link on the homepage - dialog contents can be customized by appending HTML defined via a config property * Updated version number * Fixed minor UI bug * Updated config.properties * Removed useless dependency * Minor fix * Minor change * Minor changes in chart UI to be compatible with WIDDE * IntertekImport - map alleleX and alleleY on knownAlleles if variant exist in database. * Integrated smart-color-multiselect for Fst group selection * Fixed issues handling groups for charts * Added heterozygosity chart support * Minor changes * Minor change * Now using newly created ExportHelper class to provide tool URLs * Update dependencies and Tomcat version in pom.xml * Merge branch 'staging' of https://github.com/SouthGreenPlatform/Gigwa2.git into snpEff * Minor fix * Minor changes * Version update * Added UI in admin to fix VRD records with missing known alleles * Added missing data graph * Changed ref allele bgcolor in variant details popup * Minor change * Minor documentation change * Fix importing metadata on samples * updated MANIFEST * Refactored numbering of items in Terms Of Use box * Update genomes.json (#173) (#174) Co-authored-by: droc * Removed commented-out code * Fixed issue in grouping bio-entities by color for Fst graph * Added means to increase the graph width * Manage cookie consent properly via checkbox * Fixed minor issues * POM update * Version update * Reintroduced lost change (fix for supporting metadata fields with trailing spaces) --------- Co-authored-by: Grégori Mignerot Co-authored-by: Yoan BIGGIO Co-authored-by: Dorian Grasset Co-authored-by: Dorian Grasset Co-authored-by: Alice Boizet Co-authored-by: droc Co-authored-by: Peter Selby <32845555+BrapiCoordinatorSelby@users.noreply.github.com> --- docker-compose.yml | 2 +- misc/linux_bundle.sh | 2 +- misc/macos_bundle.command | 2 +- misc/win_bundle.ps1 | 2 +- pom.xml | 6 +- .../controller/gigwa/GigwaRestController.java | 22 +- src/main/resources/config.default | 2 +- src/main/webapp/META-INF/MANIFEST.MF | 2 +- src/main/webapp/css/main.css | 16 ++ src/main/webapp/docs/gigwa_docs.html | 2 +- src/main/webapp/index.jsp | 67 +++++- src/main/webapp/js/charts.js | 199 +++++++++++++----- src/main/webapp/navbar.jsp | 45 ++-- 13 files changed, 259 insertions(+), 110 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 51dd4130..244cf303 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: tomcat: depends_on: - mongo - image: guilhemsempere/gigwa:2.11-RELEASE + image: guilhemsempere/gigwa:2.12-RELEASE ports: - 8080:8080 # You may amend the external port only (left hand side). By default, webapp URL will be http://host.ip:8080/gigwa restart: always diff --git a/misc/linux_bundle.sh b/misc/linux_bundle.sh index 42a0bbee..eb475a21 100644 --- a/misc/linux_bundle.sh +++ b/misc/linux_bundle.sh @@ -1,7 +1,7 @@ #!/bin/bash # Variables -project_version="2.11-RELEASE" +project_version="2.12-RELEASE" tomcat_version="9.0.113" mongodb_linux_file="linux-x86_64-ubuntu1804-4.2.25" path_to_ubuntu_jre="zulu17.40.19-ca-jre17.0.6-linux_x64.tar.gz" diff --git a/misc/macos_bundle.command b/misc/macos_bundle.command index 7cc39d42..908192f7 100644 --- a/misc/macos_bundle.command +++ b/misc/macos_bundle.command @@ -1,7 +1,7 @@ #!/bin/bash # Variables -project_version="2.11-RELEASE" +project_version="2.12-RELEASE" tomcat_version="9.0.113" mongodb_osx_file="macos-x86_64-4.2.25" path_to_osx_jre="zulu17.40.19-ca-jre17.0.6-macosx_x64.tar.gz" diff --git a/misc/win_bundle.ps1 b/misc/win_bundle.ps1 index d6371474..857ac3c5 100644 --- a/misc/win_bundle.ps1 +++ b/misc/win_bundle.ps1 @@ -1,5 +1,5 @@ # Variables -$project_version = "2.11-RELEASE" +$project_version = "2.12-RELEASE" $tomcat_version = "9.0.113" $mongodb_windows_file = "win32-x86_64-2012plus-4.2.25" $path_to_windows_jre = "zulu17.40.19-ca-jre17.0.6-win_x64" diff --git a/pom.xml b/pom.xml index da3e3e50..d05b7bc8 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 - 2.11-RELEASE + 2.12-RELEASE ${project.build.directory}/endorsed UTF-8 ${project.build.directory}/bundles @@ -52,7 +52,7 @@ fr.cirad Mgdb2BrapiV2Impl - 2.1.1-RELEASE + 2.1.2-RELEASE org.slf4j @@ -77,7 +77,7 @@ fr.cirad Gigwa2ServiceImpl - 2.11-RELEASE + 2.12-RELEASE com.fasterxml.jackson.core diff --git a/src/main/java/fr/cirad/web/controller/gigwa/GigwaRestController.java b/src/main/java/fr/cirad/web/controller/gigwa/GigwaRestController.java index 51f1f068..6c95c1c7 100644 --- a/src/main/java/fr/cirad/web/controller/gigwa/GigwaRestController.java +++ b/src/main/java/fr/cirad/web/controller/gigwa/GigwaRestController.java @@ -1429,27 +1429,7 @@ public ModelAndView setupImportPage() return ""; } - -// @ApiIgnore -// @ApiOperation(authorizations = { @Authorization(value = "AuthorizationToken") }, value = germplasmWithBrapiMappingURL, notes = "Lists IDs of germplasm with external reference source & ID") -// @GetMapping(value = BASE_URL + germplasmWithBrapiMappingURL) -// public @ResponseBody Collection germplasmWithBrapiMappingURL(HttpServletRequest request, @RequestParam("module") final String sModule) { -// return MgdbDao.getInstance().loadIndividualsWithAllMetadata(sModule, AbstractTokenManager.getUserNameFromAuthentication(tokenManager.getAuthenticationFromToken(tokenManager.readToken(request))), null, null) -// .values().stream() -// .filter(ind -> ind.getAdditionalInfo().get(BrapiService.BRAPI_FIELD_externalReferenceSource) != null && ind.getAdditionalInfo().get(BrapiService.BRAPI_FIELD_externalReferenceId) != null) -// .map(ind -> sModule + Helper.ID_SEPARATOR + ind.getId()).toList(); -// } -// -// @ApiIgnore -// @ApiOperation(authorizations = { @Authorization(value = "AuthorizationToken") }, value = samplesWithBrapiMappingURL, notes = "Lists IDs of samples with external reference source & ID") -// @GetMapping(value = BASE_URL + samplesWithBrapiMappingURL) -// public @ResponseBody Collection samplesWithBrapiMappingURL(HttpServletRequest request, @RequestParam("module") final String sModule) { -// return MgdbDao.getInstance().loadSamplesWithAllMetadata(sModule, AbstractTokenManager.getUserNameFromAuthentication(tokenManager.getAuthenticationFromToken(tokenManager.readToken(request))), null, null) -// .values().stream() -// .filter(sp -> sp.getAdditionalInfo().get(BrapiService.BRAPI_FIELD_externalReferenceSource) != null && sp.getAdditionalInfo().get(BrapiService.BRAPI_FIELD_externalReferenceId) != null) -// .map(sp -> sModule + Helper.ID_SEPARATOR + sp.getId()).toList(); -// } -// + private HashMap getImportFilesByExtension(Collection importFiles, Collection filesSpecifiedByURI) throws Exception{ HashMap filesByExtension = new HashMap<>(); HashMap synonymExtensions = new HashMap() {{ put("csv", "tsv"); put("phenotype", "tsv"); }}; // .phenotype is only synonym with tsv diff --git a/src/main/resources/config.default b/src/main/resources/config.default index b9727ae2..087f83c1 100644 --- a/src/main/resources/config.default +++ b/src/main/resources/config.default @@ -156,4 +156,4 @@ allowedOrigins_/**=* maxPcaMatrixSize = 1E9 # Max dimension of the genotyping matrix (#samples x #SNPs) to calculate distance matrix on. Default is 1 billion (1E9) -maxDistanceMatrixSize = 1E9 \ No newline at end of file +maxDistanceMatrixSize = 1E9 diff --git a/src/main/webapp/META-INF/MANIFEST.MF b/src/main/webapp/META-INF/MANIFEST.MF index 4de16485..b8e267ee 100644 --- a/src/main/webapp/META-INF/MANIFEST.MF +++ b/src/main/webapp/META-INF/MANIFEST.MF @@ -1,6 +1,6 @@ Manifest-Version: 1.0 Build-Jdk-Spec: 21 Created-By: Maven Integration for Eclipse -Implementation-version: 2.11-RELEASE +Implementation-version: 2.12-RELEASE Class-Path: diff --git a/src/main/webapp/css/main.css b/src/main/webapp/css/main.css index 2e9bec0c..03b34304 100644 --- a/src/main/webapp/css/main.css +++ b/src/main/webapp/css/main.css @@ -183,6 +183,22 @@ div#termsOfUse .modal-lg { width: 950px; } +div#termsOfUse ol { + list-style-type: none; + padding-left: 0; + counter-reset: li-counter; +} + +div#termsOfUse ol > li { + counter-increment: li-counter; + margin-bottom: 0.5em; +} + +div#termsOfUse ol > li h4::before { + content: counter(li-counter) ") "; + margin-right: 0.2em; +} + div#chartDialog .modal-lg, div#individualFiltering .modal-lg, div#manual .modal-lg, div#variantDetailPanel .modal-lg, div#fjBytesPanel .modal-lg, div#genomeBrowserPanel .modal-lg, div#igvPanel .modal-lg { width: 99%; } diff --git a/src/main/webapp/docs/gigwa_docs.html b/src/main/webapp/docs/gigwa_docs.html index f71810fe..ff182fa3 100644 --- a/src/main/webapp/docs/gigwa_docs.html +++ b/src/main/webapp/docs/gigwa_docs.html @@ -13,7 +13,7 @@

- Gigwa v2.11.x – Documentation + Gigwa v2.12.x – Documentation

Learn how to use Gigwa like a pro in a few minutes! diff --git a/src/main/webapp/index.jsp b/src/main/webapp/index.jsp index 989e043f..a4879098 100644 --- a/src/main/webapp/index.jsp +++ b/src/main/webapp/index.jsp @@ -1408,7 +1408,7 @@ https://doi.org/10.1093/gigascience/giz051 headerRow.append("" + (workWithSamples ? "Samples" : "Individual") + ""); for (var i in callSetMetadataFields) { headerRow.append("

" + callSetMetadataFields[i] + "
"); - exportedMetadataSelectOptions += ""; + exportedMetadataSelectOptions += ""; } $("#exportedIndividualMetadata").html(exportedMetadataSelectOptions); @@ -1431,7 +1431,7 @@ https://doi.org/10.1093/gigascience/giz051 let mdSelected = displayedMD.size == 0 || displayedMD.has(field); if (mdSelected) selectedMdCount++; - return "" + field + ""; + return "" + field + ""; }).join("")).selectpicker('refresh'); $("#displayedMetadataSelectionDiv").css("display", options.length <= $("#maxShownFields").text() ? "none" : "block"); @@ -2955,12 +2955,65 @@ https://doi.org/10.1093/gigascience/giz051 - +<%-- --%> diff --git a/src/main/webapp/js/charts.js b/src/main/webapp/js/charts.js index 36ac78cb..dc03d56e 100644 --- a/src/main/webapp/js/charts.js +++ b/src/main/webapp/js/charts.js @@ -39,7 +39,18 @@ async function chartIndSelectionChanged() { $("input.showHideSeriesBox:checked").prop("checked", false); } let usingSmartSelect = typeof $('select[multiple]#plotGroupingMetadataValues').smartColorMultiSelect !== 'undefined'; - let groups = usingSmartSelect ? $("#plotGroupingMetadataValues").smartColorMultiSelect('getColorGroups') : selectedValues; // might be different in case of Fst + let groups; + if (usingSmartSelect) { + let rawGroups = $("#plotGroupingMetadataValues").smartColorMultiSelect('getColorGroups'); + if (Array.isArray(rawGroups)) + groups = rawGroups.map(g => Array.isArray(g) ? g : [g]); + else if (rawGroups && typeof rawGroups === "object") + groups = Object.values(rawGroups); + else + groups = selectedValues.map(v => [v]); + } + else + groups = selectedValues.map(v => [v]); let minRequiredGroups = currentChartType == "fst" ? 2 : currentChartType == null || (currentChartType == "density" && $("input.showHideSeriesBox:checked").length == 0 ? 0 : 1); $('#indSelectionCount').html(" "); @@ -50,41 +61,47 @@ async function chartIndSelectionChanged() { let selectedIndividuals = getSelectedIndividuals(); // Create array of promises for all AJAX calls - const ajaxPromises = selectedValues.map((selectedValue, i) => { - var filters = {}; - if (workWithSamples) { - let filterVals = {}; - filterVals[groupOption.startsWith("ind.") ? groupOption.substring(4) : groupOption] = [selectedValue]; - filters[groupOption.startsWith("ind.") ? "individual" : "sample"] = filterVals; - } - else - filters[groupOption] = [selectedValue]; - - return $.ajax({ - url: (workWithSamples ? filterSamplesUsingMetadata : filterIndividualsUsingMetadata) + '/' + getChartModule() + "?projIDs=" + getProjectId().map(id => id.substring(1 + id.lastIndexOf(idSep))).join(","), - type: "POST", - contentType: "application/json;charset=utf-8", - headers: { ...buildHeader(token, $('#assembly').val(), $('#workWithSamples').is(':checked')), "excludeMetadata": true }, - data: JSON.stringify(filters) - }).then(callSetResponse => ({ index: i, response: callSetResponse })); + const ajaxPromises = []; + groups.forEach((groupValues, groupIndex) => { + groupValues.forEach(value => { + var filters = {}; + if (workWithSamples) { + let filterVals = {}; + filterVals[groupOption.startsWith("ind.") ? groupOption.substring(4) : groupOption] = [value]; + filters[groupOption.startsWith("ind.") ? "individual" : "sample"] = filterVals; + } + else + filters[groupOption] = [value]; + + ajaxPromises.push( + $.ajax({ + url: (workWithSamples ? filterSamplesUsingMetadata : filterIndividualsUsingMetadata) + '/' + getChartModule() + "?projIDs=" + getProjectId().map(id => id.substring(1 + id.lastIndexOf(idSep))).join(","), + type: "POST", + contentType: "application/json;charset=utf-8", + headers: { ...buildHeader(token, $('#assembly').val(), $('#workWithSamples').is(':checked')), "excludeMetadata": true }, + data: JSON.stringify(filters) + }).then(resp => ({ + groupIndex: groupIndex, + response: resp + })) + ); + }); }); // Wait for all requests to complete Promise.all(ajaxPromises).then(results => { - // Process results in order - results.forEach(({ index, response }) => { - if (index > 0 && additionalCallSetIds.length < index) - additionalCallSetIds.push([]); - var targetGroup = index == 0 ? callSetIds : additionalCallSetIds[index - 1]; - - response.forEach(function(callset) { - if (selectedIndividuals.length == 0 || selectedIndividuals.includes(callset.id)) - targetGroup.push(referenceset + idSep + callset.id) - }); - }); - - // Update UI after all requests complete - updateIndSelectionCount(); + callSetIds = []; + additionalCallSetIds = []; + results.forEach(({groupIndex, response}) => { + if (groupIndex > 0 && additionalCallSetIds.length < groupIndex) + additionalCallSetIds.push([]); + let targetGroup = groupIndex === 0 ? callSetIds : additionalCallSetIds[groupIndex - 1]; + response.forEach(callset => { + if (selectedIndividuals.length === 0 || selectedIndividuals.includes(callset.id)) + targetGroup.push(referenceset + idSep + callset.id); + }); + }); + updateIndSelectionCount(); }); } else { @@ -312,7 +329,7 @@ function initializeChartDisplay() { displayName: "Fst", queryURLFunction: "getChartFstDataURL", title: "Fst value for {{displayedVariantType}} variants on sequence {{displayedSequence}}", - subtitle: "Weir and Cockerham Fst estimate calculated between selected groups in an interval of size {{intervalSize}} around each point (only accounting for bi-allelic variants)", + subtitle: "Weir and Cockerham Fst estimate calculated between selected groups and averaged in an interval of size {{intervalSize}} around each point (only accounting for bi-allelic variants)", xAxisTitle: "Positions on selected sequence", series: [{ name: "Fst estimate", @@ -395,7 +412,10 @@ function initializeChartDisplay() { } }); - $('div#chartContainer').html('
'); + $('div#chartContainer').html( + '
' + + '' + ); let selectedSequences = getChartDistinctSequenceList(), selectedTypes = getChartDistinctTypes(); feedSequenceSelectAndLoadVariantTypeList( selectedSequences == "" ? $('#Sequences').selectmultiple('option') : selectedSequences, @@ -422,8 +442,8 @@ function feedSequenceSelectAndLoadVariantTypeList(sequences, types) { '
 
' + '
' + 'Data to display: ' + - 'Choose a sequence: ' + - 'Choose a variant type: ' + + 'Sequence: ' + + 'Variant type: ' + '
'); $(headerHtml).insertBefore('div#densityChartArea'); @@ -476,9 +496,14 @@ function buildCustomisationDiv(chartInfo) { handleError(xhr, thrownError); } }); - let customisationDivHTML = "
"; + let customisationDivHTML = '
 
'; + + customisationDivHTML += "
"; customisationDivHTML += '
'; - customisationDivHTML += '

Customisation options

Number of intervals
(between 50 and 1000)'; + customisationDivHTML += '

Customisation options

Number of intervals
(between 50 and 5000)'; customisationDivHTML += '
'; customisationDivHTML += '
'; @@ -491,6 +516,37 @@ function buildCustomisationDiv(chartInfo) { $("div#chartContainer div#additionalCharts").html(customisationDivHTML + "
"); } +function updateChartWidth() { + const chartContainer = document.getElementById('densityChartArea'); + if (!chartContainer) + return; + + const containerWidth = chartContainer.clientWidth; + const requiredWidth = $('#widthMultiplier').val() * containerWidth; + + chartContainer.style.minWidth = '100%'; + chartContainer.style.overflowX = 'auto'; + chartContainer.style.whiteSpace = 'nowrap'; + + if (chart) { + chart.setSize(requiredWidth, null, false); + chart.reflow(); + // Ensure the x-axis covers the full range of data + const xAxis = chart.xAxis[0]; + xAxis.setExtremes(null, null, true, false); + // Set marginRight to 0 to ensure the plot area extends to the edge + chart.update({ + chart: { + marginRight: 0 + } + }); + } +} + +window.addEventListener('resize', function() { + updateChartWidth(); +}); + function displayOrAbort() { if (dataBeingLoaded) { abortOngoingOperation(); @@ -508,7 +564,7 @@ function getIntervalCountFromLocalStorage() { if (localStorage.getItem("intervalCount") != null) return localStorage.getItem("intervalCount"); else - return 1000; + return 1000; // default } function applyChartType() { @@ -536,8 +592,10 @@ function applyChartType() { buildCustomisationDiv(chartInfo); if (chartInfo.onLoad !== undefined) chartInfo.onLoad(); - if (currentChartType == "fst" && typeof $('select[multiple]#plotGroupingMetadataValues').smartColorMultiSelect !== 'undefined') + if (currentChartType == "fst" && typeof $('select[multiple]#plotGroupingMetadataValues').smartColorMultiSelect !== 'undefined') { $('select[multiple]#plotGroupingMetadataValues').smartColorMultiSelect(); + $("#plotGroupingMetadataValues").parent().find("button.scms-toggle-icon").on("click", chartIndSelectionChanged); + } loadChart(); } @@ -583,9 +641,9 @@ function displayChart(minPos, maxPos) { // Set the interval count until the next chart reload let tempValue = parseInt($('#intervalCount').val()); if (isNaN(tempValue)) - displayedRangeIntervalCount = 1000; - else if (tempValue > 1000) - displayedRangeIntervalCount = 1000; + displayedRangeIntervalCount = 5000; + else if (tempValue > 5000) + displayedRangeIntervalCount = 5000; else if (tempValue < 50) displayedRangeIntervalCount = 50; else @@ -655,9 +713,11 @@ function adler32(str) { function displayResult(chartInfo, jsonResult, displayedVariantType, displayedSequence) { //console.log(Object.keys(cachedResults)); - - // TODO : Key to the middle of the interval ? - chartJsonKeys = chartInfo.series.length == 1 ? Object.keys(jsonResult) : Object.keys(jsonResult[0]); + for (const key in jsonResult) // for some reason HighCharts seems to fail (showing no graph at all) when fed with too many NaN values: replace them with null + if (jsonResult[key] === "NaN" || (typeof value === 'number' && isNaN(value))) + jsonResult[key] = null; + + chartJsonKeys = chartInfo.series.length == 1 ? Object.keys(jsonResult) : Object.keys(jsonResult[0]); var intervalSize = parseInt(chartJsonKeys[1]) - parseInt(chartJsonKeys[0]); let totalVariantCount = 0; @@ -668,18 +728,32 @@ function displayResult(chartInfo, jsonResult, displayedVariantType, displayedSeq chart = Highcharts.chart('densityChartArea', { chart: { type: 'spline', + reflow: false, zoomType: 'x' }, title: { text: chartInfo.title.replace("{{totalVariantCount}}", totalVariantCount).replace("{{displayedVariantType}}", displayedVariantType).replace("{{displayedSequence}}", displayedSequence), + align: 'left', + x: 50, }, subtitle: { text: isNaN(intervalSize) ? '' : chartInfo.subtitle.replace("{{intervalSize}}", intervalSize), + align: 'left', + x: 60, + }, + legend: { + floating: true, + align: 'left', + x: 280, }, xAxis: { categories: chartJsonKeys, title: { text: chartInfo.xAxisTitle, + align: 'low', + offset: 70, + x: 500, + y: -5, }, events: { afterSetExtremes: function(e) { @@ -715,8 +789,8 @@ function displayResult(chartInfo, jsonResult, displayedVariantType, displayedSeq } }, exporting: { - enabled: true, - buttons: { + enabled: true, + buttons: { contextButton: { menuItems: ["viewFullscreen", "printChart", "separator", @@ -748,15 +822,30 @@ function displayResult(chartInfo, jsonResult, displayedVariantType, displayedSeq textArea.select(); try { document.execCommand('copy'); - console.log('Copied to clipboard: ' + text); +// console.log('Copied to clipboard: ' + text); } catch (err) { console.log('Failed to copy text to clipboard: ' + text); } document.body.removeChild(textArea); } - }] + }], + align: 'left', + verticalAlign: 'top', + x: 0, + y: 0, + floating: true, + symbol: 'menu', + symbolFill: '#666666', + symbolStroke: '#666666', + symbolStrokeWidth: 3, + theme: { + fill: 'white', + stroke: 'none', + padding: 1, + zIndex: 100 + } } - } + } } }); @@ -794,6 +883,8 @@ function displayResult(chartInfo, jsonResult, displayedVariantType, displayedSeq if (chartInfo.onDisplay !== undefined) chartInfo.onDisplay(); + + updateChartWidth(); } function addMetadataSeries(minPos, maxPos, fieldName, colorIndex) { @@ -1023,9 +1114,9 @@ function displayOrHideThreshold(isChecked) { function changeIntervalCount() { let tempValue = parseInt($('#intervalCount').val()); if (isNaN(tempValue)) - $("#intervalCount").val(1000); - else if (tempValue > 1000) - $("#intervalCount").val(1000); + $("#intervalCount").val(5000); + else if (tempValue > 5000) + $("#intervalCount").val(5000); else if (tempValue < 50) $("#intervalCount").val(50); } diff --git a/src/main/webapp/navbar.jsp b/src/main/webapp/navbar.jsp index 8e2cf6e0..903c7f11 100644 --- a/src/main/webapp/navbar.jsp +++ b/src/main/webapp/navbar.jsp @@ -92,24 +92,33 @@ From 34e7e3440123dbbc95472c6fb60c602db5323ac2 Mon Sep 17 00:00:00 2001 From: droc Date: Fri, 13 Mar 2026 15:48:04 +0100 Subject: [PATCH 04/10] Update genomes.json (#176) --- src/main/webapp/res/genomes.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/webapp/res/genomes.json b/src/main/webapp/res/genomes.json index 22fbdebc..fd819d05 100644 --- a/src/main/webapp/res/genomes.json +++ b/src/main/webapp/res/genomes.json @@ -54,24 +54,24 @@ { "id": "musa_acuminata_subsp_malaccensis_v2", "name": "Musa acuminata DH Pahang (v2.0)", - "fastaURL": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v2/seq/musa_acuminata_v2_pseudochromosome.fna", - "indexURL": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v2/seq/musa_acuminata_v2_pseudochromosome.fna.fai", + "fastaURL": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v2/seq/Musa_acuminata_subsp_malaccensis.fna", + "indexURL": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v2/seq/Musa_acuminata_subsp_malaccensis.fna.fai", "tracks": [ { "name": "Locus", "type": "annotation", "format": "gff3", "sourceType": "file", - "url": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v2/gff3/musa_acuminata_v2.gff3.gz", - "indexURL": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v2/gff3/musa_acuminata_v2.gff3.gz.tbi", + "url": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v2/gff3/Musa_acuminata_subsp_malaccensis.gff3.gz", + "indexURL": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v2/gff3/Musa_acuminata_subsp_malaccensis.gff3.gz.tbi", "order": 0 } ] }, { - "id": "musa_acuminata_subsp_malaccensis_v2", + "id": "musa_acuminata_subsp_malaccensis_v4", "name": "Musa acuminata DH Pahang (v4.3)", - "fastaURL": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v2/seq/Musa_acuminata_subsp_malaccensis.assembly.fna", + "fastaURL": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v4/seq/Musa_acuminata_subsp_malaccensis.assembly.fna", "indexURL": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v4/seq/Musa_acuminata_subsp_malaccensis.assembly.fna.fai", "tracks": [ { @@ -88,8 +88,8 @@ { "id": "musa_balbisiana_v1", "name": "Musa balbisiana DH PKW (v1.3)", - "fastaURL": "https://jbrowse.southgreen.fr/musa_balbisiana_v1/seq/Musa_balbisiana_assembly.fna", - "indexURL": "https://jbrowse.southgreen.fr/musa_balbisiana_v1/seq/Musa_balbisiana_assembly.fna.fai", + "fastaURL": "https://jbrowse.southgreen.fr/musa_balbisiana_v1/seq/Musa_balbisiana.assembly.fna", + "indexURL": "https://jbrowse.southgreen.fr/musa_balbisiana_v1/seq/Musa_balbisiana.assembly.fna.fai", "tracks": [ { "name": "Locus", From 66895f3c913b8a6a6b8577ee65d4ee0b71b2ca68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilhem=20Semp=C3=A9r=C3=A9?= Date: Fri, 13 Mar 2026 16:24:39 +0100 Subject: [PATCH 05/10] Added igvProxiedDomains config param + implemented its effects --- src/main/resources/config.default | 3 + src/main/webapp/docs/gigwa_docs.html | 3 + src/main/webapp/index.jsp | 813 ++++++++++++++++++++------- 3 files changed, 620 insertions(+), 199 deletions(-) diff --git a/src/main/resources/config.default b/src/main/resources/config.default index 087f83c1..b20e30ad 100644 --- a/src/main/resources/config.default +++ b/src/main/resources/config.default @@ -157,3 +157,6 @@ maxPcaMatrixSize = 1E9 # Max dimension of the genotyping matrix (#samples x #SNPs) to calculate distance matrix on. Default is 1 billion (1E9) maxDistanceMatrixSize = 1E9 + +# CSV list of domains (supporting wildcards) for which IGV.js will use a proxy to access genome / track files (workaround for CORS issues) +igvProxiedDomains = jbrowse.southgreen.fr \ No newline at end of file diff --git a/src/main/webapp/docs/gigwa_docs.html b/src/main/webapp/docs/gigwa_docs.html index ff182fa3..98d5c0a2 100644 --- a/src/main/webapp/docs/gigwa_docs.html +++ b/src/main/webapp/docs/gigwa_docs.html @@ -582,6 +582,9 @@
  • maxDistanceMatrixSize - Max dimension of the genotyping matrix (#samples x #SNPs) to calculate distance matrix on. Default is 1 billion (1E9)

  • +
  • +

    igvProxiedDomains - CSV list of domains (supporting wildcards) for which IGV.js will use a proxy to access genome / track files (workaround for CORS issues)

    +
  • diff --git a/src/main/webapp/index.jsp b/src/main/webapp/index.jsp index a4879098..0c7d51f4 100644 --- a/src/main/webapp/index.jsp +++ b/src/main/webapp/index.jsp @@ -15,7 +15,7 @@ * Public License V3. --%> -<%@ page language="java" session="false" contentType="text/html; charset=utf-8" pageEncoding="UTF-8" import="fr.cirad.utils.Constants,fr.cirad.mgdb.model.mongo.subtypes.AbstractVariantData,org.brapi.v2.api.ServerinfoApi,org.brapi.v2.api.ReferencesetsApi,org.brapi.v2.api.ReferencesApi,fr.cirad.web.controller.rest.BrapiRestController,fr.cirad.tools.Helper,fr.cirad.web.controller.ga4gh.Ga4ghRestController,fr.cirad.web.controller.gigwa.GigwaRestController,fr.cirad.mgdb.model.mongo.subtypes.ReferencePosition,fr.cirad.mgdb.model.mongo.maintypes.VariantData"%> +<%@ page language="java" session="false" contentType="text/html; charset=utf-8" pageEncoding="UTF-8" import="fr.cirad.utils.Constants,fr.cirad.mgdb.model.mongo.subtypes.AbstractVariantData,org.brapi.v2.api.ServerinfoApi,org.brapi.v2.api.ReferencesetsApi,org.brapi.v2.api.ReferencesApi,fr.cirad.web.controller.rest.BrapiRestController,fr.cirad.tools.Helper,fr.cirad.web.controller.ga4gh.Ga4ghRestController,fr.cirad.web.controller.IGVProxyController,fr.cirad.web.controller.gigwa.GigwaRestController,fr.cirad.mgdb.model.mongo.subtypes.ReferencePosition,fr.cirad.mgdb.model.mongo.maintypes.VariantData"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%> @@ -2397,170 +2397,251 @@ https://doi.org/10.1093/gigascience/giz051 } } - // Load a genome file by URL with the modal - function igvLoadGenomeFromURL(){ - let genomeURL = $("#igvGenomeURLInput").val().trim(); - let indexURL = $("#igvGenomeIndexURLInput").val().trim(); - - let genomeURLObject, indexURLObject; - - try { // Check whether the genome URL is valid - genomeURLObject = new URL(genomeURL); - } catch (error){ - displayMessage("Invalid genome file URL : " + genomeURL); - return; - } - - if (indexURL.length > 0){ - try { // Check whether the index URL is valid - indexURLObject = new URL(indexURL); - } catch (error){ - displayMessage("Invalid index file URL : " + indexURL); - return; - } - } - - let filename = filenameFromURL(genomeURLObject); - - // Load a JSON genome config - if (filename.endsWith(".json")){ - $.ajax({ - url: genomeURL, - type: "GET", - dataType: "json", - success: function(data) { - igvLoadJSONGenome(genomeURL, data); - }, - error: function(xhr, ajaxOptions, thrownError) { - handleError(xhr, thrownError); - } - }) - } else { // FASTA config - let genome; - if (indexURL){ - genome = { - fastaURL: genomeURL, - indexURL, - }; - } else { - genome = { - fastaURL: genomeURL, - indexed: false, - }; - } - - // Check the genome file existence beforehand by sending a HEAD request - // Configurable with igvCheckGenomeExistence - // Default behaviour of IGV is to only download the index, and throwing errors only when zoomed enough to show the genome - if (igvCheckGenomeExistence){ - $.ajax({ - url: genomeURL, - type: "HEAD", - success: function(){ - igvSwitchGenome(genome).then(igvCheckReferenceCounts); - }, - error: function(xhr, ajaxOptions, thrownError) { - if (thrownError == "" && xhr.getAllResponseHeaders() == '') - alert("Error accessing resource: " + genomeURL); - else - handleError(xhr, thrownError); - } - }); - } else { - igvSwitchGenome(genome).then(igvCheckReferenceCounts); - } - - } + "> + + // Proxy endpoint + const IGV_PROXY_ENDPOINT = ""; + + // List of domains that should go through proxy + const IGV_PROXY_DOMAINS = [ + + + '${fn:trim(domain)}', + + + ]; + + // Function to check if URL should use proxy + function shouldUseProxy(url) { + try { + const urlObj = new URL(url); + const hostname = urlObj.hostname; + + return IGV_PROXY_DOMAINS.some(domain => { + if (domain.startsWith('*.')) { + // Wildcard domain matching + const baseDomain = domain.substring(2); + return hostname.endsWith('.' + baseDomain) || hostname === baseDomain; + } + return hostname === domain; + }); + } catch (e) { + return false; + } } - // Change the current genome, create the browser if it doesn't exist - function igvSwitchGenome(genome){ - let moduleName = getModuleName(); - if (typeof genome == "string"){ // Genome ID in the default list - localStorage.removeItem("igvDefaultGenomeConfig::" + moduleName); - localStorage.setItem("igvDefaultGenome::" + moduleName, genome); - let matchingConfig = igvFlatGenomeList.find(config => config.id == genome); - if (matchingConfig){ - genome = {...matchingConfig}; // Shallow copy as we modify it later - } else { - displayMessage("Default genome " + genome + " not found"); - return; - } - } else if (typeof genome.fastaURL == "string"){ // By URL - localStorage.removeItem("igvDefaultGenome::" + moduleName); - localStorage.setItem("igvDefaultGenomeConfig::" + moduleName, JSON.stringify(genome)); - } // Impossible to save and reload with local files - - // Take the default tracks separately to ensure the alias table is build before them loading - let tracks = genome.tracks || []; - genome.tracks = []; - - let promise; - if (!igvBrowser){ - promise = igvCreateBrowser(genome); - } else { - igvVariantTracks = undefined; - igvBrowser.removeAllTracks(); - promise = igvBrowser.loadGenome(genome); - } - - return promise.then(async function (){ - // Build the alias table - let targetNames = igvBrowser.genome.chromosomeNames; - let variantPrefix = getPrefix(referenceNames); - let refNamesForNumberedContigsCount = referenceNames.filter(nm => !isNaN(nm.substring(nm.length - 1))).length; - let targetPrefixCounts = getContigPrefixes(targetNames); - let targetPrefix = ""; - for (var pfx in targetPrefixCounts) - if (pfx.toLowerCase() == "chr" || targetPrefixCounts[pfx] == refNamesForNumberedContigsCount) { - targetPrefix = pfx; -// console.log("Using " + pfx + " as contig name prefix"); - break; - } + // Function to transform URL to proxy URL if needed + function getProxyUrl(url) { + if (url && shouldUseProxy(url)) { + return IGV_PROXY_ENDPOINT + '?url=' + encodeURIComponent(url); + } + return url; + } - let variantSuffix = getSuffix(referenceNames); - let variantSuffixRegex = new RegExp(variantSuffix + "$"); - let targetSuffix = getSuffix(targetNames); - let targetSuffixRegex = new RegExp(targetSuffix + "$"); - igvGenomeRefTable = {}; - let aliasLessContigs = new Set(); - for (let target of targetNames){ // target = chromosome name in the genome file, as used by IGV - let zeroname = target.replace(targetPrefix, "").replace(targetSuffixRegex, ""); - let basename = zeroname.replace(/^0+/, ""); // Base chromosome name - zeroname = isNumeric(basename) ? basename.padStart(2, "0") : zeroname // Zero-padded 2-digits chromosome number - igvBrowser.genome.chrAliasTable[zeroname.toLowerCase()] = target; // 02 -> target - igvBrowser.genome.chrAliasTable[basename.toLowerCase()] = target; // 2 -> target - if (zeroname.toLowerCase().startsWith("chr")) - igvBrowser.genome.chrAliasTable["chr" + zeroname.toLowerCase()] = target; // chr02 -> target - if (basename.toLowerCase().startsWith("chr")) - igvBrowser.genome.chrAliasTable["chr" + basename.toLowerCase()] = target; // chr2 -> target - igvBrowser.genome.chrAliasTable[(variantPrefix + zeroname).toLowerCase()] = target; // With prefix used by variants - igvBrowser.genome.chrAliasTable[(variantPrefix + basename).toLowerCase()] = target; - igvBrowser.genome.chrAliasTable[(variantPrefix + zeroname + variantSuffix).toLowerCase()] = target; // With prefix and suffix used by variants - igvBrowser.genome.chrAliasTable[(variantPrefix + basename + variantSuffix).toLowerCase()] = target; - - // Associate the target name to the variants reference name - let gigwaContigName = referenceNames.find(ref => ref.replace(variantPrefix, "").replace(variantSuffixRegex, "").replace(/^0+/, "") == basename); - if (gigwaContigName != null) - igvGenomeRefTable[target] = gigwaContigName; - else { - aliasLessContigs.add(target); - igvGenomeRefTable[target] = target; // couldn't find it, use the provided name (better than nothing) - } - } - if (aliasLessContigs.size > 0) - console.log("Unable to find an alias for the following contigs in Gigwa sequences: " + Array.from(aliasLessContigs).join(", ")); - - // Load the default tracks - for (let trackConfig of tracks){ - await igvBrowser.loadTrack(trackConfig); - } + // Function to recursively process track URLs in any object + function processUrlsInObject(obj) { + if (typeof obj !== 'object' || obj === null) { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map(item => processUrlsInObject(item)); + } + + const processed = {}; + for (const [key, value] of Object.entries(obj)) { + if (key === 'url' || key === 'indexURL' || key === 'fastaURL') { + processed[key] = getProxyUrl(value); + } else if (key === 'source' && typeof value === 'object') { + processed[key] = processUrlsInObject(value); + } else if (typeof value === 'object') { + processed[key] = processUrlsInObject(value); + } else { + processed[key] = value; + } + } + return processed; + } + + // Load a genome file by URL with the modal + function igvLoadGenomeFromURL() { + let genomeURL = $("#igvGenomeURLInput").val().trim(); + let indexURL = $("#igvGenomeIndexURLInput").val().trim(); + + let genomeURLObject, indexURLObject; + + try { // Check whether the genome URL is valid + genomeURLObject = new URL(genomeURL); + } catch (error) { + displayMessage("Invalid genome file URL : " + genomeURL); + return; + } + + if (indexURL.length > 0) { + try { // Check whether the index URL is valid + indexURLObject = new URL(indexURL); + } catch (error) { + displayMessage("Invalid index file URL : " + indexURL); + return; + } + } + + let filename = filenameFromURL(genomeURLObject); + + // Load a JSON genome config + if (filename.endsWith(".json")) { + // For JSON configs, we need to proxy the JSON file request if needed + const jsonProxyUrl = getProxyUrl(genomeURL); + + $.ajax({ + url: jsonProxyUrl, // Use proxied URL if needed + type: "GET", + dataType: "json", + success: function(data) { + // Process the JSON data to proxy any internal URLs using the shared function + const proxiedData = processUrlsInObject(data); + igvLoadJSONGenome(genomeURL, proxiedData); + }, + error: function(xhr, ajaxOptions, thrownError) { + handleError(xhr, thrownError); + } + }); + } else { // FASTA config + // Create the genome object with ORIGINAL URLs (not proxied yet) + let genome; + if (indexURL) { + genome = { + fastaURL: genomeURL, // Keep original URL + indexURL: indexURL, // Keep original URL + }; + } else { + genome = { + fastaURL: genomeURL, // Keep original URL + indexed: false, + }; + } + + // Check the genome file existence beforehand by sending a HEAD request + // Configurable with igvCheckGenomeExistence + if (igvCheckGenomeExistence) { + // For HEAD request, use the proxy if needed + const headRequestUrl = getProxyUrl(genomeURL); + + $.ajax({ + url: headRequestUrl, + type: "HEAD", + success: function() { + // Let igvSwitchGenome handle the proxying of URLs + igvSwitchGenome(genome).then(igvCheckReferenceCounts); + }, + error: function(xhr, ajaxOptions, thrownError) { + if (thrownError == "" && xhr.getAllResponseHeaders() == '') + alert("Error accessing resource: " + genomeURL); + else + handleError(xhr, thrownError); + } + }); + } else { + // Let igvSwitchGenome handle the proxying of URLs + igvSwitchGenome(genome).then(igvCheckReferenceCounts); + } + } + } - // Add the variant tracks - await igvUpdateVariants(); - - setIgvLocusIfApplicable(); - }); + // Change the current genome, create the browser if it doesn't exist + function igvSwitchGenome(genome) { + let moduleName = getModuleName(); + + if (typeof genome == "string") { // Genome ID in the default list + localStorage.removeItem("igvDefaultGenomeConfig::" + moduleName); + localStorage.setItem("igvDefaultGenome::" + moduleName, genome); + let matchingConfig = igvFlatGenomeList.find(config => config.id == genome); + if (matchingConfig) { + // Process URLs in the matching config + genome = processUrlsInObject({...matchingConfig}); // Deep copy and process URLs + } else { + displayMessage("Default genome " + genome + " not found"); + return; + } + } else if (typeof genome.fastaURL == "string") { // By URL + // Process URLs in the genome config + genome = processUrlsInObject(genome); + localStorage.removeItem("igvDefaultGenome::" + moduleName); + localStorage.setItem("igvDefaultGenomeConfig::" + moduleName, JSON.stringify(genome)); + } // Impossible to save and reload with local files + + // Take the default tracks separately to ensure the alias table is built before them loading + let tracks = genome.tracks || []; + genome.tracks = []; + + let promise; + if (!igvBrowser) { + promise = igvCreateBrowser(genome); + } else { + igvVariantTracks = undefined; + igvBrowser.removeAllTracks(); + promise = igvBrowser.loadGenome(genome); + } + + return promise.then(async function() { + // Build the alias table + let targetNames = igvBrowser.genome.chromosomeNames; + let variantPrefix = getPrefix(referenceNames); + let refNamesForNumberedContigsCount = referenceNames.filter(nm => !isNaN(nm.substring(nm.length - 1))).length; + let targetPrefixCounts = getContigPrefixes(targetNames); + let targetPrefix = ""; + for (var pfx in targetPrefixCounts) + if (pfx.toLowerCase() == "chr" || targetPrefixCounts[pfx] == refNamesForNumberedContigsCount) { + targetPrefix = pfx; + // console.log("Using " + pfx + " as contig name prefix"); + break; + } + + let variantSuffix = getSuffix(referenceNames); + let variantSuffixRegex = new RegExp(variantSuffix + "$"); + let targetSuffix = getSuffix(targetNames); + let targetSuffixRegex = new RegExp(targetSuffix + "$"); + igvGenomeRefTable = {}; + let aliasLessContigs = new Set(); + for (let target of targetNames) { // target = chromosome name in the genome file, as used by IGV + let zeroname = target.replace(targetPrefix, "").replace(targetSuffixRegex, ""); + let basename = zeroname.replace(/^0+/, ""); // Base chromosome name + zeroname = isNumeric(basename) ? basename.padStart(2, "0") : zeroname // Zero-padded 2-digits chromosome number + igvBrowser.genome.chrAliasTable[zeroname.toLowerCase()] = target; // 02 -> target + igvBrowser.genome.chrAliasTable[basename.toLowerCase()] = target; // 2 -> target + if (zeroname.toLowerCase().startsWith("chr")) + igvBrowser.genome.chrAliasTable["chr" + zeroname.toLowerCase()] = target; // chr02 -> target + if (basename.toLowerCase().startsWith("chr")) + igvBrowser.genome.chrAliasTable["chr" + basename.toLowerCase()] = target; // chr2 -> target + igvBrowser.genome.chrAliasTable[(variantPrefix + zeroname).toLowerCase()] = target; // With prefix used by variants + igvBrowser.genome.chrAliasTable[(variantPrefix + basename).toLowerCase()] = target; + igvBrowser.genome.chrAliasTable[(variantPrefix + zeroname + variantSuffix).toLowerCase()] = target; // With prefix and suffix used by variants + igvBrowser.genome.chrAliasTable[(variantPrefix + basename + variantSuffix).toLowerCase()] = target; + + // Associate the target name to the variants reference name + let gigwaContigName = referenceNames.find(ref => ref.replace(variantPrefix, "").replace(variantSuffixRegex, "").replace(/^0+/, "") == basename); + if (gigwaContigName != null) + igvGenomeRefTable[target] = gigwaContigName; + else { + aliasLessContigs.add(target); + igvGenomeRefTable[target] = target; // couldn't find it, use the provided name (better than nothing) + } + } + if (aliasLessContigs.size > 0) + console.log("Unable to find an alias for the following contigs in Gigwa sequences: " + Array.from(aliasLessContigs).join(", ")); + + // Load the default tracks (their URLs were already processed earlier) + for (let trackConfig of tracks) { + // Process any nested URLs in track config (in case tracks weren't processed yet) + const processedTrackConfig = processUrlsInObject(trackConfig); + await igvBrowser.loadTrack(processedTrackConfig); + } + + // Add the variant tracks + await igvUpdateVariants(); + + setIgvLocusIfApplicable(); + }); } // Alert the user if the number of sequences do not match by a given ratio @@ -2573,6 +2654,62 @@ https://doi.org/10.1093/gigascience/giz051 } } + // Load a track with a track config + function igvLoadTrack(trackConfig) { + if (!igvBrowser) { + displayMessage("Please load a genome first"); + return Promise.reject("No genome loaded"); + } + + console.log('=== igvLoadTrack called ==='); + console.log('Original track config:', JSON.stringify(trackConfig, null, 2)); + + // Check if URLs should be proxied + if (trackConfig.url) { + console.log('Track URL - should proxy?', shouldUseProxy(trackConfig.url)); + console.log('Track URL - proxied:', getProxyUrl(trackConfig.url)); + } + if (trackConfig.indexURL) { + console.log('Index URL - should proxy?', shouldUseProxy(trackConfig.indexURL)); + console.log('Index URL - proxied:', getProxyUrl(trackConfig.indexURL)); + } + + // Process URLs in the track config using the shared utility + const processedTrackConfig = processUrlsInObject(trackConfig); + console.log('Processed track config (with proxy):', JSON.stringify(processedTrackConfig, null, 2)); + + // Double-check indexURL specifically + if (processedTrackConfig.indexURL) { + console.log('Final indexURL being sent to IGV:', processedTrackConfig.indexURL); + + // Verify it's using the proxy if needed + if (shouldUseProxy(trackConfig.indexURL) && + !processedTrackConfig.indexURL.includes(IGV_PROXY_ENDPOINT)) { + console.warn('Index URL should be proxied but is not!'); + // Force proxy the index URL + processedTrackConfig.indexURL = getProxyUrl(trackConfig.indexURL); + console.log('Forced proxied indexURL:', processedTrackConfig.indexURL); + } + } + + // Load the track with proper error handling + return igvBrowser.loadTrack(processedTrackConfig) + .then(function(track) { + console.log('Track loaded successfully:', track); + displayMessage("Track loaded successfully: " + (track.name || trackConfig.name)); + + if (processedTrackConfig.format === 'vcf' || processedTrackConfig.type === 'variant') { + return igvUpdateVariants(); + } + return track; + }) + .catch(function(error) { + console.error('IGV track loading error:', error); + displayMessage("Error loading track: " + (error.message || error)); + throw error; + }); + } + // Load a track from a file with the modal function igvLoadTrackFromFile(){ let trackFile = $("#igvTrackFileInput").get(0).files[0]; @@ -2593,47 +2730,325 @@ https://doi.org/10.1093/gigascience/giz051 } // Load a track by URL with the modal - function igvLoadTrackFromURL(){ - let trackURL = $("#igvTrackURLInput").val().trim(); - let indexURL = $("#igvTrackIndexURLInput").val().trim(); - - let filename; - try { // Check whether the file URL is valid - filename = filenameFromURL(new URL(trackURL)); // Get the file name from the given URL - } catch (error){ - displayMessage("Invalid track file URL : " + trackURL); - return; - } - - if (indexURL.length > 0){ - try { // Check whether the index URL is valid - indexURLObject = new URL(indexURL); - } catch (error){ - displayMessage("Invalid index file URL : " + indexURL); - return; - } - } - - let trackConfig = { - name: filename, - removable: true, - }; - if (indexURL){ - trackConfig.url = trackURL; - trackConfig.indexURL = indexURL; - } else { - trackConfig.url = trackURL; - trackConfig.indexed = false; - } - - igvLoadTrack(trackConfig); + function igvLoadTrackFromURL() { + let trackURL = $("#igvTrackURLInput").val().trim(); + let indexURL = $("#igvTrackIndexURLInput").val().trim(); + + console.log('=== Loading track from URL ==='); + console.log('Original track URL:', trackURL); + console.log('Original index URL:', indexURL || 'none'); + + // Check proxy status + console.log('Should proxy track?', shouldUseProxy(trackURL)); + console.log('Proxied track URL:', getProxyUrl(trackURL)); + if (indexURL) { + console.log('Should proxy index?', shouldUseProxy(indexURL)); + console.log('Proxied index URL:', getProxyUrl(indexURL)); + } + + let filename; + let trackURLObject; + + try { + trackURLObject = new URL(trackURL); + filename = filenameFromURL(trackURLObject); + console.log('Filename:', filename); + } catch (error) { + displayMessage("Invalid track file URL : " + trackURL); + return; + } + + if (indexURL.length > 0) { + try { + new URL(indexURL); // Just validate, don't need to store + } catch (error) { + displayMessage("Invalid index file URL : " + indexURL); + return; + } + } + + // Detect format from filename + let format = detectTrackFormat(filename); + console.log('Detected format:', format); + + let trackConfig = { + name: filename, + removable: true, + format: format, + type: getTrackTypeFromFormat(format) + }; + + if (indexURL) { + trackConfig.url = trackURL; + trackConfig.indexURL = indexURL; + console.log('Using indexed track with URL and indexURL'); + } else { + trackConfig.url = trackURL; + trackConfig.indexed = false; + console.log('Using unindexed track'); + } + + console.log('Track config:', JSON.stringify(trackConfig, null, 2)); + + // Verify files are accessible before loading - USING JQUERY PROMISE SYNTAX + verifyTrackFiles(trackConfig) + .done(function() { + console.log('All files accessible, loading track...'); + igvLoadTrack(trackConfig); + }) + .fail(function(error) { + console.error('File verification failed:', error); + displayMessage("Cannot access track files: " + (error.message || error || "Unknown error")); + }); + } + + function detectTrackFormat(filename) { + filename = filename.toLowerCase(); + if (filename.endsWith('.gff') || filename.endsWith('.gff3') || filename.includes('.gff.')) { + return 'gff3'; + } else if (filename.endsWith('.gtf')) { + return 'gtf'; + } else if (filename.endsWith('.bed')) { + return 'bed'; + } else if (filename.endsWith('.bam')) { + return 'bam'; + } else if (filename.endsWith('.vcf') || filename.endsWith('.vcf.gz')) { + return 'vcf'; + } else if (filename.endsWith('.bigwig') || filename.endsWith('.bw')) { + return 'bigwig'; + } else if (filename.endsWith('.wig')) { + return 'wig'; + } + return 'gff3'; // default + } + + function getTrackTypeFromFormat(format) { + const typeMap = { + 'gff3': 'annotation', + 'gtf': 'annotation', + 'bed': 'annotation', + 'bam': 'alignment', + 'vcf': 'variant', + 'bigwig': 'wig', + 'wig': 'wig' + }; + return typeMap[format] || 'annotation'; } // Load a track with a track config - function igvLoadTrack(config){ - if (igvBrowser) { - igvBrowser.loadTrack(config); - } + function igvLoadTrackFromURL() { + let trackURL = $("#igvTrackURLInput").val().trim(); + let indexURL = $("#igvTrackIndexURLInput").val().trim(); + + console.log('=== Loading track from URL ==='); + console.log('Original track URL:', trackURL); + console.log('Original index URL:', indexURL || 'none'); + + let filename; + let trackURLObject, indexURLObject; + + try { + trackURLObject = new URL(trackURL); + filename = filenameFromURL(trackURLObject); + console.log('Filename:', filename); + } catch (error) { + displayMessage("Invalid track file URL : " + trackURL); + return; + } + + if (indexURL.length > 0) { + try { + indexURLObject = new URL(indexURL); + console.log('Index URL object:', indexURLObject.href); + } catch (error) { + displayMessage("Invalid index file URL : " + indexURL); + return; + } + } + + // Detect format from filename + let format = detectTrackFormat(filename); + console.log('Detected format:', format); + + let trackConfig = { + name: filename, + removable: true, + format: format, + type: getTrackTypeFromFormat(format) + }; + + if (indexURL) { + // IMPORTANT: For indexed GFF3 files, IGV needs both URL and indexURL + trackConfig.url = trackURL; + trackConfig.indexURL = indexURL; + console.log('Using indexed track with URL and indexURL'); + } else { + trackConfig.url = trackURL; + trackConfig.indexed = false; + console.log('Using unindexed track'); + } + + console.log('Track config:', JSON.stringify(trackConfig, null, 2)); + + // Verify both URLs are accessible before loading + verifyTrackFiles(trackConfig).then(() => { + igvLoadTrack(trackConfig); + }).catch(error => { + displayMessage("Error accessing track files: " + error); + }); + } + + function igvLoadTrackFromURL() { + let trackURL = $("#igvTrackURLInput").val().trim(); + let indexURL = $("#igvTrackIndexURLInput").val().trim(); + + console.log('=== Loading track from URL ==='); + console.log('Original track URL:', trackURL); + console.log('Original index URL:', indexURL || 'none'); + + // Check proxy status + console.log('Should proxy track?', shouldUseProxy(trackURL)); + console.log('Proxied track URL:', getProxyUrl(trackURL)); + if (indexURL) { + console.log('Should proxy index?', shouldUseProxy(indexURL)); + console.log('Proxied index URL:', getProxyUrl(indexURL)); + } + + let filename; + let trackURLObject; + + try { + trackURLObject = new URL(trackURL); + filename = filenameFromURL(trackURLObject); + console.log('Filename:', filename); + } catch (error) { + displayMessage("Invalid track file URL : " + trackURL); + return; + } + + if (indexURL.length > 0) { + try { + new URL(indexURL); // Just validate, don't need to store + } catch (error) { + displayMessage("Invalid index file URL : " + indexURL); + return; + } + } + + // Detect format from filename + let format = detectTrackFormat(filename); + console.log('Detected format:', format); + + let trackConfig = { + name: filename, + removable: true, + format: format, + type: getTrackTypeFromFormat(format) + }; + + if (indexURL) { + trackConfig.url = trackURL; + trackConfig.indexURL = indexURL; + console.log('Using indexed track with URL and indexURL'); + } else { + trackConfig.url = trackURL; + trackConfig.indexed = false; + console.log('Using unindexed track'); + } + + console.log('Track config:', JSON.stringify(trackConfig, null, 2)); + + // Verify files are accessible before loading + verifyTrackFiles(trackConfig) + .done(function() { + console.log('All files accessible, loading track...'); + igvLoadTrack(trackConfig); + }) + .fail(function(error) { + console.error('File verification failed:', error); + displayMessage("Cannot access track files: " + (error.message || error)); + }); + } + + /** + * Verify both track and index files are accessible + * @param {object} trackConfig - Track configuration object + * @returns {jQuery.Promise} - Resolves if all files accessible + */ + function verifyTrackFiles(trackConfig) { + const deferred = $.Deferred(); + const checks = []; + + console.log('Verifying track files...'); + + // Check main track file + if (trackConfig.url) { + console.log('Checking track URL:', trackConfig.url); + checks.push( + verifyUrlAccess(trackConfig.url) + ); + } + + // Check index file if present + if (trackConfig.indexURL) { + console.log('Checking index URL:', trackConfig.indexURL); + checks.push( + verifyUrlAccess(trackConfig.indexURL) + ); + } + + // If no checks needed, resolve immediately + if (checks.length === 0) { + console.log('No files to verify'); + deferred.resolve(); + return deferred.promise(); + } + + // Use $.when to wait for all checks + $.when.apply($, checks) + .done(function() { + console.log('All track files verified successfully'); + deferred.resolve(); + }) + .fail(function(error) { + console.error('Track file verification failed:', error); + deferred.reject(error); + }); + + return deferred.promise(); + } + + /** + * Verify that a URL is accessible via HEAD request + * @param {string} url - The URL to check + * @returns {jQuery.Promise} - Resolves if accessible, rejects with error + */ + function verifyUrlAccess(url) { + const checkUrl = getProxyUrl(url); + const deferred = $.Deferred(); + + console.log('Verifying URL access:', url, 'via', checkUrl); + + $.ajax({ + url: checkUrl, + type: "HEAD", + timeout: 10000, + success: function(data, textStatus, xhr) { + console.log('URL accessible:', url, 'Status:', xhr.status); + deferred.resolve(); + }, + error: function(xhr, ajaxOptions, thrownError) { + console.error('URL not accessible:', url, 'Status:', xhr.status, thrownError); + deferred.reject({ + url: url, + status: xhr.status, + error: thrownError, + message: `Cannot access ${url} (Status: ${xhr.status})` + }); + } + }); + + return deferred.promise(); } // Create the IGV browser, provided a genome config From ec6bfcf4986d87c3d15078d9d85aeb911559fb1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilhem=20Semp=C3=A9r=C3=A9?= Date: Fri, 13 Mar 2026 16:25:48 +0100 Subject: [PATCH 06/10] Added IGVProxyController class --- .../web/controller/IGVProxyController.java | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 src/main/java/fr/cirad/web/controller/IGVProxyController.java diff --git a/src/main/java/fr/cirad/web/controller/IGVProxyController.java b/src/main/java/fr/cirad/web/controller/IGVProxyController.java new file mode 100644 index 00000000..263253c7 --- /dev/null +++ b/src/main/java/fr/cirad/web/controller/IGVProxyController.java @@ -0,0 +1,161 @@ +package fr.cirad.web.controller; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import fr.cirad.tools.AppConfig; + +@Controller +@RequestMapping(IGVProxyController.IGV_PROXY_URL) +public class IGVProxyController { + + public static final String IGV_PROXY_URL = "/igvProxy"; + + @Autowired private AppConfig appConfig; + + private List allowedDomains = null; + private List wildcardDomains = null; + + @GetMapping("/**") + public void proxyRequest( + @RequestParam("url") String targetUrl, + HttpServletRequest request, + HttpServletResponse response) throws IOException { + + if (allowedDomains == null) { + String allowedDomainsConfig = appConfig.get("igvProxiedDomains"); + allowedDomains = Arrays.stream(allowedDomainsConfig.split(",")) + .map(String::trim) + .filter(d -> !d.startsWith("*.")) + .collect(Collectors.toList()); + + wildcardDomains = Arrays.stream(allowedDomainsConfig.split(",")) + .map(String::trim) + .filter(d -> d.startsWith("*.")) + .map(d -> d.substring(2)) // Remove '*.' prefix + .collect(Collectors.toList()); + } + + // Validate the URL against whitelist + if (!isUrlAllowed(targetUrl)) { + response.setStatus(HttpStatus.FORBIDDEN.value()); + response.getWriter().write("Domain not allowed for proxying"); + return; + } + + HttpURLConnection connection = null; + try { + URL url = new URL(targetUrl); + connection = (HttpURLConnection) url.openConnection(); + + // Set method + connection.setRequestMethod(request.getMethod()); + + // CRITICAL: Forward the Range header if present + String rangeHeader = request.getHeader("Range"); + if (rangeHeader != null) + connection.setRequestProperty("Range", rangeHeader); + + // Forward other headers (selectively) + String userAgent = request.getHeader("User-Agent"); + if (userAgent != null) { + connection.setRequestProperty("User-Agent", userAgent); + } + + // Connect and get response + connection.connect(); + int responseCode = connection.getResponseCode(); + + // CRITICAL: Set proper status for partial content + if (responseCode == HttpURLConnection.HTTP_PARTIAL) { + response.setStatus(HttpStatus.PARTIAL_CONTENT.value()); + } else { + response.setStatus(responseCode); + } + + // CRITICAL: Forward Content-Range header if present + String contentRange = connection.getHeaderField("Content-Range"); + if (contentRange != null) { + response.setHeader("Content-Range", contentRange); + } + + // Forward Accept-Ranges header if present + String acceptRanges = connection.getHeaderField("Accept-Ranges"); + if (acceptRanges != null) { + response.setHeader("Accept-Ranges", acceptRanges); + } + + // Forward Content-Length if present + String contentLength = connection.getHeaderField("Content-Length"); + if (contentLength != null) { + response.setHeader("Content-Length", contentLength); + } + + // Forward Content-Type + String contentType = connection.getHeaderField("Content-Type"); + if (contentType != null) { + response.setHeader("Content-Type", contentType); + } + + // Copy response body + InputStream inputStream; + if (responseCode >= 400) + inputStream = connection.getErrorStream(); + else + inputStream = connection.getInputStream(); + + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + response.getOutputStream().write(buffer, 0, bytesRead); + } + + response.getOutputStream().flush(); + + } catch (Exception e) { + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + response.getWriter().write("Proxy error: " + e.getMessage()); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + private boolean isUrlAllowed(String url) { + try { + URL targetUrl = new URL(url); + String host = targetUrl.getHost(); + + if (allowedDomains.contains(host)) { + return true; + } + + for (String wildcardDomain : wildcardDomains) { + if (host.endsWith("." + wildcardDomain) || host.equals(wildcardDomain)) { + return true; + } + } + + return false; + } catch (MalformedURLException e) { + return false; + } + } +} \ No newline at end of file From c2fabcb4c53ef62c48d7b56343eeaffa9ae0084a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilhem=20Semp=C3=A9r=C3=A9?= Date: Fri, 13 Mar 2026 16:26:23 +0100 Subject: [PATCH 07/10] Added igvProxiedDomains to config.properties --- src/main/resources/config.properties | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/resources/config.properties b/src/main/resources/config.properties index b7b57e96..1397b81f 100644 --- a/src/main/resources/config.properties +++ b/src/main/resources/config.properties @@ -175,3 +175,6 @@ maxPcaMatrixSize = 1E8 # Max dimension of the genotyping matrix (#samples x #SNPs) to calculate distance matrix on. Default is 1 billion (1E9) maxDistanceMatrixSize = 1E8 + +# CSV list of domains (supporting wildcards) for which IGV.js will use a proxy to access genome / track files (workaround for CORS issues) +igvProxiedDomains = jbrowse.southgreen.fr From 194251b52fbb6f2fed27270cf3e911e49d6def85 Mon Sep 17 00:00:00 2001 From: droc Date: Fri, 13 Mar 2026 16:29:17 +0100 Subject: [PATCH 08/10] Update genomes.json (#177) --- src/main/webapp/res/genomes.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/webapp/res/genomes.json b/src/main/webapp/res/genomes.json index fd819d05..162bc5e2 100644 --- a/src/main/webapp/res/genomes.json +++ b/src/main/webapp/res/genomes.json @@ -54,8 +54,8 @@ { "id": "musa_acuminata_subsp_malaccensis_v2", "name": "Musa acuminata DH Pahang (v2.0)", - "fastaURL": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v2/seq/Musa_acuminata_subsp_malaccensis.fna", - "indexURL": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v2/seq/Musa_acuminata_subsp_malaccensis.fna.fai", + "fastaURL": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v2/seq/Musa_acuminata_subsp_malaccensis.assembly.fna", + "indexURL": "https://jbrowse.southgreen.fr/musa_acuminata_subsp_malaccensis_v2/seq/Musa_acuminata_subsp_malaccensis.assembly.fna.fai", "tracks": [ { "name": "Locus", @@ -113,8 +113,8 @@ "type": "annotation", "format": "gff3", "sourceType": "file", - "url": "https://jbrowse.southgreen.fr/sorghum_bicolor_BTx623_v3.1/gff3/Sorghum_bicolor_cv_BTx623.gff3.gz", - "indexURL": "https://jbrowse.southgreen.fr/sorghum_bicolor_BTx623_v3.1/gff3/Sorghum_bicolor_cv_BTx623.gff3.gz.tbi", + "url": "https://jbrowse.southgreen.fr/sorghum_bicolor_cv_btx623_v3/gff3/Sorghum_bicolor_cv_BTx623.gff3.gz", + "indexURL": "https://jbrowse.southgreen.fr/sorghum_bicolor_cv_btx623_v3/gff3/Sorghum_bicolor_cv_BTx623.gff3.gz.csi", "order": 0 } ] @@ -242,7 +242,7 @@ "id": "olea_europaea_cv_leccino_v1", "name": "Olea europaea var Leccino", "fastaURL": "https://jbrowse.southgreen.fr/olea_europaea_cv_leccino_v1/seq/Olea_europaea_cv_Leccino.assembly.fna", - "indexURL": "https://jbrowse.southgreen.fr/olea_euolea_europaea_cv_leccino_v1ropaea_cv_leccino_v1/seq/Olea_europaea_cv_Leccino.assembly.fna.fai", + "indexURL": "https://jbrowse.southgreen.fr/olea_europaea_cv_leccino_v1/seq/Olea_europaea_cv_Leccino.assembly.fna.fai", "tracks": [ { "name": "Locus", From 2a937dc34eeaf6ea6d1f698dda5f8f45bbad2268 Mon Sep 17 00:00:00 2001 From: droc Date: Fri, 13 Mar 2026 16:37:06 +0100 Subject: [PATCH 09/10] Update genomes.json (#178) --- src/main/webapp/res/genomes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/res/genomes.json b/src/main/webapp/res/genomes.json index 162bc5e2..5eeec0db 100644 --- a/src/main/webapp/res/genomes.json +++ b/src/main/webapp/res/genomes.json @@ -250,7 +250,7 @@ "format": "gff3", "sourceType": "file", "url": "https://jbrowse.southgreen.fr/olea_europaea_cv_leccino_v1/gff3/Olea_europaea_cv_Leccino.gff3.gz", - "indexURL": "https://jbrowse.southgreen.fr/olea_europaea_cv_leccino_v1/gff3/Olea_europaea_cv_Leccino.gff3.gz.tbi", + "indexURL": "https://jbrowse.southgreen.fr/olea_europaea_cv_leccino_v1/gff3/Olea_europaea_cv_Leccino.gff3.gz.csi", "order": 0 } ] From aaacfb7a5453504f936efc508a676f5b12e56e0a Mon Sep 17 00:00:00 2001 From: GuilhemSempere Date: Mon, 16 Mar 2026 10:41:05 +0100 Subject: [PATCH 10/10] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dba1b5db..0fdb0f93 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,10 @@ Gigwa aims at managing genomic and genotyping data from NGS analyses. It is a to Try Gigwa online with public datasets at https://gigwa.southgreen.fr/ -## Latest webapp and bundles are now available for download on Gigwa homepage: https://www.southgreen.fr/content/gigwa +## Check more information about Gigwa on the project homepage: +[https://www.southgreen.fr/gigwa](https://www.southgreen.fr/gigwa) -## Check the wiki: +## Check the wiki for changelog: https://github.com/SouthGreenPlatform/Gigwa2/wiki ## Developer instructions