From 21808398afebd154504607e8e2db855034dbb168 Mon Sep 17 00:00:00 2001
From: grlloyd
# prepare chart object
-C = annotation_venn_chart(
- factor_name = 'compound',
- line_colour = 'white',
- fill_colour = '.group',
+C <- annotation_venn_chart(
+ factor_name = "compound",
+ line_colour = "white",
+ fill_colour = ".group",
legend = TRUE,
labels = FALSE
)
## plot
# get all CD tables
-cd = lapply(CD,predicted)
-g1 = chart_plot(C,cd) + ggtitle('Compounds in CD per assay')
+cd <- lapply(CD, predicted)
+g1 <- chart_plot(C, cd) + ggtitle("Compounds in CD per assay")
# get all LS tables
-C$factor_name = 'LipidName'
-ls = lapply(LS,predicted)
-g2 = chart_plot(C,ls) + ggtitle('Compounds in LS per assay')
+C$factor_name <- "LipidName"
+ls <- lapply(LS, predicted)
+g2 <- chart_plot(C, ls) + ggtitle("Compounds in LS per assay")
# layout
-cowplot::plot_grid(g1,g2,nrow=2)
The diagram for CD shows the largest amount of overlap is between assays with the same ion mode.
@@ -502,63 +519,63 @@
# get all the cleaned annotation tables in one list
-all_source_tables = lapply(c(CD,LS),predicted)
+all_source_tables <- lapply(c(CD, LS), predicted)
# prepare to merge
-combine_workflow =
+combine_workflow <-
combine_sources(
source_list = all_source_tables,
- tag_ids = TRUE,
matching_columns = c(
- name = 'LipidName',
- name = 'compound',
- adduct= 'ion',
- adduct = 'LipidIon'),
- keep_cols = '.all',
- source_col = 'annotation_table',
+ name = "LipidName",
+ name = "compound",
+ adduct = "ion",
+ adduct = "LipidIon"
+ ),
+ keep_cols = ".all",
+ source_col = "annotation_table",
exclude_cols = NULL,
- tag = 'combined'
- )
+ tag = "combined"
+ )
# merge
-combine_workflow = model_apply(combine_workflow,lcms_table())
+combine_workflow <- model_apply(combine_workflow, lcms_table())
# show
predicted(combine_workflow)
-#> A "cd_source" object
-#> --------------------
-#> name: Combined annotation source
-#> description: This annotation_source object was generated by combining two other sources.
-#> input params: sheets
+#> A "annotation_source" object
+#> ----------------------------
+#> name: combined
+#> description: A source created by combining two or more sources
+#> input params: tag, data, source
#> annotations: 912 rows x 28 columns
Now that the tables have been combined we can explore the table using charts. For example, we visualise the number of annotations for each assay from both sources.
-C = annotation_pie_chart(
- factor_name = 'assay',
+C <- annotation_pie_chart(
+ factor_name = "assay",
label_rotation = FALSE,
- label_location = 'outside',
- label_type = 'percent',
- legend = TRUE ,
+ label_location = "outside",
+ label_type = "percent",
+ legend = TRUE,
centre_radius = 0.5,
- centre_label = '.total'
+ centre_label = ".total"
)
-chart_plot(C,predicted(combine_workflow)) +
- ggtitle('Annotations per assay') +
+chart_plot(C, predicted(combine_workflow)) +
+ ggtitle("Annotations per assay") +
theme(plot.margin = unit(c(1, 1.5, 1, 1.5), "cm")) +
- guides(fill=guide_legend(title="Assay"))
In this next example we compare the number of annotations from each source.
# change to plot source_name column
-C$factor_name = 'source_name'
-chart_plot(C,predicted(combine_workflow)) +
- ggtitle('Annotations per source') +
+C$factor_name <- "source_name"
+chart_plot(C, predicted(combine_workflow)) +
+ ggtitle("Annotations per source") +
theme(plot.margin = unit(c(1, 1.5, 1, 1.5), "cm")) +
- guides(fill=guide_legend(title="Source"))
# import cached results
-inchikey_cache = rds_database(
+inchikey_cache <- rds_database(
source = file.path(
- system.file('cached',package='MetMashR'),
- 'pubchem_inchikey_mtox_cache.rds'),
- .writable = FALSE
+ system.file("cached", package = "MetMashR"),
+ "pubchem_inchikey_mtox_cache.rds"
+ )
)
-id_workflow =
+id_workflow <-
pubchem_property_lookup(
- query_column = 'name',
- search_by = 'name',
- suffix = '',
- property = 'InChIKey',
- records = 'best',
+ query_column = "name",
+ search_by = "name",
+ suffix = "",
+ property = "InChIKey",
+ records = "best",
cache = inchikey_cache
)
-id_workflow = model_apply(id_workflow,predicted(combine_workflow))
# prepare cached results for vignette
-inchikey_cache2 = rds_database(
+inchikey_cache2 <- rds_database(
source = file.path(
- system.file('cached',package='MetMashR'),
- 'pubchem_inchikey_mtox_cache2.rds'),
- .writable = FALSE
+ system.file("cached", package = "MetMashR"),
+ "pubchem_inchikey_mtox_cache2.rds"
+ )
)
-inchikey_cache3 = rds_database(
+inchikey_cache3 <- rds_database(
source = file.path(
- system.file('cached',package='MetMashR'),
- 'pubchem_inchikey_mtox_cache3.rds'),
- .writable=FALSE
+ system.file("cached", package = "MetMashR"),
+ "pubchem_inchikey_mtox_cache3.rds"
+ )
)
-N = normalise_strings(
- search_column = 'name',
- output_column = 'normalised_name',
+N <- normalise_strings(
+ search_column = "name",
+ output_column = "normalised_name",
dictionary = c(
# custom dictionary
list(
# replace "NP" with "Compound NP"
- list(pattern = '^NP-',replace = 'Compound NP-'),
+ list(pattern = "^NP-", replace = "Compound NP-"),
# replace ? with NA, since this is ambiguous
- list(pattern = '?',replace = NA,fixed=TRUE),
- # remove terms in trailing brackets e.g." (ATP)"
- list(pattern = '\\ \\([^\\)]*\\)$',replace = ''),
+ list(pattern = "?", replace = NA, fixed = TRUE),
+ # remove terms in trailing brackets e.g." (ATP)"
+ list(pattern = "\\ \\([^\\)]*\\)$", replace = ""),
# replace known abbreviations
- list(pattern = '(+/-)9-HpODE',
- replace = '9-hydroperoxy-10E,12Z-octadecadienoic acid',
- fixed = TRUE),
- list(pattern = '(+/-)19(20)-DiHDPA',
- replace = '19,20-dihydroxy-4Z,7Z,10Z,13Z,16Z-docosapentaenoic acid',
- fixed = TRUE)
+ list(
+ pattern = "(+/-)9-HpODE",
+ replace = "9-hydroperoxy-10E,12Z-octadecadienoic acid",
+ fixed = TRUE
+ ),
+ list(
+ pattern = "(+/-)19(20)-DiHDPA",
+ replace = "19,20-dihydroxy-4Z,7Z,10Z,13Z,16Z-docosapentaenoic acid",
+ fixed = TRUE
+ )
),
# replace greek characters
.greek_dictionary,
# remove racemic properties
.racemic_dictionary
- )) +
+ )
+) +
pubchem_property_lookup(
- query_column = 'normalised_name',
- search_by = 'name',
- suffix = '_norm',
- property = 'InChIKey',
- records = 'best',
- cache = inchikey_cache2) +
+ query_column = "normalised_name",
+ search_by = "name",
+ suffix = "_norm",
+ property = "InChIKey",
+ records = "best",
+ cache = inchikey_cache2
+ ) +
opsin_lookup(
- query_column = 'normalised_name',
- suffix = '_opsin',
- output = 'stdinchikey',
+ query_column = "normalised_name",
+ suffix = "_opsin",
+ output = "stdinchikey",
cache = inchikey_cache3
) +
- combine_columns(
- column_names = c('stdinchikey_opsin','InChIKey_norm','InChIKey'),
- output_name = 'inchikey',
- source_name = 'inchikey_source',
+ prioritise_columns(
+ column_names = c("stdinchikey_opsin", "InChIKey_norm", "InChIKey"),
+ output_name = "inchikey",
+ source_name = "inchikey_source",
clean = TRUE
)
-N = model_apply(N,predicted(id_workflow))
We can explore the impact of these workflow steps using Venn and Pie charts to compare the results before/after the workflow.
In this Venn diagram we show the overlap between the InChIKey @@ -709,17 +732,19 @@
# venn inchikey columns
-C = annotation_venn_chart(
- factor_name = c('InChIKey','InChIKey_norm','stdinchikey_opsin'),
- line_colour = 'white',
- fill_colour = '.group',
+C <- annotation_venn_chart(
+ factor_name = c("InChIKey", "InChIKey_norm", "stdinchikey_opsin"),
+ line_colour = "white",
+ fill_colour = ".group",
legend = TRUE,
labels = FALSE
)
-chart_plot(C,predicted(N[3]))+
- guides(fill = guide_legend(title='Source'),
- colour = guide_legend(title = 'Source')) +
- theme(plot.margin = unit(c(1, 1.5, 1, 1.5), "cm"))
In the next chart we visualise the proportion of annotations from each query, after prioritisation and merging has taken place.
@@ -729,40 +754,43 @@
# pie source of inchikey
-C = annotation_pie_chart(
- factor_name = 'inchikey_source',
+C <- annotation_pie_chart(
+ factor_name = "inchikey_source",
label_rotation = FALSE,
- label_location = 'outside',
- label_type = 'percent',
+ label_location = "outside",
+ label_type = "percent",
legend = TRUE,
centre_radius = 0.5,
- centre_label = '.total',
+ centre_label = ".total",
count_na = TRUE
)
-chart_plot(C,predicted(N)) +
- guides(fill = guide_legend(title='Source'),
- colour = guide_legend(title = 'Source'))+
- theme(plot.margin = unit(c(1, 1.5, 1, 1.5), "cm"))
To keep the records with the highest confidence identifiers We can
remove the annotations based on the name
query using the
filter_labels
object.
# prepare workflow
-FL = filter_labels(
- column_name = 'inchikey_source',
- labels='InChIKey',
- mode='exclude')
+FL <- filter_labels(
+ column_name = "inchikey_source",
+ labels = "InChIKey",
+ mode = "exclude"
+)
# apply
-FF = model_apply(FL,predicted(N))
+FF <- model_apply(FL, predicted(N))
# print summary
predicted(FF)
-#> A "cd_source" object
-#> --------------------
-#> name: Combined annotation source
-#> description: This annotation_source object was generated by combining two other sources.
-#> input params: sheets
+#> A "annotation_source" object
+#> ----------------------------
+#> name: combined
+#> description: A source created by combining two or more sources
+#> input params: tag, data, source
#> annotations: 465 rows x 33 columns
rds_database
to import it.
# prepare object
-R = rds_database(
+R <- rds_database(
source = file.path(
- system.file('extdata',package='MetMashR'),
- 'standard_mixtures.rds'
+ system.file("extdata", package = "MetMashR"),
+ "standard_mixtures.rds"
),
.writable = FALSE
)
# read
-R = read_source(R)
The standards table contains a list of metabolites and the the +R <- read_source(R) +
+The standards table contains a list of metabolites and the the mixture they were included in. It also contains some manually curated data providing m/z and retention time of the metabolite, which assay it was observed in and the adduct.
@@ -810,46 +838,51 @@
# convert standard mixtures to source, then get inchikey from MTox700+
-SM = import_source() +
+SM <- import_source() +
filter_na(
- column_name = 'rt') +
+ column_name = "rt"
+ ) +
filter_na(
- column_name = 'median_ms2_scans') +
+ column_name = "median_ms2_scans"
+ ) +
filter_na(
- column_name = 'mzcloud_id') +
+ column_name = "mzcloud_id"
+ ) +
filter_range(
- column_name = 'median_ms2_scans',
+ column_name = "median_ms2_scans",
upper_limit = Inf,
lower_limit = 0,
- equal_to = TRUE) +
+ equal_to = TRUE
+ ) +
database_lookup(
- query_column = 'hmdb_id',
- database_column = 'hmdb_id',
+ query_column = "hmdb_id",
+ database_column = "hmdb_id",
database = MTox700plus_database(),
- include = 'inchikey',
- suffix = '',
- not_found = NA) +
+ include = "inchikey",
+ suffix = "",
+ not_found = NA
+ ) +
id_counts(
- id_column = 'inchikey',
- count_column = 'inchikey_count',
+ id_column = "inchikey",
+ count_column = "inchikey_count",
count_na = FALSE
)
# apply
-SM = model_apply(SM,R)
+SM <- model_apply(SM, R)
In this next plot we show the overlap in standards for each assay detected by manual observation.
-C = annotation_venn_chart(
- factor_name = 'inchikey',
- group_column = 'ion_mode',
- line_colour = 'white',
- fill_colour = '.group',
+C <- annotation_venn_chart(
+ factor_name = "inchikey",
+ group_column = "ion_mode",
+ line_colour = "white",
+ fill_colour = ".group",
legend = TRUE,
labels = FALSE
)
## plot
-chart_plot(C,predicted(SM))
Here we plot a venn diagram showing the overlap in identifiers.
# get processed data
-AN = predicted(FF)
-AN$tag = 'Annotations'
-sM = predicted(SM)
-sM$tag = 'Standards'
+AN <- predicted(FF)
+AN$tag <- "Annotations"
+sM <- predicted(SM)
+sM$tag <- "Standards"
# prepare chart
-C = annotation_venn_chart(
- factor_name = 'inchikey',
- line_colour = 'white',
- fill_colour = '.group'
+C <- annotation_venn_chart(
+ factor_name = "inchikey",
+ line_colour = "white",
+ fill_colour = ".group"
)
# plot
-chart_plot(C,sM,AN)+ggtitle('All assays, all sources')
It can been seen that there is a large number of annotations not present in the standard. These are false positives.
@@ -885,50 +918,53 @@
-G =list()
-VV = list()
-for (k in c('HILIC_NEG','HILIC_POS','LIPIDS_NEG','LIPIDS_POS')) {
- wf = filter_labels(
- column_name = 'assay',
+G <- list()
+VV <- list()
+for (k in c("HILIC_NEG", "HILIC_POS", "LIPIDS_NEG", "LIPIDS_POS")) {
+ wf <- filter_labels(
+ column_name = "assay",
labels = k,
- mode = 'include')
- wf1 = model_apply(wf,AN)
- wf$column_name = 'ion_mode'
- wf2 = model_apply(wf,sM)
- G[[k]]=chart_plot(C,predicted(wf2),predicted(wf1))
-
- V = filter_venn(
- factor_name = 'inchikey',
+ mode = "include"
+ )
+ wf1 <- model_apply(wf, AN)
+ wf$column_name <- "ion_mode"
+ wf2 <- model_apply(wf, sM)
+ G[[k]] <- chart_plot(C, predicted(wf2), predicted(wf1))
+
+ V <- filter_venn(
+ factor_name = "inchikey",
tables = list(predicted(wf1)),
- levels = 'Standards/Annotations',
- mode = 'include'
+ levels = "Standards/Annotations",
+ mode = "include"
)
- V = model_apply(V,predicted(wf2))
- VV[[k]] = predicted(V)
- VV[[k]]$tag = k
+ V <- model_apply(V, predicted(wf2))
+ VV[[k]] <- predicted(V)
+ VV[[k]]$tag <- k
}
-r1=cowplot::plot_grid(
- plotlist = G,nrow = 2 ,
- labels = c('HILIC_NEG','HILIC_POS','LIPIDS_NEG','LIPIDS_POS'))
+r1 <- cowplot::plot_grid(
+ plotlist = G, nrow = 2,
+ labels = c("HILIC_NEG", "HILIC_POS", "LIPIDS_NEG", "LIPIDS_POS")
+)
-cowplot::plot_grid(r1,chart_plot(C,VV),nrow=2,rel_heights = c(1,0.5))
The majority of the standards were correctly annotated in the HILIC_NEG assay.
In the next plot we compare the overlap in InChIKey for each source.
-G=list()
-for (k in c('CD','LS')) {
- wf = filter_labels(
- column_name = 'source_name',
+G <- list()
+for (k in c("CD", "LS")) {
+ wf <- filter_labels(
+ column_name = "source_name",
labels = k,
- mode = 'include')
- wf1 = model_apply(wf,AN)
-
- G[[k]]=chart_plot(C,sM,predicted(wf1))
+ mode = "include"
+ )
+ wf1 <- model_apply(wf, AN)
+
+ G[[k]] <- chart_plot(C, sM, predicted(wf1))
}
-cowplot::plot_grid(plotlist = G,nrow = 1,labels = c('CD','LS'))
sessionInfo()
-#> R version 4.3.2 (2023-10-31)
-#> Platform: x86_64-pc-linux-gnu (64-bit)
-#> Running under: Ubuntu 22.04.3 LTS
+#> R Under development (unstable) (2024-03-13 r86113)
+#> Platform: x86_64-pc-linux-gnu
+#> Running under: Ubuntu 22.04.4 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
@@ -960,63 +996,59 @@ Session Info#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
-#> [1] DT_0.31 dplyr_1.1.4 structToolbox_1.14.0
-#> [4] ggplot2_3.4.4 MetMashR_0.1.0 struct_1.14.0
-#> [7] BiocStyle_2.30.0
+#> [1] DT_0.32 dplyr_1.1.4 structToolbox_1.15.1
+#> [4] ggplot2_3.5.0 MetMashR_0.99.0 struct_1.15.4
+#> [7] BiocStyle_2.31.0
#>
#> loaded via a namespace (and not attached):
-#> [1] DBI_1.2.1 bitops_1.0-7
-#> [3] gridExtra_2.3 rlang_1.1.3
-#> [5] magrittr_2.0.3 matrixStats_1.2.0
-#> [7] e1071_1.7-14 compiler_4.3.2
-#> [9] RSQLite_2.3.4 systemfonts_1.0.5
-#> [11] vctrs_0.6.5 stringr_1.5.1
-#> [13] pkgconfig_2.0.3 crayon_1.5.2
-#> [15] fastmap_1.1.1 dbplyr_2.4.0
-#> [17] XVector_0.42.0 ellipsis_0.3.2
-#> [19] labeling_0.4.3 utf8_1.2.4
-#> [21] rmarkdown_2.25 ragg_1.2.7
-#> [23] purrr_1.0.2 bit_4.0.5
-#> [25] xfun_0.41 zlibbioc_1.48.0
-#> [27] cachem_1.0.8 GenomeInfoDb_1.38.5
-#> [29] jsonlite_1.8.8 RVenn_1.1.0
-#> [31] blob_1.2.4 highr_0.10
-#> [33] DelayedArray_0.28.0 R6_2.5.1
-#> [35] bslib_0.6.1 stringi_1.8.3
-#> [37] GenomicRanges_1.54.1 jquerylib_0.1.4
-#> [39] Rcpp_1.0.12 bookdown_0.37
-#> [41] SummarizedExperiment_1.32.0 knitr_1.45
-#> [43] IRanges_2.36.0 Matrix_1.6-5
-#> [45] tidyselect_1.2.0 abind_1.4-5
-#> [47] yaml_2.3.8 ggVennDiagram_1.5.0
-#> [49] curl_5.2.0 lattice_0.22-5
-#> [51] tibble_3.2.1 plyr_1.8.9
-#> [53] Biobase_2.62.0 withr_3.0.0
-#> [55] evaluate_0.23 ontologyIndex_2.11
-#> [57] desc_1.4.3 sf_1.0-15
-#> [59] units_0.8-5 proxy_0.4-27
-#> [61] BiocFileCache_2.10.1 zip_2.3.0
-#> [63] filelock_1.0.3 pillar_1.9.0
-#> [65] BiocManager_1.30.22 MatrixGenerics_1.14.0
-#> [67] KernSmooth_2.23-22 stats4_4.3.2
-#> [69] generics_0.1.3 sp_2.1-2
-#> [71] RCurl_1.98-1.14 S4Vectors_0.40.2
-#> [73] munsell_0.5.0 scales_1.3.0
-#> [75] class_7.3-22 glue_1.7.0
-#> [77] tools_4.3.2 openxlsx_4.2.5.2
-#> [79] fs_1.6.3 cowplot_1.1.2
-#> [81] grid_4.3.2 crosstalk_1.2.1
-#> [83] colorspace_2.1-0 GenomeInfoDbData_1.2.11
-#> [85] cli_3.6.2 textshaping_0.3.7
-#> [87] fansi_1.0.6 ggthemes_5.0.0
-#> [89] S4Arrays_1.2.0 gtable_0.3.4
-#> [91] sass_0.4.8 digest_0.6.34
-#> [93] BiocGenerics_0.48.1 classInt_0.4-10
-#> [95] SparseArray_1.2.3 htmlwidgets_1.6.4
-#> [97] farver_2.1.1 memoise_2.0.1
-#> [99] htmltools_0.5.7 pkgdown_2.0.7
-#> [101] lifecycle_1.0.4 httr_1.4.7
-#> [103] bit64_4.0.5
+#> [1] tidyselect_1.2.1 blob_1.2.4
+#> [3] farver_2.1.1 filelock_1.0.3
+#> [5] bitops_1.0-7 fastmap_1.1.1
+#> [7] RCurl_1.98-1.14 BiocFileCache_2.11.1
+#> [9] digest_0.6.35 lifecycle_1.0.4
+#> [11] ellipsis_0.3.2 RSQLite_2.3.5
+#> [13] magrittr_2.0.3 compiler_4.4.0
+#> [15] rlang_1.1.3 sass_0.4.9
+#> [17] tools_4.4.0 utf8_1.2.4
+#> [19] yaml_2.3.8 knitr_1.45
+#> [21] S4Arrays_1.3.6 labeling_0.4.3
+#> [23] htmlwidgets_1.6.4 bit_4.0.5
+#> [25] curl_5.2.1 ontologyIndex_2.12
+#> [27] sp_2.1-3 DelayedArray_0.29.9
+#> [29] plyr_1.8.9 abind_1.4-5
+#> [31] withr_3.0.0 purrr_1.0.2
+#> [33] BiocGenerics_0.49.1 desc_1.4.3
+#> [35] grid_4.4.0 stats4_4.4.0
+#> [37] fansi_1.0.6 colorspace_2.1-0
+#> [39] scales_1.3.0 SummarizedExperiment_1.33.3
+#> [41] cli_3.6.2 rmarkdown_2.26
+#> [43] crayon_1.5.2 ragg_1.3.0
+#> [45] generics_0.1.3 httr_1.4.7
+#> [47] DBI_1.2.2 cachem_1.0.8
+#> [49] stringr_1.5.1 zlibbioc_1.49.3
+#> [51] ggthemes_5.1.0 BiocManager_1.30.22
+#> [53] XVector_0.43.1 matrixStats_1.2.0
+#> [55] vctrs_0.6.5 Matrix_1.6-5
+#> [57] jsonlite_1.8.8 bookdown_0.38
+#> [59] IRanges_2.37.1 S4Vectors_0.41.5
+#> [61] bit64_4.0.5 systemfonts_1.0.6
+#> [63] crosstalk_1.2.1 jquerylib_0.1.4
+#> [65] ggVennDiagram_1.5.2 glue_1.7.0
+#> [67] pkgdown_2.0.7.9000 cowplot_1.1.3
+#> [69] stringi_1.8.3 gtable_0.3.4
+#> [71] RVenn_1.1.0 GenomeInfoDb_1.39.9
+#> [73] GenomicRanges_1.55.4 munsell_0.5.0
+#> [75] tibble_3.2.1 pillar_1.9.0
+#> [77] htmltools_0.5.7 GenomeInfoDbData_1.2.11
+#> [79] dbplyr_2.5.0 R6_2.5.1
+#> [81] textshaping_0.3.7 evaluate_0.23
+#> [83] lattice_0.22-6 Biobase_2.63.0
+#> [85] highr_0.10 openxlsx_4.2.5.2
+#> [87] memoise_2.0.1 bslib_0.6.1
+#> [89] Rcpp_1.0.12 zip_2.3.1
+#> [91] gridExtra_2.3 SparseArray_1.3.4
+#> [93] xfun_0.42 fs_1.6.3
+#> [95] MatrixGenerics_1.15.0 pkgconfig_2.0.3
@@ -1031,7 +1063,7 @@ Site built with pkgdown 2.0.7.
+Site built with pkgdown 2.0.7.9000.
diff --git a/articles/annotate_mixtures_files/datatables-binding-0.32/datatables.js b/articles/annotate_mixtures_files/datatables-binding-0.32/datatables.js new file mode 100644 index 0000000..6a3c3d5 --- /dev/null +++ b/articles/annotate_mixtures_files/datatables-binding-0.32/datatables.js @@ -0,0 +1,1536 @@ +(function() { + +// some helper functions: using a global object DTWidget so that it can be used +// in JS() code, e.g. datatable(options = list(foo = JS('code'))); unlike R's +// dynamic scoping, when 'code' is eval'ed, JavaScript does not know objects +// from the "parent frame", e.g. JS('DTWidget') will not work unless it was made +// a global object +var DTWidget = {}; + +// 123456666.7890 -> 123,456,666.7890 +var markInterval = function(d, digits, interval, mark, decMark, precision) { + x = precision ? d.toPrecision(digits) : d.toFixed(digits); + if (!/^-?[\d.]+$/.test(x)) return x; + var xv = x.split('.'); + if (xv.length > 2) return x; // should have at most one decimal point + xv[0] = xv[0].replace(new RegExp('\\B(?=(\\d{' + interval + '})+(?!\\d))', 'g'), mark); + return xv.join(decMark); +}; + +DTWidget.formatCurrency = function(data, currency, digits, interval, mark, decMark, before, zeroPrint) { + var d = parseFloat(data); + if (isNaN(d)) return ''; + if (zeroPrint !== null && d === 0.0) return zeroPrint; + var res = markInterval(d, digits, interval, mark, decMark); + res = before ? (/^-/.test(res) ? '-' + currency + res.replace(/^-/, '') : currency + res) : + res + currency; + return res; +}; + +DTWidget.formatString = function(data, prefix, suffix) { + var d = data; + if (d === null) return ''; + return prefix + d + suffix; +}; + +DTWidget.formatPercentage = function(data, digits, interval, mark, decMark, zeroPrint) { + var d = parseFloat(data); + if (isNaN(d)) return ''; + if (zeroPrint !== null && d === 0.0) return zeroPrint; + return markInterval(d * 100, digits, interval, mark, decMark) + '%'; +}; + +DTWidget.formatRound = function(data, digits, interval, mark, decMark, zeroPrint) { + var d = parseFloat(data); + if (isNaN(d)) return ''; + if (zeroPrint !== null && d === 0.0) return zeroPrint; + return markInterval(d, digits, interval, mark, decMark); +}; + +DTWidget.formatSignif = function(data, digits, interval, mark, decMark, zeroPrint) { + var d = parseFloat(data); + if (isNaN(d)) return ''; + if (zeroPrint !== null && d === 0.0) return zeroPrint; + return markInterval(d, digits, interval, mark, decMark, true); +}; + +DTWidget.formatDate = function(data, method, params) { + var d = data; + if (d === null) return ''; + // (new Date('2015-10-28')).toDateString() may return 2015-10-27 because the + // actual time created could be like 'Tue Oct 27 2015 19:00:00 GMT-0500 (CDT)', + // i.e. the date-only string is treated as UTC time instead of local time + if ((method === 'toDateString' || method === 'toLocaleDateString') && /^\d{4,}\D\d{2}\D\d{2}$/.test(d)) { + d = d.split(/\D/); + d = new Date(d[0], d[1] - 1, d[2]); + } else { + d = new Date(d); + } + return d[method].apply(d, params); +}; + +window.DTWidget = DTWidget; + +// A helper function to update the properties of existing filters +var setFilterProps = function(td, props) { + // Update enabled/disabled state + var $input = $(td).find('input').first(); + var searchable = $input.data('searchable'); + $input.prop('disabled', !searchable || props.disabled); + + // Based on the filter type, set its new values + var type = td.getAttribute('data-type'); + if (['factor', 'logical'].includes(type)) { + // Reformat the new dropdown options for use with selectize + var new_vals = props.params.options.map(function(item) { + return { text: item, value: item }; + }); + + // Find the selectize object + var dropdown = $(td).find('.selectized').eq(0)[0].selectize; + + // Note the current values + var old_vals = dropdown.getValue(); + + // Remove the existing values + dropdown.clearOptions(); + + // Add the new options + dropdown.addOption(new_vals); + + // Preserve the existing values + dropdown.setValue(old_vals); + + } else if (['number', 'integer', 'date', 'time'].includes(type)) { + // Apply internal scaling to new limits. Updating scale not yet implemented. + var slider = $(td).find('.noUi-target').eq(0); + var scale = Math.pow(10, Math.max(0, +slider.data('scale') || 0)); + var new_vals = [props.params.min * scale, props.params.max * scale]; + + // Note what the new limits will be just for this filter + var new_lims = new_vals.slice(); + + // Determine the current values and limits + var old_vals = slider.val().map(Number); + var old_lims = slider.noUiSlider('options').range; + old_lims = [old_lims.min, old_lims.max]; + + // Preserve the current values if filters have been applied; otherwise, apply no filtering + if (old_vals[0] != old_lims[0]) { + new_vals[0] = Math.max(old_vals[0], new_vals[0]); + } + + if (old_vals[1] != old_lims[1]) { + new_vals[1] = Math.min(old_vals[1], new_vals[1]); + } + + // Update the endpoints of the slider + slider.noUiSlider({ + start: new_vals, + range: {'min': new_lims[0], 'max': new_lims[1]} + }, true); + } +}; + +var transposeArray2D = function(a) { + return a.length === 0 ? a : HTMLWidgets.transposeArray2D(a); +}; + +var crosstalkPluginsInstalled = false; + +function maybeInstallCrosstalkPlugins() { + if (crosstalkPluginsInstalled) + return; + crosstalkPluginsInstalled = true; + + $.fn.dataTable.ext.afnFiltering.push( + function(oSettings, aData, iDataIndex) { + var ctfilter = oSettings.nTable.ctfilter; + if (ctfilter && !ctfilter[iDataIndex]) + return false; + + var ctselect = oSettings.nTable.ctselect; + if (ctselect && !ctselect[iDataIndex]) + return false; + + return true; + } + ); +} + +HTMLWidgets.widget({ + name: "datatables", + type: "output", + renderOnNullValue: true, + initialize: function(el, width, height) { + // in order that the type=number inputs return a number + $.valHooks.number = { + get: function(el) { + var value = parseFloat(el.value); + return isNaN(value) ? "" : value; + } + }; + $(el).html(' '); + return { + data: null, + ctfilterHandle: new crosstalk.FilterHandle(), + ctfilterSubscription: null, + ctselectHandle: new crosstalk.SelectionHandle(), + ctselectSubscription: null + }; + }, + renderValue: function(el, data, instance) { + if (el.offsetWidth === 0 || el.offsetHeight === 0) { + instance.data = data; + return; + } + instance.data = null; + var $el = $(el); + $el.empty(); + + if (data === null) { + $el.append(' '); + // clear previous Shiny inputs (if any) + for (var i in instance.clearInputs) instance.clearInputs[i](); + instance.clearInputs = {}; + return; + } + + var crosstalkOptions = data.crosstalkOptions; + if (!crosstalkOptions) crosstalkOptions = { + 'key': null, 'group': null + }; + if (crosstalkOptions.group) { + maybeInstallCrosstalkPlugins(); + instance.ctfilterHandle.setGroup(crosstalkOptions.group); + instance.ctselectHandle.setGroup(crosstalkOptions.group); + } + + // if we are in the viewer then we always want to fillContainer and + // and autoHideNavigation (unless the user has explicitly set these) + if (window.HTMLWidgets.viewerMode) { + if (!data.hasOwnProperty("fillContainer")) + data.fillContainer = true; + if (!data.hasOwnProperty("autoHideNavigation")) + data.autoHideNavigation = true; + } + + // propagate fillContainer to instance (so we have it in resize) + instance.fillContainer = data.fillContainer; + + var cells = data.data; + + if (cells instanceof Array) cells = transposeArray2D(cells); + + $el.append(data.container); + var $table = $el.find('table'); + if (data.class) $table.addClass(data.class); + if (data.caption) $table.prepend(data.caption); + + if (!data.selection) data.selection = { + mode: 'none', selected: null, target: 'row', selectable: null + }; + if (HTMLWidgets.shinyMode && data.selection.mode !== 'none' && + data.selection.target === 'row+column') { + if ($table.children('tfoot').length === 0) { + $table.append($('')); + $table.find('thead tr').clone().appendTo($table.find('tfoot')); + } + } + + // column filters + var filterRow; + switch (data.filter) { + case 'top': + $table.children('thead').append(data.filterHTML); + filterRow = $table.find('thead tr:last td'); + break; + case 'bottom': + if ($table.children('tfoot').length === 0) { + $table.append($('')); + } + $table.children('tfoot').prepend(data.filterHTML); + filterRow = $table.find('tfoot tr:first td'); + break; + } + + var options = { searchDelay: 1000 }; + if (cells !== null) $.extend(options, { + data: cells + }); + + // options for fillContainer + var bootstrapActive = typeof($.fn.popover) != 'undefined'; + if (instance.fillContainer) { + + // force scrollX/scrollY and turn off autoWidth + options.scrollX = true; + options.scrollY = "100px"; // can be any value, we'll adjust below + + // if we aren't paginating then move around the info/filter controls + // to save space at the bottom and rephrase the info callback + if (data.options.paging === false) { + + // we know how to do this cleanly for bootstrap, not so much + // for other themes/layouts + if (bootstrapActive) { + options.dom = "<'row'<'col-sm-4'i><'col-sm-8'f>>" + + "<'row'<'col-sm-12'tr>>"; + } + + options.fnInfoCallback = function(oSettings, iStart, iEnd, + iMax, iTotal, sPre) { + return Number(iTotal).toLocaleString() + " records"; + }; + } + } + + // auto hide navigation if requested + // Note, this only works on client-side processing mode as on server-side, + // cells (data.data) is null; In addition, we require the pageLength option + // being provided explicitly to enable this. Despite we may be able to deduce + // the default value of pageLength, it may complicate things so we'd rather + // put this responsiblity to users and warn them on the R side. + if (data.autoHideNavigation === true && data.options.paging !== false) { + // strip all nav if length >= cells + if ((cells instanceof Array) && data.options.pageLength >= cells.length) + options.dom = bootstrapActive ? "<'row'<'col-sm-12'tr>>" : "t"; + // alternatively lean things out for flexdashboard mobile portrait + else if (bootstrapActive && window.FlexDashboard && window.FlexDashboard.isMobilePhone()) + options.dom = "<'row'<'col-sm-12'f>>" + + "<'row'<'col-sm-12'tr>>" + + "<'row'<'col-sm-12'p>>"; + } + + $.extend(true, options, data.options || {}); + + var searchCols = options.searchCols; + if (searchCols) { + searchCols = searchCols.map(function(x) { + return x === null ? '' : x.search; + }); + // FIXME: this means I don't respect the escapeRegex setting + delete options.searchCols; + } + + // server-side processing? + var server = options.serverSide === true; + + // use the dataSrc function to pre-process JSON data returned from R + var DT_rows_all = [], DT_rows_current = []; + if (server && HTMLWidgets.shinyMode && typeof options.ajax === 'object' && + /^session\/[\da-z]+\/dataobj/.test(options.ajax.url) && !options.ajax.dataSrc) { + options.ajax.dataSrc = function(json) { + DT_rows_all = $.makeArray(json.DT_rows_all); + DT_rows_current = $.makeArray(json.DT_rows_current); + var data = json.data; + if (!colReorderEnabled()) return data; + var table = $table.DataTable(), order = table.colReorder.order(), flag = true, i, j, row; + for (i = 0; i < order.length; ++i) if (order[i] !== i) flag = false; + if (flag) return data; + for (i = 0; i < data.length; ++i) { + row = data[i].slice(); + for (j = 0; j < order.length; ++j) data[i][j] = row[order[j]]; + } + return data; + }; + } + + var thiz = this; + if (instance.fillContainer) $table.on('init.dt', function(e) { + thiz.fillAvailableHeight(el, $(el).innerHeight()); + }); + // If the page contains serveral datatables and one of which enables colReorder, + // the table.colReorder.order() function will exist but throws error when called. + // So it seems like the only way to know if colReorder is enabled or not is to + // check the options. + var colReorderEnabled = function() { return "colReorder" in options; }; + var table = $table.DataTable(options); + $el.data('datatable', table); + + if ('rowGroup' in options) { + // Maintain RowGroup dataSrc when columns are reordered (#1109) + table.on('column-reorder', function(e, settings, details) { + var oldDataSrc = table.rowGroup().dataSrc(); + var newDataSrc = details.mapping[oldDataSrc]; + table.rowGroup().dataSrc(newDataSrc); + }); + } + + // Unregister previous Crosstalk event subscriptions, if they exist + if (instance.ctfilterSubscription) { + instance.ctfilterHandle.off("change", instance.ctfilterSubscription); + instance.ctfilterSubscription = null; + } + if (instance.ctselectSubscription) { + instance.ctselectHandle.off("change", instance.ctselectSubscription); + instance.ctselectSubscription = null; + } + + if (!crosstalkOptions.group) { + $table[0].ctfilter = null; + $table[0].ctselect = null; + } else { + var key = crosstalkOptions.key; + function keysToMatches(keys) { + if (!keys) { + return null; + } else { + var selectedKeys = {}; + for (var i = 0; i < keys.length; i++) { + selectedKeys[keys[i]] = true; + } + var matches = {}; + for (var j = 0; j < key.length; j++) { + if (selectedKeys[key[j]]) + matches[j] = true; + } + return matches; + } + } + + function applyCrosstalkFilter(e) { + $table[0].ctfilter = keysToMatches(e.value); + table.draw(); + } + instance.ctfilterSubscription = instance.ctfilterHandle.on("change", applyCrosstalkFilter); + applyCrosstalkFilter({value: instance.ctfilterHandle.filteredKeys}); + + function applyCrosstalkSelection(e) { + if (e.sender !== instance.ctselectHandle) { + table + .rows('.' + selClass, {search: 'applied'}) + .nodes() + .to$() + .removeClass(selClass); + if (selectedRows) + changeInput('rows_selected', selectedRows(), void 0, true); + } + + if (e.sender !== instance.ctselectHandle && e.value && e.value.length) { + var matches = keysToMatches(e.value); + + // persistent selection with plotly (& leaflet) + var ctOpts = crosstalk.var("plotlyCrosstalkOpts").get() || {}; + if (ctOpts.persistent === true) { + var matches = $.extend(matches, $table[0].ctselect); + } + + $table[0].ctselect = matches; + table.draw(); + } else { + if ($table[0].ctselect) { + $table[0].ctselect = null; + table.draw(); + } + } + } + instance.ctselectSubscription = instance.ctselectHandle.on("change", applyCrosstalkSelection); + // TODO: This next line doesn't seem to work when renderDataTable is used + applyCrosstalkSelection({value: instance.ctselectHandle.value}); + } + + var inArray = function(val, array) { + return $.inArray(val, $.makeArray(array)) > -1; + }; + + // search the i-th column + var searchColumn = function(i, value) { + var regex = false, ci = true; + if (options.search) { + regex = options.search.regex, + ci = options.search.caseInsensitive !== false; + } + // need to transpose the column index when colReorder is enabled + if (table.colReorder) i = table.colReorder.transpose(i); + return table.column(i).search(value, regex, !regex, ci); + }; + + if (data.filter !== 'none') { + if (!data.hasOwnProperty('filterSettings')) data.filterSettings = {}; + + filterRow.each(function(i, td) { + + var $td = $(td), type = $td.data('type'), filter; + var $input = $td.children('div').first().children('input'); + var disabled = $input.prop('disabled'); + var searchable = table.settings()[0].aoColumns[i].bSearchable; + $input.prop('disabled', !searchable || disabled); + $input.data('searchable', searchable); // for updating later + $input.on('input blur', function() { + $input.next('span').toggle(Boolean($input.val())); + }); + // Bootstrap sets pointer-events to none and we won't be able to click + // the clear button + $input.next('span').css('pointer-events', 'auto').hide().click(function() { + $(this).hide().prev('input').val('').trigger('input').focus(); + }); + var searchCol; // search string for this column + if (searchCols && searchCols[i]) { + searchCol = searchCols[i]; + $input.val(searchCol).trigger('input'); + } + var $x = $td.children('div').last(); + + // remove the overflow: hidden attribute of the scrollHead + // (otherwise the scrolling table body obscures the filters) + // The workaround and the discussion from + // https://github.com/rstudio/DT/issues/554#issuecomment-518007347 + // Otherwise the filter selection will not be anchored to the values + // when the columns number is many and scrollX is enabled. + var scrollHead = $(el).find('.dataTables_scrollHead,.dataTables_scrollFoot'); + var cssOverflowHead = scrollHead.css('overflow'); + var scrollBody = $(el).find('.dataTables_scrollBody'); + var cssOverflowBody = scrollBody.css('overflow'); + var scrollTable = $(el).find('.dataTables_scroll'); + var cssOverflowTable = scrollTable.css('overflow'); + if (cssOverflowHead === 'hidden') { + $x.on('show hide', function(e) { + if (e.type === 'show') { + scrollHead.css('overflow', 'visible'); + scrollBody.css('overflow', 'visible'); + scrollTable.css('overflow-x', 'scroll'); + } else { + scrollHead.css('overflow', cssOverflowHead); + scrollBody.css('overflow', cssOverflowBody); + scrollTable.css('overflow-x', cssOverflowTable); + } + }); + $x.css('z-index', 25); + } + + if (inArray(type, ['factor', 'logical'])) { + $input.on({ + click: function() { + $input.parent().hide(); $x.show().trigger('show'); filter[0].selectize.focus(); + }, + input: function() { + var v1 = JSON.stringify(filter[0].selectize.getValue()), v2 = $input.val(); + if (v1 === '[]') v1 = ''; + if (v1 !== v2) filter[0].selectize.setValue(v2 === '' ? [] : JSON.parse(v2)); + } + }); + var $input2 = $x.children('select'); + filter = $input2.selectize($.extend({ + options: $input2.data('options').map(function(v, i) { + return ({text: v, value: v}); + }), + plugins: ['remove_button'], + hideSelected: true, + onChange: function(value) { + if (value === null) value = []; // compatibility with jQuery 3.0 + $input.val(value.length ? JSON.stringify(value) : ''); + if (value.length) $input.trigger('input'); + $input.attr('title', $input.val()); + if (server) { + searchColumn(i, value.length ? JSON.stringify(value) : '').draw(); + return; + } + // turn off filter if nothing selected + $td.data('filter', value.length > 0); + table.draw(); // redraw table, and filters will be applied + } + }, data.filterSettings.select)); + filter[0].selectize.on('blur', function() { + $x.hide().trigger('hide'); $input.parent().show(); $input.trigger('blur'); + }); + filter.next('div').css('margin-bottom', 'auto'); + } else if (type === 'character') { + var fun = function() { + searchColumn(i, $input.val()).draw(); + }; + // throttle searching for server-side processing + var throttledFun = $.fn.dataTable.util.throttle(fun, options.searchDelay); + $input.on('input', function(e, immediate) { + // always bypass throttling when immediate = true (via the updateSearch method) + (immediate || !server) ? fun() : throttledFun(); + }); + } else if (inArray(type, ['number', 'integer', 'date', 'time'])) { + var $x0 = $x; + $x = $x0.children('div').first(); + $x0.css({ + 'background-color': '#fff', + 'border': '1px #ddd solid', + 'border-radius': '4px', + 'padding': data.vertical ? '35px 20px': '20px 20px 10px 20px' + }); + var $spans = $x0.children('span').css({ + 'margin-top': data.vertical ? '0' : '10px', + 'white-space': 'nowrap' + }); + var $span1 = $spans.first(), $span2 = $spans.last(); + var r1 = +$x.data('min'), r2 = +$x.data('max'); + // when the numbers are too small or have many decimal places, the + // slider may have numeric precision problems (#150) + var scale = Math.pow(10, Math.max(0, +$x.data('scale') || 0)); + r1 = Math.round(r1 * scale); r2 = Math.round(r2 * scale); + var scaleBack = function(x, scale) { + if (scale === 1) return x; + var d = Math.round(Math.log(scale) / Math.log(10)); + // to avoid problems like 3.423/100 -> 0.034230000000000003 + return (x / scale).toFixed(d); + }; + var slider_min = function() { + return filter.noUiSlider('options').range.min; + }; + var slider_max = function() { + return filter.noUiSlider('options').range.max; + }; + $input.on({ + focus: function() { + $x0.show().trigger('show'); + // first, make sure the slider div leaves at least 20px between + // the two (slider value) span's + $x0.width(Math.max(160, $span1.outerWidth() + $span2.outerWidth() + 20)); + // then, if the input is really wide or slider is vertical, + // make the slider the same width as the input + if ($x0.outerWidth() < $input.outerWidth() || data.vertical) { + $x0.outerWidth($input.outerWidth()); + } + // make sure the slider div does not reach beyond the right margin + if ($(window).width() < $x0.offset().left + $x0.width()) { + $x0.offset({ + 'left': $input.offset().left + $input.outerWidth() - $x0.outerWidth() + }); + } + }, + blur: function() { + $x0.hide().trigger('hide'); + }, + input: function() { + if ($input.val() === '') filter.val([slider_min(), slider_max()]); + }, + change: function() { + var v = $input.val().replace(/\s/g, ''); + if (v === '') return; + v = v.split('...'); + if (v.length !== 2) { + $input.parent().addClass('has-error'); + return; + } + if (v[0] === '') v[0] = slider_min(); + if (v[1] === '') v[1] = slider_max(); + $input.parent().removeClass('has-error'); + // treat date as UTC time at midnight + var strTime = function(x) { + var s = type === 'date' ? 'T00:00:00Z' : ''; + var t = new Date(x + s).getTime(); + // add 10 minutes to date since it does not hurt the date, and + // it helps avoid the tricky floating point arithmetic problems, + // e.g. sometimes the date may be a few milliseconds earlier + // than the midnight due to precision problems in noUiSlider + return type === 'date' ? t + 3600000 : t; + }; + if (inArray(type, ['date', 'time'])) { + v[0] = strTime(v[0]); + v[1] = strTime(v[1]); + } + if (v[0] != slider_min()) v[0] *= scale; + if (v[1] != slider_max()) v[1] *= scale; + filter.val(v); + } + }); + var formatDate = function(d) { + d = scaleBack(d, scale); + if (type === 'number') return d; + if (type === 'integer') return parseInt(d); + var x = new Date(+d); + if (type === 'date') { + var pad0 = function(x) { + return ('0' + x).substr(-2, 2); + }; + return x.getUTCFullYear() + '-' + pad0(1 + x.getUTCMonth()) + + '-' + pad0(x.getUTCDate()); + } else { + return x.toISOString(); + } + }; + var opts = type === 'date' ? { step: 60 * 60 * 1000 } : + type === 'integer' ? { step: 1 } : {}; + + opts.orientation = data.vertical ? 'vertical': 'horizontal'; + opts.direction = data.vertical ? 'rtl': 'ltr'; + + filter = $x.noUiSlider($.extend({ + start: [r1, r2], + range: {min: r1, max: r2}, + connect: true + }, opts, data.filterSettings.slider)); + if (scale > 1) (function() { + var t1 = r1, t2 = r2; + var val = filter.val(); + while (val[0] > r1 || val[1] < r2) { + if (val[0] > r1) { + t1 -= val[0] - r1; + } + if (val[1] < r2) { + t2 += r2 - val[1]; + } + filter = $x.noUiSlider($.extend({ + start: [t1, t2], + range: {min: t1, max: t2}, + connect: true + }, opts, data.filterSettings.slider), true); + val = filter.val(); + } + r1 = t1; r2 = t2; + })(); + // format with active column renderer, if defined + var colDef = data.options.columnDefs.find(function(def) { + return (def.targets === i || inArray(i, def.targets)) && 'render' in def; + }); + var updateSliderText = function(v1, v2) { + // we only know how to use function renderers + if (colDef && typeof colDef.render === 'function') { + var restore = function(v) { + v = scaleBack(v, scale); + return inArray(type, ['date', 'time']) ? new Date(+v) : v; + } + $span1.text(colDef.render(restore(v1), 'display')); + $span2.text(colDef.render(restore(v2), 'display')); + } else { + $span1.text(formatDate(v1)); + $span2.text(formatDate(v2)); + } + }; + updateSliderText(r1, r2); + var updateSlider = function(e) { + var val = filter.val(); + // turn off filter if in full range + $td.data('filter', val[0] > slider_min() || val[1] < slider_max()); + var v1 = formatDate(val[0]), v2 = formatDate(val[1]), ival; + if ($td.data('filter')) { + ival = v1 + ' ... ' + v2; + $input.attr('title', ival).val(ival).trigger('input'); + } else { + $input.attr('title', '').val(''); + } + updateSliderText(val[0], val[1]); + if (e.type === 'slide') return; // no searching when sliding only + if (server) { + searchColumn(i, $td.data('filter') ? ival : '').draw(); + return; + } + table.draw(); + }; + filter.on({ + set: updateSlider, + slide: updateSlider + }); + } + + // server-side processing will be handled by R (or whatever server + // language you use); the following code is only needed for client-side + // processing + if (server) { + // if a search string has been pre-set, search now + if (searchCol) $input.trigger('input').trigger('change'); + return; + } + + var customFilter = function(settings, data, dataIndex) { + // there is no way to attach a search function to a specific table, + // and we need to make sure a global search function is not applied to + // all tables (i.e. a range filter in a previous table should not be + // applied to the current table); we use the settings object to + // determine if we want to perform searching on the current table, + // since settings.sTableId will be different to different tables + if (table.settings()[0] !== settings) return true; + // no filter on this column or no need to filter this column + if (typeof filter === 'undefined' || !$td.data('filter')) return true; + + var r = filter.val(), v, r0, r1; + var i_data = function(i) { + if (!colReorderEnabled()) return i; + var order = table.colReorder.order(), k; + for (k = 0; k < order.length; ++k) if (order[k] === i) return k; + return i; // in theory it will never be here... + } + v = data[i_data(i)]; + if (type === 'number' || type === 'integer') { + v = parseFloat(v); + // how to handle NaN? currently exclude these rows + if (isNaN(v)) return(false); + r0 = parseFloat(scaleBack(r[0], scale)) + r1 = parseFloat(scaleBack(r[1], scale)); + if (v >= r0 && v <= r1) return true; + } else if (type === 'date' || type === 'time') { + v = new Date(v); + r0 = new Date(r[0] / scale); r1 = new Date(r[1] / scale); + if (v >= r0 && v <= r1) return true; + } else if (type === 'factor') { + if (r.length === 0 || inArray(v, r)) return true; + } else if (type === 'logical') { + if (r.length === 0) return true; + if (inArray(v === '' ? 'na' : v, r)) return true; + } + return false; + }; + + $.fn.dataTable.ext.search.push(customFilter); + + // search for the preset search strings if it is non-empty + if (searchCol) $input.trigger('input').trigger('change'); + + }); + + } + + // highlight search keywords + var highlight = function() { + var body = $(table.table().body()); + // removing the old highlighting first + body.unhighlight(); + + // don't highlight the "not found" row, so we get the rows using the api + if (table.rows({ filter: 'applied' }).data().length === 0) return; + // highlight global search keywords + body.highlight($.trim(table.search()).split(/\s+/)); + // then highlight keywords from individual column filters + if (filterRow) filterRow.each(function(i, td) { + var $td = $(td), type = $td.data('type'); + if (type !== 'character') return; + var $input = $td.children('div').first().children('input'); + var column = table.column(i).nodes().to$(), + val = $.trim($input.val()); + if (type !== 'character' || val === '') return; + column.highlight(val.split(/\s+/)); + }); + }; + + if (options.searchHighlight) { + table + .on('draw.dt.dth column-visibility.dt.dth column-reorder.dt.dth', highlight) + .on('destroy', function() { + // remove event handler + table.off('draw.dt.dth column-visibility.dt.dth column-reorder.dt.dth'); + }); + + // Set the option for escaping regex characters in our search string. This will be used + // for all future matching. + jQuery.fn.highlight.options.escapeRegex = (!options.search || !options.search.regex); + + // initial highlight for state saved conditions and initial states + highlight(); + } + + // run the callback function on the table instance + if (typeof data.callback === 'function') data.callback(table); + + // double click to edit the cell, row, column, or all cells + if (data.editable) table.on('dblclick.dt', 'tbody td', function(e) { + // only bring up the editor when the cell itself is dbclicked, and ignore + // other dbclick events bubbled up (e.g. from the ) + if (e.target !== this) return; + var target = [], immediate = false; + switch (data.editable.target) { + case 'cell': + target = [this]; + immediate = true; // edit will take effect immediately + break; + case 'row': + target = table.cells(table.cell(this).index().row, '*').nodes(); + break; + case 'column': + target = table.cells('*', table.cell(this).index().column).nodes(); + break; + case 'all': + target = table.cells().nodes(); + break; + default: + throw 'The editable parameter must be "cell", "row", "column", or "all"'; + } + var disableCols = data.editable.disable ? data.editable.disable.columns : null; + var numericCols = data.editable.numeric; + var areaCols = data.editable.area; + var dateCols = data.editable.date; + for (var i = 0; i < target.length; i++) { + (function(cell, current) { + var $cell = $(cell), html = $cell.html(); + var _cell = table.cell(cell), value = _cell.data(), index = _cell.index().column; + var $input; + if (inArray(index, numericCols)) { + $input = $(''); + } else if (inArray(index, areaCols)) { + $input = $(''); + } else if (inArray(index, dateCols)) { + $input = $(''); + } else { + $input = $(''); + } + if (!immediate) { + $cell.data('input', $input).data('html', html); + $input.attr('title', 'Hit Ctrl+Enter to finish editing, or Esc to cancel'); + } + $input.val(value); + if (inArray(index, disableCols)) { + $input.attr('readonly', '').css('filter', 'invert(25%)'); + } + $cell.empty().append($input); + if (cell === current) $input.focus(); + $input.css('width', '100%'); + + if (immediate) $input.on('blur', function(e) { + var valueNew = $input.val(); + if (valueNew !== value) { + _cell.data(valueNew); + if (HTMLWidgets.shinyMode) { + changeInput('cell_edit', [cellInfo(cell)], 'DT.cellInfo', null, {priority: 'event'}); + } + // for server-side processing, users have to call replaceData() to update the table + if (!server) table.draw(false); + } else { + $cell.html(html); + } + }).on('keyup', function(e) { + // hit Escape to cancel editing + if (e.keyCode === 27) $input.trigger('blur'); + }); + + // bulk edit (row, column, or all) + if (!immediate) $input.on('keyup', function(e) { + var removeInput = function($cell, restore) { + $cell.data('input').remove(); + if (restore) $cell.html($cell.data('html')); + } + if (e.keyCode === 27) { + for (var i = 0; i < target.length; i++) { + removeInput($(target[i]), true); + } + } else if (e.keyCode === 13 && e.ctrlKey) { + // Ctrl + Enter + var cell, $cell, _cell, cellData = []; + for (var i = 0; i < target.length; i++) { + cell = target[i]; $cell = $(cell); _cell = table.cell(cell); + _cell.data($cell.data('input').val()); + HTMLWidgets.shinyMode && cellData.push(cellInfo(cell)); + removeInput($cell, false); + } + if (HTMLWidgets.shinyMode) { + changeInput('cell_edit', cellData, 'DT.cellInfo', null, {priority: "event"}); + } + if (!server) table.draw(false); + } + }); + })(target[i], this); + } + }); + + // interaction with shiny + if (!HTMLWidgets.shinyMode && !crosstalkOptions.group) return; + + var methods = {}; + var shinyData = {}; + + methods.updateCaption = function(caption) { + if (!caption) return; + $table.children('caption').replaceWith(caption); + } + + // register clear functions to remove input values when the table is removed + instance.clearInputs = {}; + + var changeInput = function(id, value, type, noCrosstalk, opts) { + var event = id; + id = el.id + '_' + id; + if (type) id = id + ':' + type; + // do not update if the new value is the same as old value + if (event !== 'cell_edit' && !/_clicked$/.test(event) && shinyData.hasOwnProperty(id) && shinyData[id] === JSON.stringify(value)) + return; + shinyData[id] = JSON.stringify(value); + if (HTMLWidgets.shinyMode && Shiny.setInputValue) { + Shiny.setInputValue(id, value, opts); + if (!instance.clearInputs[id]) instance.clearInputs[id] = function() { + Shiny.setInputValue(id, null); + } + } + + // HACK + if (event === "rows_selected" && !noCrosstalk) { + if (crosstalkOptions.group) { + var keys = crosstalkOptions.key; + var selectedKeys = null; + if (value) { + selectedKeys = []; + for (var i = 0; i < value.length; i++) { + // The value array's contents use 1-based row numbers, so we must + // convert to 0-based before indexing into the keys array. + selectedKeys.push(keys[value[i] - 1]); + } + } + instance.ctselectHandle.set(selectedKeys); + } + } + }; + + var addOne = function(x) { + return x.map(function(i) { return 1 + i; }); + }; + + var unique = function(x) { + var ux = []; + $.each(x, function(i, el){ + if ($.inArray(el, ux) === -1) ux.push(el); + }); + return ux; + } + + // change the row index of a cell + var tweakCellIndex = function(cell) { + var info = cell.index(); + // some cell may not be valid. e.g, #759 + // when using the RowGroup extension, datatables will + // generate the row label and the cells are not part of + // the data thus contain no row/col info + if (info === undefined) + return {row: null, col: null}; + if (server) { + info.row = DT_rows_current[info.row]; + } else { + info.row += 1; + } + return {row: info.row, col: info.column}; + } + + var cleanSelectedValues = function() { + changeInput('rows_selected', []); + changeInput('columns_selected', []); + changeInput('cells_selected', transposeArray2D([]), 'shiny.matrix'); + } + // #828 we should clean the selection on the server-side when the table reloads + cleanSelectedValues(); + + // a flag to indicates if select extension is initialized or not + var flagSelectExt = table.settings()[0]._select !== undefined; + // the Select extension should only be used in the client mode and + // when the selection.mode is set to none + if (data.selection.mode === 'none' && !server && flagSelectExt) { + var updateRowsSelected = function() { + var rows = table.rows({selected: true}); + var selected = []; + $.each(rows.indexes().toArray(), function(i, v) { + selected.push(v + 1); + }); + changeInput('rows_selected', selected); + } + var updateColsSelected = function() { + var columns = table.columns({selected: true}); + changeInput('columns_selected', columns.indexes().toArray()); + } + var updateCellsSelected = function() { + var cells = table.cells({selected: true}); + var selected = []; + cells.every(function() { + var row = this.index().row; + var col = this.index().column; + selected = selected.concat([[row + 1, col]]); + }); + changeInput('cells_selected', transposeArray2D(selected), 'shiny.matrix'); + } + table.on('select deselect', function(e, dt, type, indexes) { + updateRowsSelected(); + updateColsSelected(); + updateCellsSelected(); + }) + } + + var selMode = data.selection.mode, selTarget = data.selection.target; + var selDisable = data.selection.selectable === false; + if (inArray(selMode, ['single', 'multiple'])) { + var selClass = inArray(data.style, ['bootstrap', 'bootstrap4']) ? 'active' : 'selected'; + // selected1: row indices; selected2: column indices + var initSel = function(x) { + if (x === null || typeof x === 'boolean' || selTarget === 'cell') { + return {rows: [], cols: []}; + } else if (selTarget === 'row') { + return {rows: $.makeArray(x), cols: []}; + } else if (selTarget === 'column') { + return {rows: [], cols: $.makeArray(x)}; + } else if (selTarget === 'row+column') { + return {rows: $.makeArray(x.rows), cols: $.makeArray(x.cols)}; + } + } + var selected = data.selection.selected; + var selected1 = initSel(selected).rows, selected2 = initSel(selected).cols; + // selectable should contain either all positive or all non-positive values, not both + // positive values indicate "selectable" while non-positive values means "nonselectable" + // the assertion is performed on R side. (only column indicides could be zero which indicates + // the row name) + var selectable = data.selection.selectable; + var selectable1 = initSel(selectable).rows, selectable2 = initSel(selectable).cols; + + // After users reorder the rows or filter the table, we cannot use the table index + // directly. Instead, we need this function to find out the rows between the two clicks. + // If user filter the table again between the start click and the end click, the behavior + // would be undefined, but it should not be a problem. + var shiftSelRowsIndex = function(start, end) { + var indexes = server ? DT_rows_all : table.rows({ search: 'applied' }).indexes().toArray(); + start = indexes.indexOf(start); end = indexes.indexOf(end); + // if start is larger than end, we need to swap + if (start > end) { + var tmp = end; end = start; start = tmp; + } + return indexes.slice(start, end + 1); + } + + var serverRowIndex = function(clientRowIndex) { + return server ? DT_rows_current[clientRowIndex] : clientRowIndex + 1; + } + + // row, column, or cell selection + var lastClickedRow; + if (inArray(selTarget, ['row', 'row+column'])) { + // Get the current selected rows. It will also + // update the selected1's value based on the current row selection state + // Note we can't put this function inside selectRows() directly, + // the reason is method.selectRows() will override selected1's value but this + // function will add rows to selected1 (keep the existing selection), which is + // inconsistent with column and cell selection. + var selectedRows = function() { + var rows = table.rows('.' + selClass); + var idx = rows.indexes().toArray(); + if (!server) { + selected1 = addOne(idx); + return selected1; + } + idx = idx.map(function(i) { + return DT_rows_current[i]; + }); + selected1 = selMode === 'multiple' ? unique(selected1.concat(idx)) : idx; + return selected1; + } + // Change selected1's value based on selectable1, then refresh the row state + var onlyKeepSelectableRows = function() { + if (selDisable) { // users can't select; useful when only want backend select + selected1 = []; + return; + } + if (selectable1.length === 0) return; + var nonselectable = selectable1[0] <= 0; + if (nonselectable) { + // should make selectable1 positive + selected1 = $(selected1).not(selectable1.map(function(i) { return -i; })).get(); + } else { + selected1 = $(selected1).filter(selectable1).get(); + } + } + // Change selected1's value based on selectable1, then + // refresh the row selection state according to values in selected1 + var selectRows = function(ignoreSelectable) { + if (!ignoreSelectable) onlyKeepSelectableRows(); + table.$('tr.' + selClass).removeClass(selClass); + if (selected1.length === 0) return; + if (server) { + table.rows({page: 'current'}).every(function() { + if (inArray(DT_rows_current[this.index()], selected1)) { + $(this.node()).addClass(selClass); + } + }); + } else { + var selected0 = selected1.map(function(i) { return i - 1; }); + $(table.rows(selected0).nodes()).addClass(selClass); + } + } + table.on('mousedown.dt', 'tbody tr', function(e) { + var $this = $(this), thisRow = table.row(this); + if (selMode === 'multiple') { + if (e.shiftKey && lastClickedRow !== undefined) { + // select or de-select depends on the last clicked row's status + var flagSel = !$this.hasClass(selClass); + var crtClickedRow = serverRowIndex(thisRow.index()); + if (server) { + var rowsIndex = shiftSelRowsIndex(lastClickedRow, crtClickedRow); + // update current page's selClass + rowsIndex.map(function(i) { + var rowIndex = DT_rows_current.indexOf(i); + if (rowIndex >= 0) { + var row = table.row(rowIndex).nodes().to$(); + var flagRowSel = !row.hasClass(selClass); + if (flagSel === flagRowSel) row.toggleClass(selClass); + } + }); + // update selected1 + if (flagSel) { + selected1 = unique(selected1.concat(rowsIndex)); + } else { + selected1 = selected1.filter(function(index) { + return !inArray(index, rowsIndex); + }); + } + } else { + // js starts from 0 + shiftSelRowsIndex(lastClickedRow - 1, crtClickedRow - 1).map(function(value) { + var row = table.row(value).nodes().to$(); + var flagRowSel = !row.hasClass(selClass); + if (flagSel === flagRowSel) row.toggleClass(selClass); + }); + } + e.preventDefault(); + } else { + $this.toggleClass(selClass); + } + } else { + if ($this.hasClass(selClass)) { + $this.removeClass(selClass); + } else { + table.$('tr.' + selClass).removeClass(selClass); + $this.addClass(selClass); + } + } + if (server && !$this.hasClass(selClass)) { + var id = DT_rows_current[thisRow.index()]; + // remove id from selected1 since its class .selected has been removed + if (inArray(id, selected1)) selected1.splice($.inArray(id, selected1), 1); + } + selectedRows(); // update selected1's value based on selClass + selectRows(false); // only keep the selectable rows + changeInput('rows_selected', selected1); + changeInput('row_last_clicked', serverRowIndex(thisRow.index()), null, null, {priority: 'event'}); + lastClickedRow = serverRowIndex(thisRow.index()); + }); + selectRows(false); // in case users have specified pre-selected rows + // restore selected rows after the table is redrawn (e.g. sort/search/page); + // client-side tables will preserve the selections automatically; for + // server-side tables, we have to *real* row indices are in `selected1` + changeInput('rows_selected', selected1); + if (server) table.on('draw.dt', function(e) { selectRows(false); }); + methods.selectRows = function(selected, ignoreSelectable) { + selected1 = $.makeArray(selected); + selectRows(ignoreSelectable); + changeInput('rows_selected', selected1); + } + } + + if (inArray(selTarget, ['column', 'row+column'])) { + if (selTarget === 'row+column') { + $(table.columns().footer()).css('cursor', 'pointer'); + } + // update selected2's value based on selectable2 + var onlyKeepSelectableCols = function() { + if (selDisable) { // users can't select; useful when only want backend select + selected2 = []; + return; + } + if (selectable2.length === 0) return; + var nonselectable = selectable2[0] <= 0; + if (nonselectable) { + // need to make selectable2 positive + selected2 = $(selected2).not(selectable2.map(function(i) { return -i; })).get(); + } else { + selected2 = $(selected2).filter(selectable2).get(); + } + } + // update selected2 and then + // refresh the col selection state according to values in selected2 + var selectCols = function(ignoreSelectable) { + if (!ignoreSelectable) onlyKeepSelectableCols(); + // if selected2 is not a valide index (e.g., larger than the column number) + // table.columns(selected2) will fail and result in a blank table + // this is different from the table.rows(), where the out-of-range indexes + // doesn't affect at all + selected2 = $(selected2).filter(table.columns().indexes()).get(); + table.columns().nodes().flatten().to$().removeClass(selClass); + if (selected2.length > 0) + table.columns(selected2).nodes().flatten().to$().addClass(selClass); + } + var callback = function() { + var colIdx = selTarget === 'column' ? table.cell(this).index().column : + $.inArray(this, table.columns().footer()), + thisCol = $(table.column(colIdx).nodes()); + if (colIdx === -1) return; + if (thisCol.hasClass(selClass)) { + thisCol.removeClass(selClass); + selected2.splice($.inArray(colIdx, selected2), 1); + } else { + if (selMode === 'single') $(table.cells().nodes()).removeClass(selClass); + thisCol.addClass(selClass); + selected2 = selMode === 'single' ? [colIdx] : unique(selected2.concat([colIdx])); + } + selectCols(false); // update selected2 based on selectable + changeInput('columns_selected', selected2); + } + if (selTarget === 'column') { + $(table.table().body()).on('click.dt', 'td', callback); + } else { + $(table.table().footer()).on('click.dt', 'tr th', callback); + } + selectCols(false); // in case users have specified pre-selected columns + changeInput('columns_selected', selected2); + if (server) table.on('draw.dt', function(e) { selectCols(false); }); + methods.selectColumns = function(selected, ignoreSelectable) { + selected2 = $.makeArray(selected); + selectCols(ignoreSelectable); + changeInput('columns_selected', selected2); + } + } + + if (selTarget === 'cell') { + var selected3 = [], selectable3 = []; + if (selected !== null) selected3 = selected; + if (selectable !== null && typeof selectable !== 'boolean') selectable3 = selectable; + var findIndex = function(ij, sel) { + for (var i = 0; i < sel.length; i++) { + if (ij[0] === sel[i][0] && ij[1] === sel[i][1]) return i; + } + return -1; + } + // Change selected3's value based on selectable3, then refresh the cell state + var onlyKeepSelectableCells = function() { + if (selDisable) { // users can't select; useful when only want backend select + selected3 = []; + return; + } + if (selectable3.length === 0) return; + var nonselectable = selectable3[0][0] <= 0; + var out = []; + if (nonselectable) { + selected3.map(function(ij) { + // should make selectable3 positive + if (findIndex([-ij[0], -ij[1]], selectable3) === -1) { out.push(ij); } + }); + } else { + selected3.map(function(ij) { + if (findIndex(ij, selectable3) > -1) { out.push(ij); } + }); + } + selected3 = out; + } + // Change selected3's value based on selectable3, then + // refresh the cell selection state according to values in selected3 + var selectCells = function(ignoreSelectable) { + if (!ignoreSelectable) onlyKeepSelectableCells(); + table.$('td.' + selClass).removeClass(selClass); + if (selected3.length === 0) return; + if (server) { + table.cells({page: 'current'}).every(function() { + var info = tweakCellIndex(this); + if (findIndex([info.row, info.col], selected3) > -1) + $(this.node()).addClass(selClass); + }); + } else { + selected3.map(function(ij) { + $(table.cell(ij[0] - 1, ij[1]).node()).addClass(selClass); + }); + } + }; + table.on('click.dt', 'tbody td', function() { + var $this = $(this), info = tweakCellIndex(table.cell(this)); + if ($this.hasClass(selClass)) { + $this.removeClass(selClass); + selected3.splice(findIndex([info.row, info.col], selected3), 1); + } else { + if (selMode === 'single') $(table.cells().nodes()).removeClass(selClass); + $this.addClass(selClass); + selected3 = selMode === 'single' ? [[info.row, info.col]] : + unique(selected3.concat([[info.row, info.col]])); + } + selectCells(false); // must call this to update selected3 based on selectable3 + changeInput('cells_selected', transposeArray2D(selected3), 'shiny.matrix'); + }); + selectCells(false); // in case users have specified pre-selected columns + changeInput('cells_selected', transposeArray2D(selected3), 'shiny.matrix'); + + if (server) table.on('draw.dt', function(e) { selectCells(false); }); + methods.selectCells = function(selected, ignoreSelectable) { + selected3 = selected ? selected : []; + selectCells(ignoreSelectable); + changeInput('cells_selected', transposeArray2D(selected3), 'shiny.matrix'); + } + } + } + + // expose some table info to Shiny + var updateTableInfo = function(e, settings) { + // TODO: is anyone interested in the page info? + // changeInput('page_info', table.page.info()); + var updateRowInfo = function(id, modifier) { + var idx; + if (server) { + idx = modifier.page === 'current' ? DT_rows_current : DT_rows_all; + } else { + var rows = table.rows($.extend({ + search: 'applied', + page: 'all' + }, modifier)); + idx = addOne(rows.indexes().toArray()); + } + changeInput('rows' + '_' + id, idx); + }; + updateRowInfo('current', {page: 'current'}); + updateRowInfo('all', {}); + } + table.on('draw.dt', updateTableInfo); + updateTableInfo(); + + // state info + table.on('draw.dt column-visibility.dt', function() { + changeInput('state', table.state()); + }); + changeInput('state', table.state()); + + // search info + var updateSearchInfo = function() { + changeInput('search', table.search()); + if (filterRow) changeInput('search_columns', filterRow.toArray().map(function(td) { + return $(td).find('input').first().val(); + })); + } + table.on('draw.dt', updateSearchInfo); + updateSearchInfo(); + + var cellInfo = function(thiz) { + var info = tweakCellIndex(table.cell(thiz)); + info.value = table.cell(thiz).data(); + return info; + } + // the current cell clicked on + table.on('click.dt', 'tbody td', function() { + changeInput('cell_clicked', cellInfo(this), null, null, {priority: 'event'}); + }) + changeInput('cell_clicked', {}); + + // do not trigger table selection when clicking on links unless they have classes + table.on('mousedown.dt', 'tbody td a', function(e) { + if (this.className === '') e.stopPropagation(); + }); + + methods.addRow = function(data, rowname, resetPaging) { + var n = table.columns().indexes().length, d = n - data.length; + if (d === 1) { + data = rowname.concat(data) + } else if (d !== 0) { + console.log(data); + console.log(table.columns().indexes()); + throw 'New data must be of the same length as current data (' + n + ')'; + }; + table.row.add(data).draw(resetPaging); + } + + methods.updateSearch = function(keywords) { + if (keywords.global !== null) + $(table.table().container()).find('input[type=search]').first() + .val(keywords.global).trigger('input'); + var columns = keywords.columns; + if (!filterRow || columns === null) return; + filterRow.toArray().map(function(td, i) { + var v = typeof columns === 'string' ? columns : columns[i]; + if (typeof v === 'undefined') { + console.log('The search keyword for column ' + i + ' is undefined') + return; + } + // Update column search string and values on linked filter widgets. + // 'input' for factor and char filters, 'change' for numeric filters. + $(td).find('input').first().val(v).trigger('input', [true]).trigger('change'); + }); + table.draw(); + } + + methods.hideCols = function(hide, reset) { + if (reset) table.columns().visible(true, false); + table.columns(hide).visible(false); + } + + methods.showCols = function(show, reset) { + if (reset) table.columns().visible(false, false); + table.columns(show).visible(true); + } + + methods.colReorder = function(order, origOrder) { + table.colReorder.order(order, origOrder); + } + + methods.selectPage = function(page) { + if (table.page.info().pages < page || page < 1) { + throw 'Selected page is out of range'; + }; + table.page(page - 1).draw(false); + } + + methods.reloadData = function(resetPaging, clearSelection) { + // empty selections first if necessary + if (methods.selectRows && inArray('row', clearSelection)) methods.selectRows([]); + if (methods.selectColumns && inArray('column', clearSelection)) methods.selectColumns([]); + if (methods.selectCells && inArray('cell', clearSelection)) methods.selectCells([]); + table.ajax.reload(null, resetPaging); + } + + // update table filters (set new limits of sliders) + methods.updateFilters = function(newProps) { + // loop through each filter in the filter row + filterRow.each(function(i, td) { + var k = i; + if (filterRow.length > newProps.length) { + if (i === 0) return; // first column is row names + k = i - 1; + } + // Update the filters to reflect the updated data. + // Allow "falsy" (e.g. NULL) to signify a no-op. + if (newProps[k]) { + setFilterProps(td, newProps[k]); + } + }); + }; + + table.shinyMethods = methods; + }, + resize: function(el, width, height, instance) { + if (instance.data) this.renderValue(el, instance.data, instance); + + // dynamically adjust height if fillContainer = TRUE + if (instance.fillContainer) + this.fillAvailableHeight(el, height); + + this.adjustWidth(el); + }, + + // dynamically set the scroll body to fill available height + // (used with fillContainer = TRUE) + fillAvailableHeight: function(el, availableHeight) { + + // see how much of the table is occupied by header/footer elements + // and use that to compute a target scroll body height + var dtWrapper = $(el).find('div.dataTables_wrapper'); + var dtScrollBody = $(el).find($('div.dataTables_scrollBody')); + var framingHeight = dtWrapper.innerHeight() - dtScrollBody.innerHeight(); + var scrollBodyHeight = availableHeight - framingHeight; + + // we need to set `max-height` to none as datatables library now sets this + // to a fixed height, disabling the ability to resize to fill the window, + // as it will be set to a fixed 100px under such circumstances, e.g., RStudio IDE, + // or FlexDashboard + // see https://github.com/rstudio/DT/issues/951#issuecomment-1026464509 + dtScrollBody.css('max-height', 'none'); + // set the height + dtScrollBody.height(scrollBodyHeight + 'px'); + }, + + // adjust the width of columns; remove the hard-coded widths on table and the + // scroll header when scrollX/Y are enabled + adjustWidth: function(el) { + var $el = $(el), table = $el.data('datatable'); + if (table) table.columns.adjust(); + $el.find('.dataTables_scrollHeadInner').css('width', '') + .children('table').css('margin-left', ''); + } +}); + + if (!HTMLWidgets.shinyMode) return; + + Shiny.addCustomMessageHandler('datatable-calls', function(data) { + var id = data.id; + var el = document.getElementById(id); + var table = el ? $(el).data('datatable') : null; + if (!table) { + console.log("Couldn't find table with id " + id); + return; + } + + var methods = table.shinyMethods, call = data.call; + if (methods[call.method]) { + methods[call.method].apply(table, call.args); + } else { + console.log("Unknown method " + call.method); + } + }); + +})(); diff --git a/articles/annotate_mixtures_files/figure-html/unnamed-chunk-10-1.png b/articles/annotate_mixtures_files/figure-html/unnamed-chunk-10-1.png index a529faddbe0e023db90e0093decb7278cdaefb43..5aa4dbce0b4cf356b93d2a30f68f80fd123153f3 100644 GIT binary patch literal 35804 zcmeFa2UL~k)-Al%sK-jMAw5xSpdbo@RE-Eh1XMsonu_!$y_dv9ks3ij0YOEigMjq1 zv4IppdIuHhy_Ze-=d(3A-@V`ewKLB7$Nk5+jATG4Z+XgEbIvu_lRM`WWHzm5Sx=!* zHp%`hb%8=zZBL=By85>@_>Shv8ZCTUb4^Z03ZL;YcPiHnpVr;@S5?fI`S(LPlB!m#Ga zX|Em3e&gfgEK_dFi!*7{+NsxNiwYVv?KF#ASFPFjt}*$^lU)P%mnS5bl}08K4L60R znKY%|7cjiw&363AbemnryRtH`yu7^Lwp=^s5vLBXwVcj}gZQ+yMlKIT<}}#!*TxF= zYhS(`lD>SjQY;|FTun`_-eq9kemJ|w$Rh@;-D~0Y^qpvn}rWA8{J$drvvd;zzS4T$3K)s%?VUoJr>C>mvL$qCw3$Y2`xT0a( zq&rqE$A@p8e(0yIp~0poxoGXpe01-cjXS@8C}QIobVyygr{bZ`$HHGM`l5o1@HAUl zh0GEf9D;&^it-#LKF<#%`#nB&TY6@=Ez)M7{%&wEQ@-Prr;?($;BZ!_ul?dw!+?zh z<($%cmRw3|d`yf-b#?WckqFaY&tJS4lwvL`D_d`2ETU&x_v}`(`U`{e-W=j0-S0z# zZD=j#RCaUfqOI6`ZzLllzy0XPoszE3gSjIGQ#?7F i;#`Odx~ z%thP& ;vzI96 zSd%{PN#W+^{u~#d 4J=fMMt zgdQ`!%;r4$G6k0q@f-8fUf3n=%XZ+)YKpCayhW0_`o4$O7o6w&qC?wy?(7Tqc+DUw zR-dG)9<7sbIh}icdMNj8)VtEMvd-Q}Ki}0GcGUI>Gcu;>z4z?&6|E3sa!-7 9b=*7nwAjq^#-!^Pi~Kk_*K*ij(QqDns~%r_`S?iQ z#I^a!KJ}@c#q4$(lOr9}`o-y1t4gEHeC#BP{@Cb?x#B`XLS$25!}_HgHxzX&FU^n{ zuXo{eTNuk4D`M@u#cCE~<3pjypHvDRaP1N8xYM5Rr2irrkFHmKpOMpf^kh*67Jt@U zcW7r)?r^C{w-;Z*&gh4>m(z<)u-4cdzTB Shn9>v&pmKp=j~^)IcZJvyTj?ivV6XEM>Vh+p6=I zwHzLti@h>+^_wU*{(89rx~2E5rLK#WmX;dTx|R6vQKKy06!n8tIS} z#|E=@aBz^Oc9jK3Cue>3G+Jg4;?-<^WV4de;%6$@kocahMk8}+X|B>uFYyy*x6Oq5 z;HdsHrC4QY>czy%x-N RVO}&xlA{y zwTzG$HkeXO_CIqAhUHrMeSQ)%bxlYta~jT$emRg3kGPmtG;rILfj9e$f8+D3r=)4R z!J0cJy5D=f-Yk0XLTovwpk}VEPm5XZAuX4wIs;61(X{sRe1m$F7R7x?O(|kP&+65> zeQ&V!!m0`^I*VozZH_j_tFN`g9Z4AK?V!39ZF+wpR+%i6F+aD3-4(}s!n_N3RFlf* z*QFxbCzQp=KNJyc&NSB^I?nFc` -W;azewgFN%2x5z63?d6rjBBl%1W60=uyg3s^fxtQQ_o6}V|a
!S`}DdfB@KsY3ij(66npI)BzTaFpq*;M}n4>xyQUE2-T=96pLmAmHNGHGk{ zgq}8$Yx=cGnS1-zeG27SM%S#Qy>~K_T^8m%UJ~fn=_e_n^Xn$chkJr`*=F+A!>z}S zoh&RY0(;2sH|`5O^LBBjL%VZMpF6PB@$OH^jSCHKi>iSc) gD8{(Iz(G^-<6BsceW-j(5q z-7+3@@7|m3sv0S6+NS)j3$xxC=Od--`lmc*KcDfGT%N1kq_Tr@Vi)}Zl{2N9R0kBI zYsPXr7I}3dsv>2ID?~>$;yZ_s c;KU9_IuxZdtn_hm&;QBp-_~bbt z7QM9MCc6$7oer0oL={uYIrWw8zZsIeG;Qq3Y1)DHT0a$6Fq1!0XB38yM&smsmbip9 z7KJO;zPZn@^LaRTB*`RH*y0*N3@t;y-N4_fNov{FtLY2=BuF`-bS0@ppeILgtleou z*R;blvyGdom2IiJZToiJL96Zu`t^PHd%~;~qziLze0sH?mp93o&$hqzK#tuA2dC@o z)xgEQis5WZ&(7Y#IK1tPj^J3!Zhg*rs9Dg`%4$G)1Eu!%9+oZ7Uc7iA?Z?&jH2xVq z7??_abYGQWu}8gFUZ`ywVu(s}g~&ic0_TQ!zfZ4k%XsZLWHdEP>sSuwuV`pEMXScz zZFn@bk|Mg %~<3Og^+ tEXGUr2{l?EjTtXSJTdvv%4T_8Q}w2O>({Tp*8IXy zP-NJl=OGijsBJ=30fi!4U)z(@gn0HQRk!SOh;h=LSfyw^ljUJMD(m}@#SS+$)r3oT zZNhRYnBr-fni%uJv`UG<%G;Y*cOa>&YdCZTs MxNW0z(t FAJd+VV;xghiR;Imlq}_2Ku`~l$6O{k}Gr(rveKE-J^{q{6 zEmV g$fqwWMWU+jOsg);ezRO!@ z)fK3EQIv5mjzMza@W8{X0SP%1W@n6_M_q`Wtz2HH6rTKWil@cX)AMsii<#FzGPQt_ zr%YM%qsGOH7b9o(Qwkd%4CooHw`$9A{&g^KqQYwxgV^&k>Nds#NE5YvrH%bYal`2Z ziI;~RKZ@kUPNH^nxFdHsGQg!gdkY`mnR<2ew@b7UPPw3ihSjQitDX<7v)|esq;3{l zr5zLc`DrGgw_1yrr>BuiL+en}kGI*jbRal%R=O=&>9!D@4#em>_@YK(ienim#cOGi z))1$dY1Ve6IZoa4YFB9>2^IBG0+cIj^^-zlW8d2te?|uI8|y6Af8k~$-ky9p-_hpb zk`FRoU^>aW7+<8Vq7RQx>GXbh%sBRT^=6WU^MDV>N_mpsASzrNZp~JsC23P7D#T}6 z?XmKVHTfvYGGxk`_7?z8sMNnRT2By>qS%;J*LK$J+qYXR&Gi%mbX*)saT^bac=P<~ z>wa+s;QO=;K8>t%;8W-1M5{G2MTVNwy@1ihD gX zvq49JOQP`_ca!h(xTIS$Om* sD1O#n>eBw)sN?s(aS6fk zbcBRUzY811 gxy}o77k+Cq4_*We*b= zyf{A Zq i6-5e2&G2J8Zs}!rdMR$E!%cEP#F#=6 z^=Ah)Rb|q+aZ*|xKvBcG>a-w7BsjF|=uOV+ZMH}t7ab%p7g=BY<>JI#mnI?_;<~|~ zS<+XW&|GIPd(U-o{w;?0VU;V=?pyHEjK*co `>XXf1uQM=_Jw^9EMpH` zlgJ_N_y(CKysF^J%U@?_+?Hn=v0!!-?>~6Z`xJxt3)$8mStySrY+q!XX%410Xb~Qu z_hPwE(ajS SwQEe>++@(@8Dofag@BWCr-uD|=kd-0iK1jk4L&CI7?Gs8r! z$X}1~Q}}j@0Mv;){s947Nm{NqUA1 TKV zd7DZu_9=Fbm+|?%yt%fR#6d>wR(3mV?eiBd_}k=ZFZQceMacz=)TJIGZ$nt(TXQ#B z%Nphp1X#b!cE@-H7ry+-YVUQ!fH`bO^~ycWT?W*dX>JG=1nx)!L2d+mcbpk!fBW|B zSSEGxki_iAo8F7ydj{f1C|7c?CFL$+A!rg>r)Dl81QaaX`{?EqEKh#SPj79k5(x%l z$dyI6HnGOBPb9l78e{tRM&@G!TMVT)6aiBxN9D)F?4O?*q4Py=7D4XK!XMp9eA>pu zBm)w(08T;@m%ezt%S@YU+0ixb@3)R#FeeYqd{nPCt`Z|~p-0yI4U^D?OHhgGWtAX! zh%HXmj7`=knp=dCSGT~J*Jt~hb~R9!Y{c*;DS-cueu9vJ?u*zMJ)GCVnovu=LgJ>_ z_`BWA#z8Kzd3j>p6%SdhdMerm8#@-dg*p*R5|vCT6!n|g!_$509rG6#J~6nx%Q3T% zdV^i8Q+n@!Ad7K#`GcxV^A4j-ZMVg#yActb$@$Zli!um^5p2* VGe)H%XK)oA zbp8AkB%AO<6-}JU?iF%!_ZOT|+2UOU_HYLy{Im7!q &xnzSZG-tRXS}$5Xf7fGlq#k;jMj^g?Xb zcqvaOR0nzhkaj^l-C8L=qd^3ybgl8vf1KKUCm7;ckq>L;sl2@u6Q>}9AI5w!C-xKX zg*t~aJA6R}iV^Nulc`IN371lKg$Nq&AW8}%iohBM4z^-WH)q7|$P~Gx08kXN>Dg=> zuli_~>ZO~u93z2$_AQ?)BSA#uS`(F$oX{r}?%lg5g%qq)=)QvAd3IE|>z)!xHGwXo z*1g|D`-^vOudVl5MTtpd*G*FA1bp=FSRA&qXfe%vOK2v7lXTu>wOq>e68}q7SC<#& z9TEuW+hsT$u5V-)5%4|UeP2X#Dipzqq$Tg2{JYwnC*Ip5=zWd~*79u7b`u8`V}bNo z1wE({!e=LlwI|u-A79?o0oH6;ufK`1yl>w_g=p5?ztJ^SqspFB^e_Is3~L!6IO|DW z=;Bi`8?QU3aJ=gt*_Ai_pw qx7 z(y-e7S;>}7^sBGyTK`SlwOswLlz;yT3i3aEi8wVKro!o-$G15=eOQI0rAY4LfJAHb z#j}y)UQm#<$U|Qb5A2rH++8E5OtcVYx*84vbH6#$I4pnv`@Z8=yEM &Px zzgK+^*@#=arM0yc8mH+OPlN9R_MSd1D_b|x!Xds(K_dI^hR7dh3rtxotF`wY8>Nca z^lt-$W@H>|Hclq!hb4Pg###a>79K}HD}Em`NBh1G*@+;((jEe=KtQDvJcgQoye*YC zZ=^>{P-8rG%Y^3WF11t4{zmzOqx*L4;(!9OQ_x7cbBEM>>mimg0K1m?89)2EuA anJ**&Gi?YsfZ+HsmnyE zkLc=BF)S-7A*!cLFt29Dg+R&&fvB23`>8rr9S8 O?~Z)vF~N72D76th_AA^IhvPX{%ST UfpsdIUg<}lG+1!i2jv}eyA^~|3s2CoX{bA=&{h6u*Hu${YnnFWl7xpT#bER=Z) zkJTlprIGyIHGvhJKugD~RWIjiLpva{bIVf+ia|e8NBjbBQY_wZ_9joE?Ylpt%zvXm z^j}TFKk*VVdS8oQe;kZgP4?Y!=(4gQ8O(cufv2V3AVWoBXw+yKc*D!-M(K6N-!-29 zn?38h{<`>1AY~td)5d_fiV3YH36SWWbxRk#JV=p=C_Z#XuBD|#WV10M4o_nXzvoC! zT13!vp~hw6&FHz0Ts`}$hXL%Sq%S;}vFSXkbK9k+U-6s<5e5wR>rWP}(
hJxXv5i^jZBpa_eZl(#ex22|YnpC!z{!Njp$o@Pvk341Ju#i;2&718>$3ksZIS z+j_0~sVqRQlNA1j VLcp)z=@I!+~?RBIABw6&{OxZnEm zV`4P<4XHN>I-O4g6g;tLJUn^ #w;gNQZk! +0%Cfjl4# z3f+mqiN`Oi)?Uo!o-Vs#VrBQeDHJm)XA7P5`DrXK3MFK(>S+DzV9t}R&__T2&ZHHy zQFZq&5OYQ--F~>L(E1GpNxMz(%|9{Riheg7h_B{!A9P$nPXj& zV+y_KJU$$JC5&XHx@Cs*`5OI`3-t4}4SHwXXSPu&g-jT;4 cOgW3}lH*Nf8QIH=KqkM9>=VD&uJZW82EexO(;Kx@q<2S56i++6=8F zqbDH0vfWV_CFNbau8n hGZE1pa+zRrhN-$K^?=f|H;+!<{EQ9ahXT+K z6=D TmpdIHdZM3q~c*)PYt#wv5)V&LR>eVd_U@3(P(yZyg! ztoRq^%@9Fb5)uo$w=*#nhng2KW7FI8M}VYc?V@Bb(QP4(6^FD7#6VDM8g(hp+36OM zLfa%ns?rI-(aV0HxV6YaB36=h)sPcO{Vs9dr1`}GEcDz!?v;=DvrAaWZ2pU!C;!3$ z`XBgT{ Ms3E5ckMRiCmRs6Rx00uMyX zBO{7C`)%UId1r)TG|`8%KK{CacrWgPjFMb)XX`#`QW=1;qO6(kzl04AtP$&w!RS!y z2*MOE5pS_W#%dy#1Te>2Bixo8z?g~#o6;ho2#p0qEO3!Zp6=ws!&iSI#Yrd}e*5?D z_we`kCp= FwcxoP2Cx8Un~+B0~{L@t|7j(^MA-`$Q!ojA(MO>1bp}%=t)yH-K-ip+Kb`ZI5B~ zAj2Aiy6`Si!TE|@(rN6_8vs+>mKI7gNSR~TF3|nE>5AY %_H 7a5@7x8_Ik8Pjz7w5!8Xj%16iy1LbpFT~IGdKq* zUtqp Ym^Mc1=Ir0YUJ!Z??l8(n9)N}C2k-yQ$2`I+nHizSWBpD{GF~y>3 zWW6E;OLj&^RK^ob+~5P+y^D@dZ3p?1*K1{%H0c==NQ%0+iC6lwnm+CUN`ROamECWo z<;%ppptx4emsBYv?LR&-QD=c_S|QVtnpo<%XH#lw>iwho3T&3|ehj2tL}kuZ6UHe< zMsV#I+d*QO0!p~Pit@-+<0az7bu1-u5_tJB)mg(i1D|sSlDw3;RhYXufV;+SaxVNZ z3<2K&rEgtoeWJR|8>W*tYzh`)fZyK>slV288rr^riMKpw2m+svs+Pl^UAqVnfcld- zS5jqQX<4vza)dAFyRmuGrl10C|7SpAS~I`Bn|28e4IP*=-bL!wbE3Hbdh)~i@~7mY zGAfOAQLm$%xClb#LohSY2R#$`NzCuxEH+w5w~%=st?*zq%K`!fNIBmW4GRRS16fT` z+Y2 tM3g0 w>#u!qlpe-{+;ABhBD@F`UWXN$VJ$jIWnZbrO zJJuPAB}(8kxAEv^7J>aFF~g&GURn8Gl{rERwCyb<@oQpZ#Md#j1OP4(RYfOw#fhz7 z15ub|>47Tb3X--_d)<9TPHy*Suh3}m(ZW?0C3}LUpr(F?;GIyN49ZJtJ02dwLoaHs zp@tLK(hgKh%wbrHsN=Wvp1nf*Lm?U$nBol;GS?cAQBuFj )qY;6gJSQ2KS~CkXHLBcX=~mdAiN^C*9JqB{Fm61wBS>c}v@g;vNyj4m)> z2@~FcYL>b4WR*-6NNU0&p-jZs^`F6vmf5qw%Dk$?rdihyx ;=wPo=WRt?|Ao|D+6tYyEdjp#NDC{J)o=&&dD8Q~Q61^M5V_{;B<#RKg&7t*<(Y zFj^to1C&2b(lKf%aah{Jo6MTKk@BvTE?_-p2L3P_+(LPDIf*!(-C3aEcp+ca$0 yCwZ_B)_=3l{1s*L1H^Dk5F6re0u7)`czI!h6f*h2VPQM+ zRg{?H+N%(@lUS5X0`>_Rd}UyLjHAb%Lv$)0E9RuWUEF*2K4fiqPx0x--LU06dHR%# zglp(9kei79fN!!Se7*G~M7483mm67+9js5*<`f-zp_EQD1v5a-2a_d*94Kqhtns(x zm-fnsY-41UTpIIZefsqh0_*HqKfsl@lP_=dCsN=ebWb(61F=sB@s}8%dr@R8nua;* zG=iX7n%-N~mHOL=HbyZB`7UqM7ed&G({_fsyhz-+$8tyhF7o8$|0VJ$XUnQ zka4xbwU{T$04U85Z9+yx?&!_1sA<<&6)e{6CDyXiT{wUYZRC{Y? z!=^=!(QxF8* ^C$!a>x_RXfgjF~W$FNC-lNfEXDgIqSnP_7rv8nlK)@Uk z1}u=@Nl`_s(-kH^Uk^TsYnLDLNtQN4YbGKR%s9!m58H{JHNMvr6MQGbw8hN!pe-iV z!~+WsSI+O oUJWtexNnYUwlx;zxGvLbVO5T$A-+Bfp64;C&nQG`;1S4DgX(#g5o( zKi{LhJnM{WyW6d8_(Sq2g!+YzWF)kC(_$hCv;+Im0kI;+d;(F&?~6OxLYAsYBa(4; zbqpXEBjfpV=gtky5?$ZJgRF!0ziTIoKn&;6iZC{Nfyf3yK^L=~W&x@oV_-uJ-2V?G z7a0&aUuzM~A)V2WFCl;S$0wsim9*u5$4~szpe$sEt=Y`B6LwU&f1_YBlF~zwFFPwE z-M<;g%9xMBGXJ$Vc*-oS5tctUP DvK?%J$hGw*)s?k?J@D8@)65JJ~Tt(Ca6!yLLFq@WIx)%(PY<;Aycp7pC8&)k6a za?V>9vXMC@#1N6R;!_6b3N^*xG9B&zvl>nQiiMcUf3R2OpL~hXbd-y;`ksYeLt618 zJO!N)#7cs-k<;W#%H(8FL{N@~gzkln2ZO|?V{t5$E5$#Md=n=KVUE;z7Z!RXiO&P( zUG8hJl@o7LZ%d{*JQezrXzro=&mclwBM2MRD#CeO_H39iKPguS>{B%R?dS@6&?Ec> z=$(W|?1nKIh@}Z%MC3&$4F|FL9MsAa=7pNz?Mh?`*?_%oIh+QGwn*@LR8$nPKR~#G zWtkb@^tmAo^S&a2;>Q7yNmYmKg;-7q2O!2?%S$86pvGV+qswVCGgoaJrXJ2NhaElo z-~rd+6O>JMKgRs6-}A+`J(pz5OMNyN?-z`5&xqIb)X1<*SGLt0dTTaN-w#J{^GZk4 zD6{;?UC(~^IKT4cuKj!VmT7JIA!Pse>tui6I%?HDys-39bfWB!2V7Pa55guMh;cLs zHrRBBRnn}q^efs1#ClxXBQ$QjT&;8gE3@{=&x^yr+#y`qaKokAkA1G1K)+6C?r;td zd_pDQ-XVm--DbDF hjETD%9P) zVVh*ut;4>(!^0KOawp1 *+IeV8yF} Y!3C31RUM?+Vt)HN3Ppw6lIjFw z;2v3t$;S-xS?}$CoIsa}KbKrEFT34Y85tSU97Os8Km|*{+>~w`t)Ohy!aafysRxk+ zwpB|4LligNQdM={RF* f`5t{w;1j_7p>4!V$k+z0Stj7igKeDs2m2cq7J3OqLTgj57%&qgQRfuZY zcRfP1#fHJD?SWKd{%unZWnUa5+cJZK?u0Y?)LF|3)V^lc53B0kg5*%j=HWVntoZYI z^@UBu{YRXrRb?pK{vkS&cNE69;)xHZ7`nD6+rQ=L^cDW>v2I2j^%gF^M4mh$ ?;p$Lo%E48G!C`f=w{?OZ?>z3_N?OWVXWnhR{+O9^V(6J>AuYwv 1TTDsEymUE3y%kibSA{}_hpm0{VCjCi965|O%Y#+iP94OYh4s?A_s=|%BrfW z`VXc|j}Fh%XaU##x{y-u=iTD18V#Sx+lMr^)2bcoEfrNDt`gMfUN4IDO8Hyc+I9oz zloL _!@fp1dql%YB}np58Y?h55Ij zYR_3wc6N4oH|oSPbB{wyeSP;*=w3%hN9l(2k^cVX0ihcPWO-_jd8&G=CG$A+#cS-| zGy-v@Tlya2r_K2 Y%mHq@vca z&~3idoz+QP5?oCK1p(OSQE6#bugU-yU)@Y_={_2f^5`gqQgiYAd>ev+5GjXnAYq1~ z^w4Q`)bd1nL&0= c%> zYEI))%cGo2r~JPtMa(NF<@Cpi>s4>wxbX{gS8M+-XjtSBcY_h#(A4xjyIt#Dpe$4< zkLwgAA`&bvE~-7h@{$9<@UVkC(`Z3!Vc`i5>~`Xna}q!Iy6mCb(wx#Il$vT~(x!7s zK$8H5?jaWNG}Jzvhm5t$0M-1tAc#~z^|e{|AXooCT9lLQiLxFm==nPiG@|V3|q}Xmz8k5*H#jv%&l~eetX9G zHo60}KCukm!=b8GdFdG$z8p9D<;@+H&>l7Nv`csG$82bQ;~#G_=*^9H^&uj*v`=T4 zwB-n5P0IVX+IRXK$D2jxxl-J3o~+?`@DZNLN|ct=Rnnkpy-)hvqM5O=G0ZCFT~LL} zMLXsyHg4J!MWsp-J=qMJx)_=g9-s#NTPOLigBn?f;iz>( ITY*d;#6UT-fC{Zrtd7-2AQQcD0miZ(T m2re7m@u0KsUK z=jPu!jDP*sVZ2RQDd}ZPM@Prkm`;C1rlVimlGlIZ{OA5`Oa9fURM6fH#%6tc`>g)4 zePK6#LL3@@hDrb#gdD@tq){N83hauyP9UNZvh{5eu<`o8KJ29@^uqt6N8iM-)!(cp z`(vJI+?-LR&a0rENg{J#WTcYR?VWp1ah`E=b8DYUvg_&&HLrkq(t#599mT!>uem_O z#pq)Dv?ou}JJ=^eVG->?+sHd?GRxPSgw9??Y5p85eGrZu3Nk>Mu&VdqE$Y$$wE{l9 z+P}}x+3T0T%R6-Fki$f1huA!=2MtbZE<# 0*gQEg{UE#(_*?Q%3 z`@-!%Z&SgqrjyHHK{AgPeA;CGjy`tFoo)+$Hh*2{ME+T?JuF(oWPwG>K@~X=NgnFW ziw$&~5TGCuTZPR~c<%!)^*q;b>u9)LyTcs1@50dFn^P5qb>G&9)w@#>vrRPi#rBM- zs6C+l3b6vFsYVpav*X_u$_jI2hQHtBkygl&k8Ppt?d|u rM|g`S7CH-G7F> z;q`5wowN`v-m9o;1`ZDo6Uzw!wVRKB`GXkFN8FZa6DaNr<)BkPCWNdKBSD+O7UX;~ z!N;K;kK4J;*fl{GxL@Yv=s3=>Jj!q!+5p_d!B4R{PX?}}6n-HOAEQ#UCa=r#xVb}_ zx+N^5Lk-i!U7H4HJf7`9(nbo!C>rDPh$H0K$NcUP<5alyWPKn+$S37bHy?=mwhkj! z+`#jHJr{s-LL3{21`znge0kcuILxl`>g_JR<#E0VM8$5bs4{}W7Lu2zs7F8z_*-1C z{3~HKw)&wA7Ejr#KPS(1aX_1Mf(D5r0&d|R;z9x*dk~-~ah%lMmPQ$t50l1qUc@B+ z7PqN*ZSg<4@M=&0TFKyw!&Nm8A3mImcAJm3wzl@p9Rem9_c3?gjm8uf@evYV?=|2x z>b?ar=27A)kBlZMRS%|Vr?!H~$laU^AJ@vbk(S423CA$(fqzCQJCtuCqlvP=$U^!! zy+t{oV*C90;~8`;bBO2D25;pzx#YGLuR^r^>-|WOXBS5a#;&JiUqtJ`w4(TwGI6tb zFS$aLII9Lz_c@+F7tshJbkV|6P-fm94D0f-;S3aQ3|u>fTy@GFMjRV*|Cc8-wqDVh z4ZFutog(kanVEgE;mGg{^U2GOT5tvHLl|)A;?aIUq!(DHD SDt3lto2A; zLAgTxYY=mDa>fbNuYj@sSoZ?yGRB>IwTjzO$az4zID;_xg5O>%Tk4d59(u{jA{I&_ z&Ki}=NQ~cIs2EwOsNDt+h|SYKSChQIqT77U9BQZ8mCWnO6cxKI6t~`Zy$Lq0<0tA? zP)?ZA!+hL 3){T-grP(1mWV_wZp9 zg;BAeo#)E5D{$$WN2N$Pw)2{xvURm9Mt}sZP 7-t4UgFW7JEI^zRXYf|&y!~rFcp1A z-bOOy2|PpSs~AMg0G3R2Q 6Pgc1yeMX%HtvE7J%X_9A&^V)B2QBHgQ0>OPaxWK%84pV9kp1Vk$qfwV>{5>78 z?kQ;^kt)monGjJ3o$(FVLRGFE9q@`-rgCy}5~IH`T!$wovZoD;Sm`q1r9b`o2c)sr zH@rRnZd)ky6}QPrGTk{1SOZf}$kdP^I5arOCZ11S8&0f;(u#K0*0uS2F+WUyt$9!* ze#d!IHN17})^yGyLU{pv^1e$;&xG;~ON}sWm;ZV cftup$;?a19)1yuPgq=pD@Sne}_5W4pRB| z$m7qmSWbRBi{;;go xO?;@7^BD!Jntw6vm z_KsKP&PLacRoS6TE?H27H0;AxbgpEh{cR#W+2s(ZU0dgT+}Aq_;KMN|IhoJJ#YJVr z7*ZKn=SlI>mHWk@z&yGTdmYYB2b}j{Io+h+qc2HQEA6txKdwju-3t?y1fB$>RfzDc zTD3~2h$KE7#G-x#2G|5N*@DC}@7(zgyOjtRI9Ejv#d{|W{q#<}?W4QgfoIyLRd|C3 zGWgt|K>z=UW{y(S0IX0S??H=_tAg0ro2oKjl${oEbdZ%6t+_!D#e=yXx22w{GCT$; zZ*i5Nl{cUvuUk-7Yd)j|K2l*3(HY3=99AxMzJS&4deu!$4~|=PpPi3}{7@CON`QTZ z`KE&9#gTooNqOU?77b0N;NYB#Rh)Zd32UmFKnvl+f`S5KKt!KShfXVUd=TunXuItu zPE`jsyn#Rfl #H%^&leY%xzb~M%G)vMn($W+2eqDfa2AKYjqxDItVeds*U z?5vDfgu-!D%MVasS!Yt){H`}QGz5o-hp(G?{ZeOUDivDQ!c3TKUtgar@=#>iqhI>7 z71}?1_+#eT-nyoyzD01N&4*WvY@7F@&Uz6wkfb vaN`#5q^NYvrUA(BIo@ zK=|>E4~(M EyNWi-9=hTG$ zd&&8_2J{(vC+#kOnW(|*$6Iq)r)Z=0OWkCUz58wgHrCdco_zX2wUS0v=5{bQy3o!_ zp?oaF+ItC}jM{Gy7}MU@uU`{dFfQaaAvEXHG3#TI83A>e$c=znidmHe9m-mk^B0rh zA--UOS1kBwWz>Z-{uRl|>i&6?l{fFYXXfXNo0@7#+`o@ggj!tKM=l4R*)t-K$zSHI zIcTKJ8=Qu+%EBkkrK`S@OS5D?B +Hht$dVZmhw^}&+j_FVO!vt`^p_a!I(6!l8cy2qUv`U8FB2x~?uz3U zY&AeJEpoyVS>fbRsG02HA#%LYv-EU9yH*Ps;!P{=f?w<`Nb!z?mk#SKwj2GMyGJZ9 z^aOCheWhzV-1>lYOTUV^doyBB30ecJaPHDRS>A%#&x6>_ld1oUpj1AkBKqh0-xIqA zS@~ywXb>Y!JsCugB3gy-l53kyLl@~GN9d?&YkQcPKV(XBHmrrAT-oUHwL*8wNJ_+? zPvpm(r_w((#D0{=E)_EAM54J1PiBX!6FfWjiA$d>Vbm_d(cIJpgnUEu+ndn;4yy`h zCm3=f=HfU5FaWvD@4k%`|4q81=D$eg?(5LROD!dpfjqF3vk+s^ib`jlbLcM5t-QIp z12|C){jL!At*~ysp!K*MjkcT5bx|m5*OD{Z4kxL^U+lWC<69E3*c73Qnvo%B2bLt^ zpIy+H-hY$4hWL{%#D0-n?pY4Qe04*C_}Sdt+!ZX3u3N-wO~pBbAet8@u0V_->PI1m z7L=SY@N{PA0{RjY4=^u01i}F}Lh=hxEf^1km~&cUCcAQoe;OiJDXD ku5TS|W4Sy~;ucv;=0nDliAzsbK*JwfU=L*)aEXfL9 z&UKp^@#n&G|Lo?r+&c#ugy{1q+up-DW+VK=IQc*h_2kk(o(X8wwbUDs61~4(=C2${ z!t-z!!i>B>8%P4im{Z&hT9&LoT#~+`N#L~D-M0SMVH%w}$V~b8s1}JHNu+xMbL{x4 z?3j6b7||1D#|Z$1iW3=3&cKm{oh|yT@Dk{KIa-R+(6SI_RdIZ)Swl@t04nyrPEGL2 zODi%kF!(t=7Yb1v5bl8jwNYqpQAkXzwRg+T+dYfFoUiRUM1yD0JAB$T$2{tS;n|Lw zgY+?$KqsQh+&dWwf2cXT#Kpyr1IA7SCOhvLaj0}EGMRyp&n5e{^dU$}A0>Q<-}zcS zIB{cQo&NVDNl5ad_(6}YK>OpsGg?b2_E6TK#kU3j_}D>!)~T1XbHqVZK;WL{(o&5= zwg&z&vu2hzg$iS6%0AM;;iF63bXOu!<`2RfJ&TUsCtD451)FMU?BsJ4r(} &;vg~1tGw6Nt~=G(r;^hOpeqquHU$E A1$ew(%N8t5^`}9W?StD+65~q*<7wo*I z0h@ EJ@)kVT85rrRbR z%9fCGOR!7)gWDBnrZ=&L1iNJ-*y=!|?jlX%D72=m%LIilLF8@cDuJ*dT`37o%AX6n zDdNW9Za*>q>mzF|S5dAk(~Z3RYu2u{Lc4J{YKu(+BH6nK#(WQ!{lSG+E(QOC+6B(7 z{0Q!mN6lqkT>%B(M{3GNy}iBf2xj1#!&y|4!^e=X=c uK3C>ar93xQjIeH_&OEc)%mo%@v{&iLoi+D6D(DFL58eX1%8NB;|HeQ3&a zup+Gx3(d)k&Aqyii_4LHBe+N*Aq^T`RT;@f2Q_m9WK+>|w8{dtm1p8>)quvPpQ9-2 z{CR7{yX9HV<=y7!_XjvUf|ZG?s*APZEus8Ad3x9^X3R`R-Ig2xK+fer=$y#N%s)gR z0@6wV7fw^rA%)3RuA#X4ZlWZ+TP)5jFLg{12Rj%$HG&*&Md}}Txe0aD*XDxV=7LF~ zfNY L34!pKekES&!Z>ltbE27^^@cGnzmI ~m_9L9PLA&&kQb$$b}8*ulU_#S=+dv(BDkaFI@ipOM@KZ&45_Ka6q&GoTg< zQHqwH3``wj)Rb ^2|4_8SUj6dr3*7PxUDa)TEe#oz z8;8d$AYy{H|5aG{{~O0n7tA3q;JykV1tIeJGZ$n`M4|fFUGB~F7k9t;6F?dk< 1iTKqMLgwaFWH+) j-Z9De3~VxHu9Z?uO^Y1B%P7G!Ukrzk5!&t*tG9BTc7x z^9sJhL;qoI>ObC_|G3B$msmw+@^Hbv2 Wk53?a3^38b=&ch*PW1+tsrr2I rHKy$DvUFI_y^<30`#-*HVlt%ZF#MbVRE8#;uvFJcN^u0gq2ece z>8qT!*AS)+au%=S0^CXyq#mHDscDn2UU;V(y%zXr1p-K2?-qB%!i~fX<_-h>8B#EQ zS{zu`Md~^+f4^8P775%50USby4 ?4e#IH^n*9%C=IPxKo4~ 9Q+mR#=fL;?AGvIo4$kpfK_rt0r$2LjI=#{n}0jEN`<9?!j9 z1Do%4d722!86?qOmRm{bFQ=QJJMAIa52QvQyXRpu*fgmMHfZuq%{xJH)m;NxQmBPP z2CoCp2XHiWbVU5}%P$9FVniTsz_swO_C8h%893CtV(2WUP)nJ0^dfiQv-S2Uk5tzm z%l31}(lv1XW|`i<|7*dQ Q#zFWceOZP5>o#`@=aWs6~h3Tm; z1#DmG93$n#7#uYa&I??e _CRo|n&MGQaJ}rh4 zu6R %ECsFu{2B8_za#0tc=k3e) zv&`10^ryH7({KJ<5r-?igN5fvp9L`;B93oU{*J=+=YSvlI^d7i<9J$UaCNmB2{eh{ z1>8Im4fe4c{@OtF5Tay) zz9itt3Hr|!&kGj5Ow!=hhr9 h)`<^39Mm^<6W}G=VpWsQ4l?Q&IkLp)38( z&;EkdS5>n)e3d`|sZya+ygAvFC$N;GXA@jD7*jca ~Id8QU+Q`7C%}Ox7#Qy|xu7Bmq52$D=0<{u Wc@5z@frU?`ppYu^p)TW`og#Lrn)fIsx9t=Jrfv (>H5;z^eq7gm`6 zLXMu2-K&wD@QvRSb0VbJFNCB2(ItNdsCLboHE|pr&CQqVwsL&Dk^z-Iq#^wYKXL>y znCU|r2eLv|Ft>lrYWd}^OnvN;#v1E>wG|Z46NTzOfKJKzlcu( V5FvU#(N%_K*!I`0OwakSHFrub~tva8R Pl7*U>153!eX~vY9wy7lZ`EG()h!0az|a>(nw-> zTV+d&tKG7wvij@c2KD!|Z5_9bT=G<$w1>@RXu59lZ#xZBkDjnge#}wXA}z+JU66d) zIHsFle0$}clJ?+SKfd5Hl^MOMbFs=S^{9(ZdWUn6JB%~&xhV)fDQ3@JM*SC}KL{*? z{U$!hi@kl)`Nsy6^@i+L=dB+9y8@E=0H=cZ2me%??|{yc2~c6Th=!(y2BGfN@f_%O zy%4;Jg9vs5;3XOjqW9Q%{*lJy*lo11-e>yo#K(NHBe~aYS-N8hbz)r@8Iq_cI|r@; zUXU1C*SU7hnj)MgcB*t3@$~rcRPFe@B%HG$?F~lk9d&hqaInM`H@pS{Z0l$tCB)o* zhSg^|rv}w
_&xEYfM5ythVh)>r`-+1m$jV&YHKCA_zpEF4fu<9y39tD zn+^;N$U)$eZ_pm-NQsV)epcLqYrYq(omZp=Q<+q8STDPv4~9)=?_K4{`j!vKcr~ )oEeNmEfI9=Lt^?mM|D1CV` zZOS8q&GqFa8{1d*(e?S{PVWU77wxs@j-zpM0&V0jt!3D5;vx_8p)H%5PPyDC|62fH zGZmLagE#LQ%HqV&opo}e^>$2LVee>aIv&d(ct&?pGLw$KsJRVibNuRBIgEH4g- zFP&&zYNK}7RaYOG7Ysrb06rkA(quRDPF9CSEMY-Sw?kxxkk#Xs{3fvKv9VDAQmd;9 z-p3+YMLP@AQM0dzk)I2a!$p_L9&-Kq8_C79NCK*Y8E_OL{|mm~Nsfrqf mQzA<^h+K7uz)lj3M92BCIqw zH^(h~fbA^;*~;3+MrBHjlY@f@mq_Nt`J0A@hK~2^CeQG}#1{;bQ&CY7XS9=r &rmOqjr!`GSP0F#%!W` K2$CF$Lj zEq7^8nmHcTw>v*AxE%B$E{e*jR{todQeSq+#qm(ueVOpg-4RDwY_zARrgV`nRc7X7 zG0>K^=Vjz&6%RjAqiG`8e)9%54zPIs8*QzC2({>8^Koi|x9)h(uw1$fUu!6~*w7S@ zlRz}7Axyr04{ye^$<+a$|DWo {29mbu*384t%O&+cb%=BUC;_CEziw4 a8jVP;vAXeJ`V$<-}w;gXh9_hW}TEoo5 Yo7d(PxH|(jDbCowk@ob?c#2H)=^15%8Ofj+7CR0Ep=mE z$OSr~u>chL*sD+WG(c7`h^mhHu9N+!Fc<%I7Y}WP^?%Y6Ih-Sdm~P;t0=%Gfed}$K z8)Sn|wJ1}Miz74%Qr KA}fg1fAKp9|D#q2 z9@hUKTwt-(#HyZupjvT{aoI*T8-@KzX1j>*?^k=e3qJkq?|gr5Ukr)nW@fS|dzU3f zuC{Y41HdfubW>34GG@|JaGc+6NVV=}pLB|AB&?7g&~yNQvr~!HqBFCH);d{o*Z?vU zUI*OgGK`fK_ lyqi zY*NJ6uFp&?fmEkN!V#3K2x&0>c6P1bIetXiwa4CdFImyzI-U_Of^W9(s!p0qQ45ot z9VnVNnI}lL7e6e5dzAE4a|g4U*tAwsq+d@}?C&IkqCfr+aE`3juR1mV!EH(aXw5nL zSKXa6Y9vt;&iOS+{lbL{^X<&@mq#a{cPt3+%9P}8FhNv6$QG7IR7_9s{UE({rWAK$ z_~_K?J=Y(KeYvxFG-<_Xw1CS#(s7{|3}_9#Cv>>>2XTvi+9{~222jyMK4 Gj|0J`e&LG_O9;=;(SEy)8X9p0-l;)N_JQx8beQscvJcZVbY{#GN}i ze#~!W5L_Dnd@8u;9r&j8xYxV?Er6kPDQX?%!I!aAHy41_kBVQWe~N8rXb3^jk1Fz) znGb*43b%7(RIoPbOw8OurhQMfyvoGRQYjjB|CMvAeaowOf_JTcAir9HYu_c7D9JxN zRK$3y!c=Z`-p-ljS*vs@d7a8}(=AmO6W$v89oR2_{N&Rs^3uy?Zr!}K{H>6B_xyaj z-ur`x)D5@;9qAJTA!4Pu6KLsO=++KPZFB5)q)_HJ?$|LfdAJCQ1WP!XX5+yP@va=l z_70}uYwZ`HIOko7?oJl@ULW}P_Rd+=)z$c0!IrC@g10!q!&>}`=+1cpF5%+W{s+qe z6D)qIv8-7fvcq4V@@yhF5&60-PyLJI4KU%)vjnOjUpIgAZC|mUUqFDAuT>0H?Rc^> z0Ml62DEX8jNYxve`zgmVN o$ZR6E5X6BMXxvu#91 h r*1v&6A7NVwpamY1%budg#!hWs>Z5 z8z2<1HPe1bdk;4^FVX xVjeNQE=zC zq#&HAV`5O;y4#cdJYY^lGdZAxPLOh5Dyc+kt=!)2G2yHpd#ioWA=xejt5>$``UM2U zVcW&=HEY(Oogc23@frWwe0{r-Y3DIb $iP|^LEw`CJGRg1W{ zL}$79>-8^=0wNQkm!TV^)sMvz?5YM~$6UlIQ8VEk9an3g&^B|u!& $m%?)1lw)oG=4q~_5>Tv08^Xh1BZl!JgK$lT8MK7^67NLV2&21)aG^neZ4v^ zU^Sek-@kufP&Bwax-coJ%(A{6nChmR8~@BZlHH(_RC^aHAk9=NH2?~7o40R&x20e! zM89sX7;Jrh2{Gm(#N|Zv-fr-pMzUgrrgM;#TVa!h;-FnVJY#o`j{@{l&`TFh)_&eN zG;yV+z%k>2hX(_XDJSQM|1_`#)yT+5jeNW0A3uL;^+UFrpGJ&br?kH6No?#nI$?(A zCl9scXH)yo@-iQa)@O*lb{_Fg9Z*UyVTUZ+Qi^l7>zos<2J_WhI61#7?A*FF1|#fe z>8PhX#5r~t-?cWGdn0ZJ4Zl(;UM{Yam={(;qXo9*aDTbL<>Q#e4x}12=U8$!LA>n* z&V@26jL`S%)~;pQxii(zj|G>n0nH*_9-b=jHy9xyNie&(h=_>yf}oq6h4ZOLs?BHS z|M)RCjZM<&S<^IPBH!-a$LX5pU(G!&XG0PZa+ST;gh_(x`@XEK4wlf|(9_*5L;sbF zt3FPiD^@(Q>~E8z+jhIBmad@;`@lo+2?^EcYW_--4HEXHLfPhH{nPdpILg~_ls~2G zdq9Xj1q>=_bkpr4?k(H-5MC&*gY!@D92BoyiPUzRvt?ovKC7x}`3HpQGp~O+k*)fj zO;^~a_oBXmK|c^nc{w?6Ot%wL?XB94`!qu7&ju|ThH5irzl|Wro!&Ia$of!G@t8W+ zTm4!$cyiK#X65+a90vyO0pq=qK%ILbOMekht{#CJpVEgQhl8pfJyYrkO~Y!4k4FK6 z$gSg$a@9my5KiqwD?j7Ji4*L$DnW H&6uqQKPSKefw*oJvuY)*b?`94cAJqXiM15&o2k+dYq7u zkWKrGCFK2KbgBrO9sUdnVUHbzU{ahw(X0YAl%Yr9d-dv7cH6YHG#qusRPE=_9Df>+ z!{N$d0{bl$-69pcfJ^OEa>a-~t!F2XJrs3}F#ITP|2?E4OfnHjV;>|}m9g!^=EqlB zk!R*^G+iI16iypMTPhBR`h7)31DI(~FsdP*nNbsRrO=_$-_|% e*Cb4e8yQ2>uMpQBXYf6I+kxMKpsAt6X2(il$xXsI2q`e$ `xf6J6EfvJ z4JC@?SFcXcdZc2%eKYm1d?90fcdA2Dry-uv?JfZOjdOvxX^s0vE@9h#B|L~;4AfZ7 z=6kjm=VpGn657CwVk!v`P6A%lSv>w*oWIPr)1{dyH#49YEB>9!DH6o50s6TLfzJ=9 zGH{>_U*9h_C%nA8FddMHRZWLEj$D_9SIz@ANTmTI^QA&x{gOtWO?=aw>TFp*Q1sys zBf@5lN_1Y?IAw{tfsPwR<3nvauv6GiXb5M}7T`>vQ!8LLkjU18^&gAtI#2c#=#-d> z)CV5gc8*5#6+DfXf!%xc5EO_+p<@hphmfm)xJflB|NG>s1aTFz@1;ac8?yyNd;nyJ zuo}?W_~5B!6pW1%AjzpXp!DnT2t#mpvym?cB^h4BZ z&ylutE4Y4<+}D(85(^{fML@BeF*CyN0~NtiHspvr&`*0RX3C}vX*E!$yx4Vw-}!Ad zoNBB^;Z#ZdJXk$`RM~)o$HC{ZOSl0L4~tUH&=hc&zpx*PfGFvW=8w?cqtakF*Lf|V zH}>IV2Y+3Tr7q}yicVmyW@0RKW$@F)ijWa1VlND@24lC7Adbc>Pu9v;fw?`>OC~Nk zOkki+#=6+Qb2&vx$5wYUqA1o+C_-k1-eRsw><7gAK4_yQCPuzG6LM)ls=;W^s=I7^ z3$!`<8#5DIvAs!}d6ny_+uK%?f+sm$7V%{aqWcM;_;NOBLKBnz1bQFh@9`X?nwvF4 zB^(uy&((9*f_;MIr$4JgYpVGEW{-Ff|Rhv08+C+fJ{;d4pVheCO!AlAITKIeRZ! zF4W(ROAx~bxfC4EeN^hHmVA5rCT{!zNU#qeV@I_5Ui-EuTM(NL6L7QUEB9@jpIA+~ zu5H_)9$X#+AuS?A2K}lGkn-XnFH!%pxBUX{>oD%i8hUD5SXg?&6jVBO>drZrH?JEz zJ3EQ3A;UuApOSKKjlgwg?H9KpugOb8#P^4zVV1}_7&!qQI)$348E!{ m6(7s1xpn8d~H(@W$3 @z29xp4-&1i`(buF`ta9JtDM*|dR6U>p zB qafJ!v&Wut~CN1Pn;0VjWkw?|s2v=_Q$?k}?L&!mXaJV%^@twGJ@khMFNgu(} zP~NLT77Jzxvk@XD)ocHQ#aNo$2Ct9L*V5jCk4JLbpYvwz{uh*|A+V_(G;VS6Ml1%) zt@HQu3;6U&jq4qjAAXiEwCa-KdIt@O=1+rm{)6~=Su8y8q9TC|lPMiWtALHhEg+Cu zV~5O(d;k8jvqDk}vl-`Ha2LLMP!H)inBu4jF}wO{O?|AI2;ulD~(%FuzE0eS;NcR%z30@=aC1UroSsI zTR(d7;z=a<9zmjJ__ZjzoUGv%&rHE}pT~w1u7`QUdYanW=7@S%sDT&>uiUqqjINbK zpwW~1$j?uyYd$g9U9cno;>s`23W3!VC5Au| h55SfV97SSXIARy7 zVdOoT&Ov$M##_M@A0Lm_<{?I*spzGMdmg;VsKb#_Z(gH4@axNUY9w*)e8Tq*lbo-h zOWin92wAso1RwsUuI}z2IE?0Me!RWn;^HH%69XSB_Z|HCg!_I#db*JHKytP?-2#sZ z8OJeimx{ncrJY&m1W3tVyB6oGcjDwpS$Jq!Oxv+HLAJIIY9Fd%D_IGWC_Zogef}}Y zZ6QI&-=uC-aZsnA=Yx1=4f{yfktL|&kho3SXD58broCm7no74|X2s%W0cVFbwyn{A z@e1aKhC}9dF{>v=4M~CIf7``v;?&{iYqtb#g94EtfQW aNNoF;dKa6^&kj!35WkJ>j9$OIq-?v=A7YC|q{x}S^zKki?&lS*T4ZC8 z($eAH|9M49ejZ;Qc>90-iL&*#fBu&Li&FRRPy2uW!R^~1*Q@|4XXhAZ9vX6F<>h@9 zUGR6(s#U#5S-v8_<$L-sI1ocMDdHw_bZj}hK|~fw;ebP%0BJ+gwLF%Msi|W>L}K=Y zt*A(pJAbLgLFLz&6$w_fh*g*@hlrRm&RnO9qhszzGk(47rnw_BFW3??eo4p(E1Vm@ zg#Bx_a{AB`hT_e`3uc(1O`Y9XyQy+%k8 NN(C8$6)$8Z0^mktFtT=|1$|RW9AEJ>=`m#L7Y&jAu=VbyjGa~<=<5hC zrqy6-Rns+Hr52wiY*YN~rkj%CfsM6G>t7$h4CB}XLGG%Pt2Ky$)sRn`KPhoZ?5f6# zEdVNP;nL($_uCgB=^rz;$VWoy#VM%)8FAy!>CsR#N~x<;VlWtv%`sp?%U#gdDs;}J z#y@#-3M+zwNEQ$9AiF6HQ5D^vY9ww*^Y)4$d4jp-LG3`;@rNE*Q_(f643zBKaN@2z z=6%C4=}ButdfgUphIF}YXY-Ye&)7lB603?sMa3SsbKq^rlfnQ=8?V$VxA8rp3o5NG zkK%z{jzKkoS4ce_2$}mx1m8ZildW&m-`X42P>WnB0!9Fc<)wZw_SZm%NCpo<#2iPU;-_fL z7z{hBSH;lnzBbZ#Id(mxNRi{J!-9$iEdgiI~>*^jZp-8#=_#CFQrveGcEir>| zduacjKumT9Af@~iUC^;*^JZNzi97Aid$EaF8Ry&dDLe`fw?K)}!E^5Xd7H)(^Xy=> z0IECZ`JKnUeXeDfa&3-v!6Jpm=r+j7lytT^LO4s#j40kD91CJQ3asHn0FwD~_+!2~ zc5r)?6J56JdX^hl8x`rj+f<9CuRe2H*t{GQ7x$L^nDZwR=5+J#+*z)h@fo{X3mj%a ziH|K6#9qyMY?;pSOI6P(i59pp(1$6-_=eeJqe~PD+fp4}4Y$&ak~)c) NPe--kcEW$JPs#isrsDXZ6S6X3* z^{0G$i_q~sG?+;>IOP%zhA2#-{H}_JWivZz=`)Brj+etf6#>u|tO65^G;lV3!{iF0 zQKl$ZZWkwJw^zz2mp-%QsFZ1NH=5)4eL6!bu{dLs;DHTE!01*Rx4A7mr|v@ziy+Ye z)uaT}qGFl*4jib5lvt>1-q#AW{Uba**ZYj+=H~LhypC4Bu-!McxweJ5a4bbg0%8$6 zjYRw!8sW;EXY^;ngpKI6Gk7XQjEsMfBQdlYF?kPxW&vPxvu(@ JX8VO6-sDvUP(*N8 J1M&CW4Pz4 z#;&*(-4z=s{lSfSt1M8p5c;_0G>{MeIdDrV C8 sL`^ l;2_wSp^OM z8+YnY>y+da8GeS=L0^rkwszRLe8`yPKEujdTcd2389R%4Q-%|aK6HE|8=EmUcK2ls zf`*Y$pc5$AnjBkJc0PM1iH39CpV64JDpg=4Nl?UtZ`t-rw#^cM`!>URobrXzXZ7k9 zB<>6=ue=#S1kiKm`s7jU&yU7Ri7^nY`rvptXYv;2XKX>YaZD=0{JpT-ymKkvo;| cf1n= z0@yuz2VPwB{mBz2gfX`mn~v*7eN;!&p$lIvUhSU_(aan_c+&ULNdUb0^<%cW7AlWy zE?i6CI2)U1O<91|hiB&T4%;UW{q+uU {sP9I%4X6UBkzW5FUfC6o(ySsjSN7{E} z4|e;GT77jd9mm#!)P`ebqQT&J3mer?v6oy2zf1uYVm+J>#sRGB`=erJzGHF$cwFdv z0=G;Kfs*km-W+T7w7p(mVn|XWkuCG z4rUk;k?V9)%bzX_q@T( zwzfL@@JEM *_5BZ7P+LFzf`Ase1DG6#gQbI=0(%EssXbP zKpwK8PA>e|U|I^|N_3L=t=PRNBV}kbc}vT*s3eqUL5B>z3#a?}$mvIM#&*~| c$@;RpKx{iz_Mk_bUM(O0r?FIkz_Y8#q)yopiQI-LgXvYRt8cb)9Ky>%Y6hyb6r3 z1W-p}XWXY00cYgd4fZRIV`3wJmc 02Zqt@UL 7(qoBXl7@vfG z0v2*IRA-RVSg$FDY-QrZha3Fo_%CIZCAuh;k|R9oit%E+zWFYt#~~q_CNmRXaj8~$ zo1WYjJPo+OnwPkSrtoN-dlf;0GS=7$Y){B4O*}@>>4rCp&&B8(o=eETKNqARK%T$Z zR3-rFlZ21tZwu04KReamv8#?X3ao>^qJ_CsoeAqmkD-%Uz 79;GO#ok#4b>qIFXQp!7zE|z*ZnAzh%DhJ z4k~L~0~*={c?1t28Q8y=3;=8tN2CD+Avy22UqV6?G;5pOJ~!F&0KwN?m->(fpeJ;M zVLQ~S1!DH^opU71t#uU;MFxE4pmQYbxdk)mt}Uvzd-m=%L1xRlZ(lW*#k6D@p-l!G zPMl?*{p`;mD;Xi?&j8@o&ci1l@DWH&NF!-a0UJ#i_<|81jhYRe>iZ_IA}Na~eg+3| z@XJ;v%I!wXdFu2XZbh#95sMebJalGX^|+9P9$J^fYCDk;qs8_dO%A!Pz`($1%$B*d zy8VIjHiv0xsps}6v>;~8q(rbz_Pqa%cr6~JCLk0mX=-YQj_dCso<&9F@yhkA?P;>6 zrb&cFb}tHbXlC;6k_?66H_zt1kKZ@CkIdl&$sDfLJh|^WJ8~xW dpdC<<7O<%y-fViGpE%G8^7& _K&&5wbWon3&rYdUi*!^j-H*smq;uVG(TZh6{T? z{ma8%1EfZpc`2pc6wx5>%iSI9L%|{@F|)JIT2{2dHuWAvMPl2EpFwH`%`^4pI2k`M zS)=sVY1k_Lv=fQ)>Jy|6a9Y5!+&Y>Sq4y7OyQ)En+W{ 6<&rEXfk#i*Hd{ zQ6}EZd#P$j*{~d)O!8nqn~IWq#l)^<-EU{kB-#(Pa%Lak;kk(VSEs~s4}q5{A39FrBA?C%F^He1+UPF#3U2>T5OR2^~e8B gaK!)6756VJiE S&*P4$r-dH zh)5F2Nx71 ;K`|4NyzJC4Q>mFVGMvs3y$Ed>68_wBh?X~8bbFN+Y&YhK8vyycs zg+f_#@`Utx3T3G+g|hg{FU#;Jnv1Hn@MD?IX*p^9#INb&+0OW};`#|yOA3YkF8PPS zr(;BqLiv?)Qu?TpZD4 ^uGH{Qf2Q={wy !BsjcYLC YQ@*Ye!?p9c(7f>URhlC+yxGbT(=+xiFW4lc_fu$nPizy f_^Vg1wC1E{y2E4VnE5oGS`1!P(e$-weY>19&_c_~%BogiVbXaay`&;NPsrrs z!=0yfzJDtbF!~%_Z&@J`u3PL~m8e_X-Inj{Fqz%vwVcyo{}VpV#&2o?VexfVA0F== z@6}dRe2}_;SNmC*>FMd!T5hG;e!auYI;O$H!$a-x)AsI*&*pPo%g!ocKjCRT&?r+E zuSxxKd!6@axi~+EeShK}K0dR`49b!3rY|$ij(0xo%#v5p&}a^w?(nmG_xASk;zDL^ zwuGGTr<7DwL}q4o?%X-jpIlLtVL@#tA2X2NCfS&3%q(Fy=0;vE!>qZfLSj Udzl8B(tW(y#bw*_9I4+PDR?)TW%IV$^~LRy;G{$+ z 3 IMROQ;al($eBc$emEjnP@Z?xbo*8Zf>IUQ&eiX_>jqi zx+RqQ?`fYk(w|$Nv22NaX&j#=(9zK$U{D_TRM6mTvqdp`Rg^-A?d(Xg?w2>ejdSR> zWLa5Iy+n#k(_5a5`z_&7j&)<=QKqj5KGCzexjAF@dnwhOC*Eyhps9`~=IiS#V9{PM z)uZ6-Zxy_iQBg^0lZ=dvfK~5jH@EsLciG$Hvj^QDY?qTgd-iN@!cxkbD@*zBUDVWM z_tUgi?N4uWvN~&<{OpjWem`!>qFP?0Mm(jtSvg*VMQUNXLOsiKmwjJcdYH{n>&T;! z*%##}&4sHY 7A!_^N2#3$kfF zWR^GGX_r7rc=gJCt9g!q-Q*zqXlGfBN=T}4T~X-K+wH%v;xnu~Y%~46!=mw8RAH4I zXUjL=ca(F?4E*<$aG~L{UhV 8 !Upew{VugWsm;d_bbSmkluGJhej ztRi $&!p-Im!{Kn$H)}b zr|wpcdb-(&m$}3Cv *CHvVc5;%I$%Zmc?B zoK=p~@#bpQgVAs2teZ^J#e8zdKApDsUb4IH)?pD5k+qUjU%G2zRAN<=cg=tI%P+uX zB?Z&)WyRxl+MIQoHk!AMmrx8>H8yHYHnq)Lj}5g+g*z6yGYObBrd-OgwJ1t-cX!Vi zDcXuHGx%Ptvy?xdEh_NFsi3-#6%{^sGKcNPx*1EK@Ot=ZJ00p<+1A_JE7}_!-fr$p zQ}4%qP%XCWetx&TOr-6jVMSS)S5>%N@%?Sb!{mH85}oE|rm%6_AINdsL%fJnXB(0Y ziWjdtynbPXb3xIdg;Kfanp(u}U|TMBMi!qBA3oTPYiC@)QaitD^S<|@lTlK0*NSc} zXS;r+=+?!H7gdWL#;W83W*z#}kBbGJtq2kqF=l6cFh7x=AH>Ja{h>SAtR-uV-KP0L zssgv~Dd(A kadJs-vPVyi}6SH%)8U)n~6$^Xg&`Qcvxn1&8q(mFk+Bn$#Jkix(pd zDuRPr&GYAyCYrFvJ|V}c_Vr;|il%QbUw1Y^D>tjWYpl^YAtKwV?@5(89(tLW$TFA6 z*D{OmS#~}%&^OH=NNZ|OEavc#bey@$)>ogX>$z2gPYV(5sf(|%z*85p5Aw3Y?bq2a zysrK7=PG{BHCu(Rrf55i6p|Do`tAO4F5eHcQIWVBi&Eb70_-ow>FH^fV&@SLZIdT< z3>3BGG|9TM&U#%RFS7Z zvmAIMiayr@0 zjwd*KA^fk7X<|RsD%@vcW;SS%HAVHPia6~j7E*n5Is2w${jv{++L<@>YMDwYm75l6 zZRjAoU}hw`T0u&JFK?<{enJXiLk;ub`(PL%YN3<-xsFY8cKDs`@Z04`D|V`BCaQ#c+wR+gns!dYU-yW&8 z@vA27;BcDy>N$G$P{-qpxrru`Lkk}fwk3~tldwJD-GcIvh}xlM8sa#iT68N&+=dh$ zr|`OaZuQO!bIE4%iH22S@)Jv4T-Ogj+>=jh8A?i9sB@lE$t`}Lwp`W1!s7OzYg6+? zlX<>coA&j$OTF?`3!XgYeJUSuUf|lN7oHb4ZWX-o?Y(Hn;|uKt8x>-2u~~i;VA*Iw zaY>7fE4S(k$KFwGI(_Ptk)srOz2*6rO3nD{@nGl3^DkpPHlb!wo#(rpjZ!0$n_rj> zG^Q%5si`Fx%Rf7?bAGl ;cxBxtcaDLf%sIQ%{9W)#4_!=XzBH z@R$BWHYg~ubNLH%L&XWsbA8>F&)>-=BzFbd7X{l7HiR}(LV=W4ROdO;?1JhxOEuz5sN{}5h2PE^~>>1!d+S8BVX@w>YKLR>IyQ>(?VEoLQDo)VsD7_`Xf`9{9@Zx7Ql>c zC^S?^00w~#oX)craibiLV^y~8_jc=ybyr#RzKkkzq75(;XoFfRTeFBFf5pS~kC)NW z(RJHTiuAlnAa|$QG}&Mf!mClzY4L82h;*?xyK!oeMG=dt)aKev+hsl7e*gXVWfCM+ zU-cZVpgniI1_WSKYscUccQ?~4GVMTJ%i|Ghu|Y~lXJ-*I@}*Hn&286pyFwOnhD#5Ph$8-IS6!+qmf(QVcZdVcRqOFiZ$+oX!Twle!1{(d3{k*%n#TpNv* z-4^?0r$BF`&$zKRt#Nbw)zNOG4B(dMwJYoEwCs6wS8HnCf)3-jnnE{MA@OL9@3|O0TuT2lYRGi;xo7+&pdl*F)Q9S zefC?&95p`{1*25#tZtWw)m#IgcHZF$ ?~Q#(Uu`65vPgdkizVtJoRd8 z*kHM8X7b{<77AIb|AFTbJJ1ksiT4!T+a0`xTIA&c5Eps0jv$+lfrkWVCWkl)%Ir@n z<@0*QOzGwNBL5Uun^J!7y^13MnCj5#fU8y#Luo0?)wzA$7v_f+yl<}+i;j%T$PhKl z?0Awf(;K7yI_Zqn(sF>VaXX(ylvzQ`mNTYT;+vi-+z)b`XhGTtX?e`g(Tc} &HWoy;hPqx_T4&_dI-&w_+*5lhiF}by< z^^eS+$bjDQt-WcYJ!ExiC)(TFm$eC{3yBZESxi-Ln^DGIc8Ql zH*Op6t1DJD2x3x@7+1R8x6~!;7u%DbS&IsNBxj;)IaE#G0u{BF><)=lO;kjrbp=|U zC1E7bL6ei3QC!U_nNva?>#gB2rsdokC2iK46d_#Sr%cPP3`7By+*@E^peGd}V_)mU z=`eisHuHXI=^S(;s!jHjgDs&J-@ktkQ%TUOicv|}B5ZO=16wH(jf*WoG^m;%FWo`K zQz-TqG7bY4SQF1eNn#v+xFtx;${n}5?FqlOR*Yls=a(bq3lrw!98pP02ja7OUwKY= z@aKsFI8}WU3pnn>A<=FiMN`!P4iqxI5Ovmc*hxF!Lb_Rt4wlGML252VnFb`ab$8Is z^FecGCB{F;ikR9@G#F5^YfLA^7$^@zc`K~)=LXE>7Ut)YIZCnH0doYFt!3M?c-iXu z?m0GVv;^IsB2EJdMk>>WoF=nnm*%XKoVqOAgwBf*S8OzVX(86Kt9&G!b8b)G{CGmK zq8z)g#VkSJNFDhlbqp@QFz~OiBv2Z8{9?=innzCrKwPWHHrnvJb=1xP^K!$55(TGE z`-Fu3JovKqnB~t+sByB>-mY`1ys=b|B>OE=P7ZNtNbP*Bs9DPr4nD(oTRvGvxi4yJ z6>pDTulUVxivtd$t2pb)E-q}AJ6@|cfLPYnKzvx4Sw1MkeNab=IV%kFzx=++$Wi;! zr7b{fsf~F6&!1`2tJ%d4irEaZ8P~ nq2b z?T2#F7qu7MV=>jtx_NJ~C0pcWTBq{4bL+`U8mGo-WG3BtRbuw>seo#eoEK|qNj8^r z4FLCJs}LcIOI%6q!8+4e^t&SAabaOx kRr zQBAEpzRtAgK=0>ln<3Hh&ldz6yUK&Ml2pQhzA=C;JUV*MTCwg>VT-$l6qm^Q#?CA+ zQWA@?K?1AhX>ALV@p&`7vFg7$B8OiLv}wIX`VdcD QWKqinVNxR z34UEFY-%KJ@5+@c4>STmhim6M2{_J-MaRZ|6wT8qbkCTd=?gb+%d={IyM=PuI4KoP zUf*xaIo^vc 7*GEZ1fT9jXn`^ zV4=z3{kFRWeF^z_RbtG%YMYL`t}Zbg0! t_QXy~I+! z?1Q~_1dj$Hv05z5O&YM#Mtq#vq^3(0bbvf8zWlaawt(5D*b(ay4%E|UOBz;PY(^6$ zUiD*mV(qh|6+Q&LlpScr-rz)bHVdE8x>+T|vauQPn=ev6U~}7Sr-)$r0-7x<{_z1S z*mBtRI_a=ZojTQC;XH4-Pb){1I*WYR;Io=yc=4l^dI+Mh82K=0ywegu*d1k>KkH`C zhIg)OJP7(A2-_BYsxJP(%6l3O&|a~d-S?|nQG_$Tf8e3IZZ(Up(bCXhyS;3UyW?bw zh^!9>JFP!i;bzaLo%r4`1j_vIU8w@V^JJ|)psQ EATMt zyHA=IFpHeOMEb3h7jE6Ubr>H*_!hGpf#!~V>J~i_e!^MaydBK>Q! `9zF**G^Xy`Jnx z{(UN8Z+&yHIrFV!g(%gN4)?_pmr?4~OiV3Xio~d%f;vT>EUGH4wQ@+u?cgyybbTbO z!@D}Vx=6>kscouAgw-mDKgfB0T2)1V@sSIuEg=&rSgl(lZx*jB=x<04cLvlR6hZB@ z1^?3Smp`i)wktg~Fu)|=r%htRm__I!#U%PDJSHn^(-zkE 9f9ow?avxlIe((e~Q|Io6zx zLOjbY33)R{B;TnQ8ZUbD#K2?Oh9Tr@p9Na}0-di4cADuSVMUEgZ*|uzeIf(yX>71X zoTL=OVvz=<3aRG_U#23Ojl;NR3+%g;n|rs`kjR_v44|SX^?D{~P=x4ZM?TZZS(v9O zg2KwR8+S)i5ta4a!bmFEP1M6kb5MV}B@cHBwWX)dwNogpM#j(jgk{Jg=g|f|)Xg$? z&W}Y&6{cLPZYOjPAWYHBcwacGY*o5>n d^)+i5RF`0KXno1h~u-qMMs*x?J^!_D(0{^ zA^W9Fa?m&-cQbi1$vz-*$Mxb@QA{84veLh2!-thWN!_I1|F7`De*>lbAN>g5x=%|h z?al-U^KM3ZW@MCOvykpb{ovJLj5{h8 mEvE%q zv{vQ;G{AU=? ;bO4(V)$Io}K4VnE|iSuU34=S8R@V(;pekelTAh5{9%CxtE`x zzr9{}`^?z2w|^S)p%oAs9me&Xh=^S(9~L;~m%yc-`{=CL-j%CXZRes#DN27jzEmh0 z1f*48txN&)##BDdtd#3AJKh2k8tqy^fFSZdzDt?c*L|w1=Y$_0V~k4r8Hpn?=`Gex zg&%x*wsYys&rXk^?$hr8?5ED#-#@0=w3$A$GgQWPBqD>lxxEJoaW+US^}5VK5A6jp zosUmHPV5fx6rKLYtG%v_^!P&gwjg_`%nXwEwm(@;0_<;A6aJ%*{=Y`k_;D572@%*% zX=_1fp^tr1)hgLk(sT3i d$NtDYXOtPb!Bbw*ZNn)~9US`xNfy?%hHu(2aWeJg|cM#8%e zq8?1m4LNh7163r1vc+I#SJ5+rU>5L^RulG6nA(Yefzl%jI)w_t%rgYFgI+er?7RlZ zPVj;(p*cDZ5U5iXCga5s1JQxhk UPoAC9d z|5&zq%X%y>i{q!Bo9G!230!@D|LFm}Q;4+!kRF=>Ox?jt6@~jq`j4yq_}H>>3kY zUhTY`hN4AB-ZU8!q67@GEC_(8U^{LEN_cGSK1gV+cRB0~2^~Z3OMff?eCC xT5>=GF}Y`AmqU#0^VJYI^>hX{=i6t4IYr1;Pw-ap{)_vOs>5 zLpLr}HT?CRE~Gz3#*ufN^RH`DS)113%lw;!n@l#MPr5d|efRFDh||jrA1%r88phHW zKtf?55Oeib;eAwFTo3_h9JXJ61NRkuEd&?~&$y`a`SEat5J{Fff=FK~aqEA4id2|j zc0&puPYE^MI4t}pE*iNN7&Cq}kZ5!OUmKz3MPIWAwZX-u^X>fuRa%O19p75<{?~49 zwxeYt0nOy{D27en)v{c6fH?BQ>i`u=*319|p+fcXB9I21F_jQdo~%OL-$zG{#{;CC zous9K$Mee*m#<;nlx$Gp4W&=?`}^$ypZNojHN-?x_<4EbrXeOYG2e50-^^J```z{X z(2BA$`&Z-NtJEn_;m9hW7D*Q{s|(??UnyT!gHlGYG*3d#0WJH1)Tze0JW%!718Fgq z{lOCUveE_6SN)s$0|<^IPueSZ
n*%afOA3hL$Jh=OlNlENpMCcOr{-q-oa|(?9s7oHD?D&~dHy!c5(E2uJ{X z1P2m)SdVT3ZDzFo0s;zHN>2LoO2-9NAYEzcxB>PbVS?viQdRkj_+W;v1E?=RbCu7r zfpf1Ag&E%-o^D4^>jA)6gdp3_6^3visv?g{e5_?Zf4Y4=s3oID)+YV{P3wl8cmPQp z;-KZJFQbCp-T04nAorQw+?Tc%yNl#H+_>LX5|UrtPy8-qT>H8ILGSh^X=zZnB2Aar z#cmD@!Zh$@A^G=oMzAFz*avk9+CefEF`@tP0(A8wy)ZH+qC4_WBLpcI*XDhf&Kcx` zCDQk7sdsAl_U)TAx9^9U>$;oBQk~e$V7{FVWEl1bfM4CMaz7E-CSW|HM%Oi4H#0Kc zxpPOA1|g>qObb^#=X0|QGxW@A8sL)7cvkc)WZVo;aAA;3sJ7VI&1hs887)5SjryFj zj^)4(Y*AeZBC1la#L%Gc;pg{(lr0A~>aRdYz*Uky810@eQR kiZU?kukqg7(1MzkLh%mjS=GzHr{YXKQg=`M0kCY^yFd*MyLhF(~uN_L68!%2y z!w{%ew{6E=3&4|Y@nGinCelx5MF=Of)_K{4y2_tnix1t12UPG6I`Q^Q#QB$71azc5 z@h^`ty6vKI7th~erT9;m$BrN7k0ULpBMwtv?-CJ7)<;oMvC5N)q0;{sa*?4->JYw& zmKae=3H42qA$O3X17)9Aa}g!(DHCyv++R-~DR!VphPH*ph<@}km)awAm&gjho{cXb zucIy+UgyM39_qi(Ci0)Yx4*@#^0!=tOYEuBrwOSiU_1PU2pNO|CI$^eWa} `7!T++2Y%=2QbP4gOOsb0xOyT7#t!UBFfB5G zo;@nc%A1Ld;pRr#b+EsR)#g*fUj=S`|K{d41@b>*XUJO0k$_$FkA8OW+IeYd;*U`E z8A9JiL;xb%>wNw5j?e(9nV&v=A{+r~_wMkv?yB(ZzNZR3Sp;>)dusxsY(bQI5~or$ zVMd`co qEwXyj@o0j)jWtrg$L#YH$C{g=$32>sAR zBADakfPo;I6DokDJ|bAgcHzO=Gcu+s50|6 |q&@8JXco`@ +zD3#&_rsET3x$#4Ou7UvQV5hZDL@%uaRS)-ck}fyxZlxJxT4PCqR(oz7`Gu zoP;$)(f{62YTQVqYxrj%hxW43N-w?z)7oIT2E2wB90KM^waAGeE1JLEO3~8Fm!}y~ zsyDx=zLsEr>_xBb&_xv$`!!9Wcnz+whSr =VmkkAmJPlz~6h|70(*ZIW6 zXK@p$0tA%Ml|eWWHWOBxijwD39wc5jW(>=ew26a^KE3{b>*_L3aXor_Ss}tUnS)lb zFk6BC yNtE@_TrAVpiCWe*fOsoG+s8QOhDuR(KHls9N*&cWXufp?QgqiO^i0 z91>y#R>M!@ N}DHIhiw`jqfrIgSs1>Z@GK$h?#dmm(1l9Oi^+5iHD z85xluVy|&RW+Lt_QdSLutdwQ*@vxr?87oWAZeQ&h_ZEZ!0bx}l5jO;RD8fZQo*Y7_ z7|XE 5vv% !2!`A+bPl(ouV7QH^y5QbbItruXLb`Kx2 zjkuKP=nx7jUAKC|tG*e#F%e995vY9*^y;PE_RwVuV-oUMfv~FJ@Pegkdl(edPRR3& zjK~4ztGDdGd|GO!p%78lzDSaFMs6xQw}xW+8>yT}Tp1QGD ?;`4bL;MB9;eQM_6gTn#abng7V{s&BzUT&;l>f{@38 zX*%_fVRj)xKI3*-lgJMg%G5)8(f%*9c>XO_6S?(2XEgnPSMj&{cK?5}oIfFNrT;~E zZ^4MNwvcSb62suqNcTOTy9;uR_*LQOO~1L9a^IIujEAlTJlZ_CsPYwkdr`*k`}r}0 z?O8LZa{hc0%PR&K{vT+W1ATlr1atg_ni^}*O*=uQC6qKnf_o_^_Wj>^BErcb3$tB{ zP>s!DN0qH0gcJZKBS}%DpVjHFPehZOQZllYPNYx_?U&?~brVs87-P7(xx=!u#QKQ* zP=2T@kmQ>)uxSyAFu6j~ 7Q?jmXV Txd=@SAQr6{4mx>hS?v#&BpcryitlrU(+2#44>5O?kAh*)Y0j`< zWE6mn8J+?OE<&Cc{xcJ#`Y(Mf3sK@nKAq+#!X6v(Uj}uR!L&i3Hw0ATJocm_Cwe0n z3hIA!g9BA0lTcQ4JRHys8@DG~(&EXy$z0KalxktSK_b{NG6Ft*w97#|X#qc&jP&QM zlj^!O`eray2YGn^D%!tWY|gVIRSDYp{-zs*^^uMl8LRT07sM)9;LRk~5!9&m_uG9; z&gYvbCup(Tv{ T0*VONaXcciQW8z(gp|?%kW4Vgrsxb6-^V_$R fuBg--=^+dp4Wh_lHj%uGR%ihyJ0CtfM3{F1q(9^wFH5!}_6$Rem;&|c^PT*xFk zuIhWH$?t3wF}HWx53TB@#>7~Uj*b#`Z)wbPAvFD_X^FF*M!B<^%PB6r3U8W@07BpU z`tB~ot$xOhyUrM%YlFp98W1}cR*7;;*j+dn8G*_!Jfi7U1Dk^aTn-AS=fxwVMXk_W z{4YO?Q^yL+zX^Esh>FEWSM$05T@!d6{WX&(B0FchosJ&;&DjHyGB0KG)-V*dP&(L0 zvq9z$_UzdcJ^mEND`^Ld>SQqI-Jf4xkZ+^oEu*%N{UK%W{GtVbcj(*C%>riY1=w6* zruViEJ*5{R+dusd>Kv#MEM?zQT#f+l%WX<7y6N%1yeAaiDmipZqXN)V(WXGU xiU>7i@c#R}ji2c`M=!r6Pcs5&-# z_ziSHW1=5}Y3$(XpX8_Iqf}mEAoEbsb`-+a4F`C!C}5P6hFkPwfCxVq*C5&)0#%a7 zf3O;SJhthtL95xq>`ndO&?QdE6MwxSpn)*s^1+WzMUWIl8sTMLAi2oSJkoS^>EAhe z^53?Eg`Oq3bE{d#wh5@ghDfff*+ln~?w)S{n}n|mYqLEJPav`Stau@I4-mKEs*7tV zugBMu;Tu(S+JA>!(732#2_ CgbVhTQ~Z%`r;EWzx|ysgbT&3xpF0{b(4!1K7MRXTCwDz>wT9a8HR6GF$?It zzW?-t%u(hl%8hEfqhHQNuy%ZI lLGk3|5XTCeJOw^KkJ{SW z6Y%=B-s&m@ul?$}4~60ynPtTMI+qMr9NhS2mG;!1W XXN zjugr{?-yg)T|qWl($d7mOePOLO%Mw&wlycIBCwe8nq_=-t%uzM+cqYKhj^LX+z4_Y zMk6jRBF$LUs*>_GRj^x cPH7=8p0cP$Mj)RttT5ZkEheN5t!oq7)cA&9(`eGtG9|k_;9b!D< zRZlChfd~%2gt$(}2l`Y`28?rbGJa-ehJ(79UyF?~p<@4FDflZL+-ai2^!K1;IYhJ~ z{=T}tG?Rw&S lGBre#2$iZJIFbOXuV% zy@Lyz8OeCBW;!k)1Fov}QtkO0#MViOI`$$j=qnYvK18(A(!OA85DW*`RBd`*YN?uK zW*~)c8!PP>TjRNrCa2Ak9jE`KmiMuKpVmsZv9XaW*A@L{o7 e|}+Jvy(^Bar7y z!hCPSD-f%c)&6e>ZNL+7GSWjQ=j$4Cw?YqQIlS@P=royPYe#hWkbx->9QLz<8CJ5b z<@3`GVzi8y7~}Or3 }q$3s-ej92^GnQyF+i&{c}D$ zw-MrNs*vA_(cFQ$RTm~&Qh!AIVv`|Ufiz)E9A!D8ZZKe)pYv{lP~)FYcBI*${>T~_ z81#rfF+A-WR}nLiUNb{YkLlu`^ZTPXs7|lf&WDJWTmu4Z8 h+TE>m!SPuj_reayRorx=I}&q2S)XMQy+ks!#V zntkpd(cAWZF*PyqjEszI$ !sN@={MinEVlNu$!WcxZw>`uKE-q_36AHyzyR7hgowl=MPYZ@fw-`OF90~?a zWC0XcctJseDJsZXErDht@w_phbu98z8vJmDBJh=xOrYS q-#{bX0X}KR$}!s zN~nVn^YBFNFIcm45B_kPgi!4awiKAt0!G%MVZ~kzK4#eJ>$tdxnR>5j2$bWi2cBhs zuOK6N0-!25sQMMb> 6tPnP5%9iFdm3G`9vXpIyuZp!@lIs8&6S#eei9%75 z<( )Lm{eCvz!LM}}z0XC ?8wh zRANP@o1bYz`BJ10U}XHsKFUcK$cwZbG)!Gg={GeJ{=&OyC4Cw#53A`{p3=mr&g`}f zWVTY@+#sxQTZu3eGS_bq@hDsGz%x4EC(UNlY(boF{nv$ijN>zRks%4PcEp*rH!5%g zCImcWJT^m1!bDheS4lO6(w*NTu4m4q0rw;cFb1J))Pz2x+_ti-2j~-#AZM6i+of@Z zRLBVjc%dls5?26`$D#9tRan@xIogn>i6}&5u#q^ANlsJIu#jOITtj(NdGV2;Z5RHQ zknBIVCW;PJXyO_a9sMXs6XFXxD|h 6Da|)#vqq`^j6(;m$Ym13J1bn~yRP@%ic- zaTx76+Cs2ZMn3Q)NK1zJ9DDv+T^pAiGlmOSIwj9n^%~;jrw(jDl5)ddzI^G<#6UUH z(QU&(iGJ)K_|_isj~`#i^*>kmYob FyR_SmDp?l-!1qELn zz;b!u?;xK!%}y0dd4pAxBrC+ZFjX9$nRzSSo5%xeZulZ2@1Ma%aYyW0-1_9nllL7r zB+>}pfi|7mRJ>=ihc6!}FYaSM*5liTH3U3T`u+d)H1fqq#>4u_4gcfnJtbt>zctFN zBp?5l21~8n^K)B LNuj_$*Y1%x@y@-K9K> zC@vqW=!^8^XV3pXO;5ktRrf_R*UoB_N1^7RUzt9Y48 !;pmoo?7Hn8U za6y?4sYOga5)+qv|GKzJzio`e7|XKl5^qzTnJ?OE9;+jSXi+7rJab^jqE Hi8%Z^eAQZNCp%ETw5_zO!5xW$U(yo>~+_PzAc5}kH zJq5%NkGbZ-#5tt#tuH`3hC2N96w`A$cpsE@EIT~Em~-mL(ikl3haa(ZT<4zafF`1= zh7WLknHO2@I9H7~_N1jmPSqMXK%rbdN8+}=_WlZ62+U)2FAl+1si*Nl?$ucT$^d_j z+cwZ`vUANL(17`r7L&M|gzI$`{cAnnKiD2aFSV?H{ZaYNn>VEa4?fq_6asO-(!`P~ zJth}1f1;o(hj #k?a%@LfB;=|PCnRnyGhRS)3|Yxh+vI7k$GLnf^CWRUu!Ea zCf2^4bNU1u+$ 6U3*v^bF $jF^Ee^vGM^?iA3)~q2j*E_J3T8$G(*Q@Qku=CIT^{tq4NVz$U z`Dr(ljiAA{`uZ&)K-Q<)7H~K0DUAp)jlW(zF){I}0~TM~^dhz!@3-Z8gD+6~u{Dei ze3AI#viOu=dQi)2!KT6V0aV>+p}*cr07yhBnM0YyoOxr {HW2A1a znklvl-#;S89QoO2NCAf`p3zC6+? z^FDeSLjYVm^{>0FYPznLU@~ z5XUunbnbApzb>92{w*>S7v!A_Jy8eJ>1|aGgoVssDUp$pbFN#C>Y+WWlV7BP{WX&4 z<7jeyB@)(3`qixv+Bz&N?J!`h?LlZeKfz4|f{~HUb{P2Gl( Q<+) zW2AL5Po$OBI;oruA4@cSUoO5_efl8D$dt--=_NU&a*|dHvszWUH0X!ayHc}P2(@#{ z7v*c3eBNp$`Q S-n~&qv_H zoyp_=a50%-h O+%Vb6(SP`_w)H5~34|z2TwG3G z>Hso8Pls+R5%}Z!S7al4G)WdHP6HLXi{NUGCSn;Q&L zLGd62Q{~192;ysv91*K){}HQ0WVe0lFx}b^bO?{;lwSyL$GvU|Dt#EeYG7-a8X0kG zwzh0kAV!*qQZFwrrUaxT?w^r#^$h{8PxuzFAMQCw=2y>IW@?b*H+UVLp%Je6=gl#c z#wn$aX}j4D?RmYF^z_NBA!0q}<>i&oKNtv%ds}Ne9w0rxo_ZN$&5%1yo)TJf=2LBL zI6Z*){q~n5j%@K2?1a@dX-wJcL~Cj*f-;=?&hswZIJOFYt0|Oh`5& iCp60s;g4p5p=_M2w22v>iXELRaOBYzxY0?qhj8G@>Yr^iY zcc1&Oa!z(fZB31gv9YnkT%WejnO(`Hb5fI;6@|0YqxOLhw!rb=OAmtRlAWw+@IH=A zS2#=Z!2?H>(!*$6M!;eh!r>d1nR$r5=j~fZgUOypl2*#rK;~!uk?nh2Y+_Kb3J~gF zr!W_zF@UlV+WsTf%lNA3+v837e; x< yp+m^ykpq7iLJVbLeY9uR~==Aj*WPHVf99T1g%sL}#Y?%b-Z ztSpkk7jsB~Ub1a!YTf+oOymt2h8<5?au%n+kYZb^-nxNV z!V&l6+3YcbiYZ29eWHB>(G>L7#87eA0o{r>h}J{P+&q+uZw=iH_{-t9 zd?ppyl++BSp%q6=8Hd*%NxbAq+?WuWwnFf}ad@)XvZ|(r2}p;o?ywjHGQyncX_MQh z2?S~9NiTSvAD>xZ0JaER9~XF@|A_#!&M5oM8=SL?+QU;3T#rh|Xgt2Be7zr3bj8@N zqZoO(*igt}Kg6@hM$#?{LNN>`%2|V >a!vv zBW6OVvB~A`#P%&+05z?FI }f=ME3ScviXD zu1EJUe>V!+4T<|nS_K$(F>!HSOeT?ocG!!^I3vD67qe=KU?P)>>49=Q0GF;Dr^bZV zz&WQ+bEwxc1iXe@_jKX|)nxr2rfYllYJerOn$ptyG%{ZIljn{OR9R(U(Zg<1-9bHH ziK3Zk3SWWGGs2W~(Qa