.
This commit is contained in:
+113
-75
@@ -1,4 +1,5 @@
|
||||
use crate::rdf::ontology::{LabeledIri, Ontology};
|
||||
use std::collections::HashMap;
|
||||
use crate::rdf::ontology::{IndexEntry, LabeledIri, Ontology};
|
||||
use crate::rdf::term_helper::{TermHelper, TermHelperMut};
|
||||
use gl_search::{Schema, SearchDocument, SearchIndex, doc, IndexWriter, Value};
|
||||
use http::StatusCode;
|
||||
@@ -17,9 +18,10 @@ use ldp::traverse::Traverse;
|
||||
use ldp::{RdfSource, RdfSourceUpdateResponse, ResourceRequestBuilder, SerializationOptions};
|
||||
use oxigraph::io::RdfFormat;
|
||||
use oxigraph::model::vocab::{rdf, rdfs};
|
||||
use oxigraph::model::{BaseDirection, BlankNode, Dataset, NamedNode, NamedNodeRef, NamedOrBlankNode, Quad, Term, Triple};
|
||||
use tracing::{debug, debug_span, error, info, trace};
|
||||
use crate::app::Message::URLInputSubmitted;
|
||||
use oxigraph::model::{BaseDirection, Dataset, NamedNode, Quad, Term};
|
||||
use tracing::{debug, debug_span, error, trace};
|
||||
use tracing::span::EnteredSpan;
|
||||
use crate::rdf::curie::CurieHelper;
|
||||
use crate::rdf::language;
|
||||
use crate::rdf::vocab::rda;
|
||||
use crate::widget::iri_input::iri_input;
|
||||
@@ -27,9 +29,11 @@ use crate::widget::iri_input::iri_input;
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum Message {
|
||||
None,
|
||||
RebuildIndex,
|
||||
Traverse,
|
||||
IndexRdfSource(RdfSource<Dataset>),
|
||||
CommitIndex,
|
||||
AddRdfSource(RdfSource<Dataset>),
|
||||
ConcludeTraversal,
|
||||
IndexQueryResults(HashMap<NamedNode, IndexEntry>),
|
||||
WindowClosed(window::Id),
|
||||
URLInputChanged(String),
|
||||
URLInputSubmitted,
|
||||
@@ -86,6 +90,7 @@ struct SearchState {
|
||||
|
||||
pub(crate) struct Publisher {
|
||||
http_client: ClientWithMiddleware,
|
||||
curie_helper: CurieHelper,
|
||||
ontology: Ontology,
|
||||
abbreviated_datatypes: Vec<String>,
|
||||
window_id: window::Id,
|
||||
@@ -94,7 +99,7 @@ pub(crate) struct Publisher {
|
||||
hovered_row: Option<QuadKey>,
|
||||
search_state: Option<SearchState>,
|
||||
index: SearchIndex,
|
||||
index_writer: Option<IndexWriter>,
|
||||
traversal: Option<(Dataset, EnteredSpan)>,
|
||||
show_overwrite_confirmation: bool,
|
||||
modified: bool,
|
||||
show_new_document_buttons: bool,
|
||||
@@ -102,6 +107,8 @@ pub(crate) struct Publisher {
|
||||
|
||||
impl Publisher {
|
||||
pub(crate) fn new() -> (Self, Task<Message>) {
|
||||
let curie_helper = CurieHelper::new(Ontology::prefixes().clone());
|
||||
|
||||
let ontology = debug_span!("Ontology Creation").in_scope(|| {
|
||||
Ontology::builder()
|
||||
.with_path("/home/alex/.local/share/org.graphofliberty.desktop/ontology")
|
||||
@@ -114,31 +121,12 @@ impl Publisher {
|
||||
.build()
|
||||
.expect("Failed to build search index");
|
||||
|
||||
/*debug_span!("Ontology Indexing").in_scope(|| {
|
||||
let mut counter = 0;
|
||||
let mut writer = index.writer().expect("Failed to build index writer");
|
||||
let index = ontology.index(&*language::ENGLISH_OR_UNTAGGED).expect("Unable to generate index of entities");
|
||||
for (individual, entry) in index {
|
||||
let mut document = doc!(
|
||||
Schema::type_field() => entry.catalog_id,
|
||||
Schema::iri_field() => individual.as_str(),
|
||||
Schema::curie_field() => ontology.abbreviate(individual.as_ref()),
|
||||
);
|
||||
for (key, value) in entry.fields {
|
||||
document.add_text(Schema::field(&key, language::ENGLISH_PRIMARY), value.as_str());
|
||||
}
|
||||
writer.add(document).expect("Failed to add document to search index");
|
||||
counter += 1;
|
||||
}
|
||||
writer.commit().expect("Failed to commit changes to search index");
|
||||
debug!("Added {counter} documents to search index");
|
||||
});*/
|
||||
|
||||
let mut abbreviated_datatypes = ontology
|
||||
.datatypes()
|
||||
.into_iter()
|
||||
.map(|node| ontology.abbreviate(node.as_ref()))
|
||||
.collect::<Vec<_>>();
|
||||
.map(|node| curie_helper.abbreviate(node.as_str())
|
||||
.unwrap_or(node.as_str().to_string())
|
||||
).collect::<Vec<_>>();
|
||||
abbreviated_datatypes.sort();
|
||||
|
||||
let (id, task) = window::open(Settings::default());
|
||||
@@ -157,6 +145,7 @@ impl Publisher {
|
||||
(
|
||||
Self {
|
||||
http_client,
|
||||
curie_helper,
|
||||
ontology,
|
||||
abbreviated_datatypes,
|
||||
window_id: id,
|
||||
@@ -165,7 +154,7 @@ impl Publisher {
|
||||
hovered_row: None,
|
||||
search_state: None,
|
||||
index,
|
||||
index_writer: None,
|
||||
traversal: None,
|
||||
show_overwrite_confirmation: false,
|
||||
modified: false,
|
||||
show_new_document_buttons: false,
|
||||
@@ -179,48 +168,80 @@ impl Publisher {
|
||||
trace!(?message);
|
||||
|
||||
match message {
|
||||
Message::RebuildIndex => {
|
||||
self.index
|
||||
.writer()
|
||||
.expect("Unable to create writer")
|
||||
.remove_all()
|
||||
.expect("Unable to clear index");
|
||||
|
||||
let query = Ontology::index_query(&*language::ENGLISH_OR_UNTAGGED);
|
||||
let query_results = self.ontology.execute_query(query).expect("Unable to generate index of entities");
|
||||
let results = Ontology::transform_index_results(query_results);
|
||||
task = Task::done(Message::IndexQueryResults(results));
|
||||
}
|
||||
Message::Traverse => {
|
||||
self.traversal = Some((self.ontology.to_dataset(), debug_span!("Repository Traversal").entered()));
|
||||
|
||||
let client = self.http_client.clone();
|
||||
let root = Url::parse(&self.url_input).expect("Invalid URL");
|
||||
let stream = Traverse::new(client, root, None);
|
||||
|
||||
self.index_writer = Some(self.index.writer().expect("Unable to create search index writer"));
|
||||
|
||||
task = Task::run(stream, |result| match result {
|
||||
Ok(rdf_source) => Message::IndexRdfSource(rdf_source),
|
||||
Ok(rdf_source) => Message::AddRdfSource(rdf_source),
|
||||
Err(err) => Message::ShowError(format!("Unable to fetch RDF Source: {err}")),
|
||||
})
|
||||
.chain(Task::done(Message::CommitIndex));
|
||||
}).chain(Task::done(Message::ConcludeTraversal));
|
||||
}
|
||||
Message::IndexRdfSource(rdf_source) => {
|
||||
for catalog_id in rdf_source.classes()
|
||||
.filter_map(|class| self.ontology.catalog_id(&class.into_owned())) {
|
||||
let mut document = SearchDocument::new();
|
||||
document.add_u64(Schema::type_field(), catalog_id);
|
||||
document.add_text(Schema::iri_field(), rdf_source.origin());
|
||||
Message::AddRdfSource(rdf_source) => {
|
||||
if let Some((traversal, _)) = &mut self.traversal {
|
||||
traversal.extend(rdf_source.dataset());
|
||||
}
|
||||
}
|
||||
Message::ConcludeTraversal => {
|
||||
if let Some((traversal, _)) = &self.traversal {
|
||||
let query = Ontology::index_query(&*language::ENGLISH_OR_UNTAGGED);
|
||||
let query_results = query.on_queryable_dataset(traversal)
|
||||
.execute()
|
||||
.expect("Unable to generate index of entities");
|
||||
let results = Ontology::transform_index_results(query_results);
|
||||
task = Task::done(Message::IndexQueryResults(results));
|
||||
}
|
||||
self.traversal = None;
|
||||
}
|
||||
Message::IndexQueryResults(results) => {
|
||||
let mut writer = self.index.writer().expect("Failed to build index writer");
|
||||
let curie_helper = self.curie_helper.clone();
|
||||
let index_task = tokio::task::spawn_blocking(move || {
|
||||
debug_span!("Indexing").in_scope(|| {
|
||||
let mut counter = 0;
|
||||
for (individual, entry) in results {
|
||||
debug!(%individual, ?entry);
|
||||
let mut document = doc!(
|
||||
Schema::type_field() => entry.catalog_id,
|
||||
Schema::iri_field() => individual.as_str(),
|
||||
);
|
||||
|
||||
/*for quad in rdf_source.dataset() {
|
||||
if let Some(field) =
|
||||
self.ontology.field_for_property(&quad.predicate.into_owned())
|
||||
&& let TermRef::Literal(literal) = quad.object
|
||||
{
|
||||
let field = Schema::schema()
|
||||
.get_field(field.name.as_str())
|
||||
.expect("Field not found in schema");
|
||||
document.add_text(field, literal.value());
|
||||
if let Some(curie) = curie_helper.abbreviate(individual.as_str()) {
|
||||
document.add_text(Schema::curie_field(), curie);
|
||||
}
|
||||
|
||||
for (key, value) in entry.fields {
|
||||
document.add_text(Schema::field(&key, language::ENGLISH_PRIMARY), value.as_str());
|
||||
}
|
||||
writer.add(document).expect("Failed to add document to search index");
|
||||
counter += 1;
|
||||
}
|
||||
}*/
|
||||
writer.commit().expect("Failed to commit changes to search index");
|
||||
debug!("Added {counter} documents to search index");
|
||||
});
|
||||
});
|
||||
|
||||
if let Some(writer) = &self.index_writer {
|
||||
writer.add(document).expect("Unable to add document to search index");
|
||||
task = Task::future(async {
|
||||
match index_task.await {
|
||||
Ok(()) => Message::None,
|
||||
Err(err) => Message::ShowError(err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::CommitIndex => {
|
||||
if let Some(writer) = &mut self.index_writer {
|
||||
writer.commit().expect("Unable to commit updates to search index");
|
||||
self.index_writer = None;
|
||||
}
|
||||
});
|
||||
}
|
||||
Message::WindowClosed(id) => {
|
||||
if self.window_id == id {
|
||||
@@ -297,7 +318,9 @@ impl Publisher {
|
||||
let term = TermHelper::new(&quad.object);
|
||||
let datatype = term
|
||||
.datatype()
|
||||
.map(|datatype| self.ontology.abbreviate(datatype));
|
||||
.map(|datatype| self.curie_helper.abbreviate(datatype.as_str())
|
||||
.unwrap_or(datatype.as_str().to_string()));
|
||||
|
||||
let datatype_state = combo_box::State::with_selection(
|
||||
self.abbreviated_datatypes.clone(),
|
||||
datatype.as_ref(),
|
||||
@@ -417,12 +440,13 @@ impl Publisher {
|
||||
}
|
||||
Message::DatatypeUpdated(key, Some(maybe_prefixed_iri)) => {
|
||||
let node = self
|
||||
.ontology
|
||||
.curie_helper
|
||||
.expand(&maybe_prefixed_iri)
|
||||
.unwrap_or(NamedNode::new_unchecked(&maybe_prefixed_iri));
|
||||
.unwrap_or(maybe_prefixed_iri);
|
||||
|
||||
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
|
||||
let mut term = TermHelperMut::new(&mut quad.object);
|
||||
term.set_datatype(Some(node));
|
||||
term.set_datatype(Some(NamedNode::new_unchecked(node)));
|
||||
self.modified = true;
|
||||
}
|
||||
}
|
||||
@@ -535,14 +559,14 @@ impl Publisher {
|
||||
Message::NavigateToPredicate(key) => {
|
||||
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
|
||||
self.url_input = quad.predicate.as_str().to_string();
|
||||
task = Task::done(URLInputSubmitted);
|
||||
task = Task::done(Message::URLInputSubmitted);
|
||||
}
|
||||
}
|
||||
Message::NavigateToObject(key) => {
|
||||
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
|
||||
if let Term::NamedNode(node) = &quad.object {
|
||||
self.url_input = node.as_str().to_string();
|
||||
task = Task::done(URLInputSubmitted);
|
||||
task = Task::done(Message::URLInputSubmitted);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -578,7 +602,7 @@ impl Publisher {
|
||||
container(space()).width(BUTTON_WIDTH)
|
||||
};
|
||||
|
||||
let predicate_input_base = iri_input(self.ontology.prefixes(), "Predicate", triple.predicate.as_str());
|
||||
let predicate_input_base = iri_input(&self.curie_helper, "Predicate", triple.predicate.as_str());
|
||||
let predicate_input = if self.ontology.is_read_only(triple.as_ref()) {
|
||||
predicate_input_base
|
||||
} else {
|
||||
@@ -587,6 +611,9 @@ impl Publisher {
|
||||
.on_shift_click(Message::NavigateToPredicate(key))
|
||||
};
|
||||
|
||||
let predicate_info = self.ontology.info(&triple.predicate, &*language::ENGLISH_OR_UNTAGGED);
|
||||
let predicate_label = container(text(predicate_info.label));
|
||||
|
||||
let term = TermHelper::new(&triple.object);
|
||||
|
||||
let value_label = term.value_as_named_node().and_then(|node| {
|
||||
@@ -607,7 +634,7 @@ impl Publisher {
|
||||
})
|
||||
.unwrap_or(Horizontal::Left);
|
||||
|
||||
let object_input_base = iri_input(self.ontology.prefixes(), "Object", value.as_str());
|
||||
let object_input_base = iri_input(&self.curie_helper, "Object", value.as_str());
|
||||
let object_input = if self.ontology.is_read_only(triple.as_ref()) {
|
||||
object_input_base
|
||||
} else {
|
||||
@@ -617,7 +644,9 @@ impl Publisher {
|
||||
.on_shift_click(Message::NavigateToObject(key))
|
||||
};
|
||||
|
||||
let selected_datatype = term.datatype().map(|node| self.ontology.abbreviate(node));
|
||||
let selected_datatype = term.datatype()
|
||||
.and_then(|node| self.curie_helper.abbreviate(node.as_str()));
|
||||
|
||||
let datatype_selector: Element<Message> = if state.read_only {
|
||||
selected_datatype.map(text).into()
|
||||
} else {
|
||||
@@ -667,8 +696,9 @@ impl Publisher {
|
||||
let row = row![
|
||||
button_area,
|
||||
predicate_input,
|
||||
value_label,
|
||||
predicate_label,
|
||||
object_input,
|
||||
value_label,
|
||||
datatype_selector,
|
||||
language_input,
|
||||
direction_slider,
|
||||
@@ -685,7 +715,10 @@ impl Publisher {
|
||||
entities: impl IntoIterator<Item = LabeledIri>,
|
||||
) -> Element<'_, Message> {
|
||||
let buttons = entities.into_iter().map(|entity| {
|
||||
let label = format!("{} ({})", entity.label, self.ontology.abbreviate(entity.iri.as_ref()));
|
||||
let abbreviation = self.curie_helper.abbreviate(entity.iri.as_str())
|
||||
.unwrap_or_else(|| entity.iri.as_str().to_string());
|
||||
|
||||
let label = format!("{} ({})", entity.label, abbreviation);
|
||||
button(text(label))
|
||||
.on_press(Message::NewDocument(entity.iri))
|
||||
.into()
|
||||
@@ -717,7 +750,9 @@ impl Publisher {
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default();
|
||||
|
||||
let abbreviated_iri = self.ontology.abbreviate(NamedNodeRef::new_unchecked(iri));
|
||||
let abbreviated_iri = self.curie_helper.abbreviate(iri)
|
||||
.unwrap_or_else(|| iri.to_string());
|
||||
|
||||
button(text(abbreviated_iri).wrapping(Wrapping::Word))
|
||||
.on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri)))
|
||||
.style(button::text)
|
||||
@@ -764,9 +799,11 @@ impl Publisher {
|
||||
|
||||
let add_row_button = button("Add row").on_press(Message::AddRow(None));
|
||||
|
||||
let index_button = button("Index").on_press(Message::Traverse);
|
||||
let reindex_ontology_button = button("Rebuild index").on_press(Message::RebuildIndex);
|
||||
|
||||
let address_input = iri_input(self.ontology.prefixes(), "URL", &self.url_input)
|
||||
let traverse_button = button("Traverse").on_press(Message::Traverse);
|
||||
|
||||
let address_input = iri_input(&self.curie_helper, "URL", &self.url_input)
|
||||
.on_input(Message::URLInputChanged)
|
||||
.on_submit(Message::URLInputSubmitted)
|
||||
.on_control_click(Message::OpenQueryWindow(SearchResultClickAction::URLInput));
|
||||
@@ -797,7 +834,8 @@ impl Publisher {
|
||||
let content = column![
|
||||
row![
|
||||
add_row_button,
|
||||
index_button,
|
||||
reindex_ontology_button,
|
||||
traverse_button,
|
||||
address_input,
|
||||
save_button,
|
||||
],
|
||||
|
||||
+3
-3
@@ -18,10 +18,10 @@ fn main() -> color_eyre::Result<()> {
|
||||
.init();
|
||||
color_eyre::install()?;
|
||||
|
||||
let application = iced::daemon(Publisher::new, Publisher::update, Publisher::view)
|
||||
iced::daemon(Publisher::new, Publisher::update, Publisher::view)
|
||||
.title(Publisher::title)
|
||||
.subscription(Publisher::subscription);
|
||||
.subscription(Publisher::subscription)
|
||||
.run()?;
|
||||
|
||||
application.run()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CurieHelper {
|
||||
prefixes: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl CurieHelper {
|
||||
pub fn new(prefixes: BTreeMap<String, String>) -> Self {
|
||||
Self {
|
||||
prefixes,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn abbreviate(&self, iri: &str) -> Option<String> {
|
||||
for (name, base) in &self.prefixes {
|
||||
if let Some(local_name) = iri.strip_prefix(base) {
|
||||
return Some(format!("{name}:{local_name}"));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn expand(&self, abbreviated_iri: &str) -> Option<String> {
|
||||
let (prefix, name) = abbreviated_iri.split_once(':')?;
|
||||
self.prefixes
|
||||
.get(prefix)
|
||||
.map(|base| format!("{base}{name}"))
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,5 @@ pub(crate) mod term_helper;
|
||||
pub mod vocab;
|
||||
pub(crate) mod materialize;
|
||||
pub(crate) mod conversion;
|
||||
pub(crate) mod language;
|
||||
pub(crate) mod language;
|
||||
pub(crate) mod curie;
|
||||
+53
-43
@@ -1,18 +1,20 @@
|
||||
use crate::error;
|
||||
use crate::rdf::vocab::gl;
|
||||
use oxigraph::model::vocab::{rdf, rdfs, xsd};
|
||||
use oxigraph::model::{Dataset, Graph, LiteralRef, NamedNode, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, Term, TermRef, Triple, TripleRef};
|
||||
use oxigraph::sparql::{QueryResults, SparqlEvaluator};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
use oxigraph::model::{Dataset, Graph, GraphName, GraphNameRef, LiteralRef, NamedNode, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, Term, TermRef, Triple, TripleRef};
|
||||
use oxigraph::sparql::{PreparedSparqlQuery, QueryResults, SparqlEvaluator};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::fmt::Display;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
use oxigraph::store::Store;
|
||||
use tracing::{debug_span, info};
|
||||
use tracing::debug_span;
|
||||
use crate::rdf::{conversion, materialize};
|
||||
use crate::rdf::conversion::{quad_into_term, term_into_named_node, term_into_string, term_to_named_node};
|
||||
use crate::rdf::language::LanguageCondition;
|
||||
|
||||
const ONTOLOGY_PREFIX: &str = "https://graphofliberty.org/2026/04/ont/";
|
||||
const ONTOLOGY_GRAPH_NAME: GraphNameRef = GraphNameRef::NamedNode(NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont"));
|
||||
|
||||
static PREFIXES: LazyLock<BTreeMap<String, String>> = LazyLock::new(|| {
|
||||
BTreeMap::from_iter([
|
||||
("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
|
||||
@@ -44,12 +46,13 @@ static PREFIXES: LazyLock<BTreeMap<String, String>> = LazyLock::new(|| {
|
||||
("rdax", "http://rdaregistry.info/Elements/x/"),
|
||||
("schema", "https://schema.org/"),
|
||||
("quill", "http://fedora.quill.lan/rest/"),
|
||||
("gl", "https://graphofliberty.org/2026/04/ont/"),
|
||||
("gl", "ONTOLOGY_PREFIX"),
|
||||
].map(|(k, v)| (k.to_string(), v.to_string())))
|
||||
});
|
||||
|
||||
pub struct OntologyBuilder {
|
||||
path: Option<PathBuf>,
|
||||
materialize_inferences: bool,
|
||||
}
|
||||
|
||||
impl OntologyBuilder {
|
||||
@@ -59,9 +62,18 @@ impl OntologyBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn materialize_inferences(mut self) -> Self {
|
||||
self.materialize_inferences = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> error::Result<Ontology> {
|
||||
let mut store = if let Some(path) = self.path {
|
||||
Store::open_read_only(path)
|
||||
if self.materialize_inferences {
|
||||
Store::open(path)
|
||||
} else {
|
||||
Store::open_read_only(path)
|
||||
}
|
||||
} else {
|
||||
Store::new()
|
||||
}?;
|
||||
@@ -71,10 +83,12 @@ impl OntologyBuilder {
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
.collect::<HashMap<String, String>>();
|
||||
|
||||
/*materialize::same_as(&mut store)?;
|
||||
materialize::super_properties(&mut store)?;
|
||||
materialize::super_classes(&mut store)?;
|
||||
debug_span!("Optimize Ontology").in_scope(|| store.optimize())?;*/
|
||||
if self.materialize_inferences {
|
||||
materialize::same_as(&mut store)?;
|
||||
materialize::super_properties(&mut store)?;
|
||||
materialize::super_classes(&mut store)?;
|
||||
debug_span!("Optimize Ontology").in_scope(|| store.optimize())?;
|
||||
}
|
||||
|
||||
let mut indexed_by = HashMap::new();
|
||||
for quad in store.quads_for_pattern(None, Some(gl::INDEXED_BY_FIELD), None, None)
|
||||
@@ -92,6 +106,7 @@ impl OntologyBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IndexEntry {
|
||||
pub catalog_id: u64,
|
||||
pub fields: HashMap<String, String>,
|
||||
@@ -142,14 +157,22 @@ impl Ontology {
|
||||
pub fn builder() -> OntologyBuilder {
|
||||
OntologyBuilder {
|
||||
path: None,
|
||||
materialize_inferences: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prefixes(&self) -> &BTreeMap<String, String> {
|
||||
pub fn prefixes() -> &'static BTreeMap<String, String> {
|
||||
&*PREFIXES
|
||||
}
|
||||
|
||||
pub fn index(&self, language: &LanguageCondition) -> error::Result<HashMap<NamedNode, IndexEntry>> {
|
||||
pub fn to_dataset(&self) -> Dataset {
|
||||
self.store
|
||||
.quads_for_pattern(None, None, None, Some(ONTOLOGY_GRAPH_NAME))
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Dataset>()
|
||||
}
|
||||
|
||||
pub fn index_query(language: &LanguageCondition) -> PreparedSparqlQuery {
|
||||
let language_filter = language.to_filter_expression("fieldValue");
|
||||
let query = format!(r#"SELECT ?individual ?catalogId ?fieldName ?fieldValue
|
||||
WHERE {{
|
||||
@@ -165,12 +188,20 @@ WHERE {{
|
||||
{language_filter}
|
||||
}}"#);
|
||||
let mut sparql = SparqlEvaluator::new()
|
||||
.with_prefix("gl", "https://graphofliberty.org/2026/04/ont/")?
|
||||
.parse_query(&query)?;
|
||||
.with_prefix("gl", ONTOLOGY_PREFIX).unwrap()
|
||||
.parse_query(&query)
|
||||
.unwrap();
|
||||
sparql.dataset_mut().set_default_graph_as_union();
|
||||
sparql
|
||||
}
|
||||
|
||||
pub fn execute_query(&self, query: PreparedSparqlQuery) -> error::Result<QueryResults<'_>> {
|
||||
Ok(query.on_store(&self.store).execute()?)
|
||||
}
|
||||
|
||||
pub fn transform_index_results(query_results: QueryResults) -> HashMap<NamedNode, IndexEntry> {
|
||||
let mut results = HashMap::new();
|
||||
if let QueryResults::Solutions(solutions) = sparql.on_store(&self.store).execute()? {
|
||||
if let QueryResults::Solutions(solutions) = query_results {
|
||||
for solution in solutions.filter_map(Result::ok) {
|
||||
let individual = solution.get("individual").and_then(conversion::term_to_named_node);
|
||||
let catalog_id = solution.get("catalogId").and_then(conversion::term_to_u64);
|
||||
@@ -187,8 +218,7 @@ WHERE {{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
results
|
||||
}
|
||||
|
||||
pub fn catalog_id(&self, class: &NamedNode) -> Option<u64> {
|
||||
@@ -211,7 +241,7 @@ WHERE {{
|
||||
}}"#);
|
||||
let mut sparql = SparqlEvaluator::new()
|
||||
.with_prefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#")?
|
||||
.with_prefix("gl", "https://graphofliberty.org/2026/04/ont/")?
|
||||
.with_prefix("gl", ONTOLOGY_PREFIX)?
|
||||
.parse_query(&query)?;
|
||||
sparql.dataset_mut().set_default_graph_as_union();
|
||||
|
||||
@@ -232,32 +262,12 @@ WHERE {{
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
pub fn abbreviate(&self, node: NamedNodeRef<'_>) -> String {
|
||||
for (prefix_name, prefix_iri) in &self.prefixes {
|
||||
if let Some(local_name) = node.as_str().strip_prefix(prefix_iri) {
|
||||
return if local_name.is_empty() {
|
||||
format!("{prefix_name}:")
|
||||
} else {
|
||||
format!("{prefix_name}:{local_name}")
|
||||
};
|
||||
}
|
||||
}
|
||||
node.as_str().to_string()
|
||||
}
|
||||
|
||||
pub fn expand(&self, prefixed_iri: &str) -> Option<NamedNode> {
|
||||
let (prefix, name) = prefixed_iri.split_once(':')?;
|
||||
self.prefixes
|
||||
.get(prefix)
|
||||
.map(|base| NamedNode::new_unchecked(format!("{base}{name}")))
|
||||
}
|
||||
|
||||
pub fn info(&self, iri: &NamedNode, language: &LanguageCondition) -> LabeledIri {
|
||||
let label = self.store.quads_for_pattern(Some(iri.as_ref().into()), Some(rdfs::LABEL), None, None)
|
||||
.filter_map(Result::ok)
|
||||
.map(quad_into_term)
|
||||
.map(conversion::quad_into_term)
|
||||
.filter(|term| language.primary_matches_term(term))
|
||||
.filter_map(term_into_string)
|
||||
.filter_map(conversion::term_into_string)
|
||||
.next()
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -273,9 +283,9 @@ WHERE {{
|
||||
.filter_map(|quad| {
|
||||
let label = self.store.quads_for_pattern(Some(quad.subject.as_ref().into()), Some(rdfs::LABEL), None, None)
|
||||
.filter_map(Result::ok)
|
||||
.map(quad_into_term)
|
||||
.map(conversion::quad_into_term)
|
||||
.filter(|term| language.primary_matches_term(term))
|
||||
.filter_map(term_into_string)
|
||||
.filter_map(conversion::term_into_string)
|
||||
.next();
|
||||
if let Some(label) = label && let NamedOrBlankNode::NamedNode(subject) = quad.subject {
|
||||
Some(LabeledIri {
|
||||
|
||||
@@ -7,6 +7,7 @@ use iced::advanced::{Layout, Widget};
|
||||
use iced::advanced::widget::{tree, Tree};
|
||||
use iced::mouse::{Cursor, Interaction};
|
||||
use iced::widget::text_input::Catalog;
|
||||
use crate::rdf::curie::CurieHelper;
|
||||
|
||||
pub struct State {
|
||||
control: bool,
|
||||
@@ -18,39 +19,23 @@ where
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
prefixes: &'a BTreeMap<String, String>,
|
||||
curie_helper: &'a CurieHelper,
|
||||
on_control_click: Option<Message>,
|
||||
on_shift_click: Option<Message>,
|
||||
text_input: widget::TextInput<'a, Message, Theme, Renderer>,
|
||||
}
|
||||
|
||||
fn abbreviate(prefixes: &BTreeMap<String, String>, iri: &str) -> Option<String> {
|
||||
for (name, base) in prefixes {
|
||||
if let Some(local_name) = iri.strip_prefix(base) {
|
||||
return Some(format!("{name}:{local_name}"));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn expand(prefixes: &BTreeMap<String, String>, abbreviated_iri: &str) -> Option<String> {
|
||||
let (prefix, name) = abbreviated_iri.split_once(':')?;
|
||||
prefixes
|
||||
.get(prefix)
|
||||
.map(|base| format!("{base}{name}"))
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> IriInput<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
pub fn new(prefixes: &'a BTreeMap<String, String>, placeholder: &str, iri: &str) -> Self {
|
||||
let display_value = abbreviate(prefixes, iri).unwrap_or_else(|| iri.to_string());
|
||||
pub fn new(curie_helper: &'a CurieHelper, placeholder: &str, iri: &str) -> Self {
|
||||
let display_value = curie_helper.abbreviate(iri).unwrap_or_else(|| iri.to_string());
|
||||
let text_input = widget::TextInput::new(placeholder, &display_value);
|
||||
Self {
|
||||
prefixes,
|
||||
curie_helper,
|
||||
on_control_click: None,
|
||||
on_shift_click: None,
|
||||
text_input,
|
||||
@@ -74,7 +59,7 @@ where
|
||||
|
||||
pub fn on_input(mut self, on_input: impl Fn(String) -> Message + 'a) -> Self {
|
||||
let wrapped = move |value: String| {
|
||||
let expanded_value = expand(self.prefixes, &value);
|
||||
let expanded_value = self.curie_helper.expand(&value);
|
||||
on_input(expanded_value.unwrap_or_else(|| value))
|
||||
};
|
||||
|
||||
@@ -100,13 +85,13 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iri_input<'a, Message, Theme, Renderer>(prefixes: &'a BTreeMap<String, String>, placeholder: &str, iri: &str) -> IriInput<'a, Message, Theme, Renderer>
|
||||
pub fn iri_input<'a, Message, Theme, Renderer>(curie_helper: &'a CurieHelper, placeholder: &str, iri: &str) -> IriInput<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
IriInput::new(prefixes, placeholder, iri)
|
||||
IriInput::new(curie_helper, placeholder, iri)
|
||||
}
|
||||
|
||||
impl <Message, Theme, Renderer> Widget<Message, Theme, Renderer> for IriInput<'_, Message, Theme, Renderer>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
//mod search;
|
||||
|
||||
//pub use search::{SearchWindow, SearchWindowMessage};
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
use iced::{window, Element, Task};
|
||||
use iced::widget::{column, mouse_area, space, table, text_input};
|
||||
use tracing::info;
|
||||
use gl_search::{SearchIndex, SearchIndexBuilder};
|
||||
use gl_types::CatalogEntryType;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum SearchWindowMessage {
|
||||
QueryInputUpdated(String),
|
||||
SearchResultSelected(String),
|
||||
}
|
||||
|
||||
pub struct SearchWindow {
|
||||
window_id: window::Id,
|
||||
index: SearchIndex,
|
||||
query_input: String,
|
||||
results: Vec<String>,
|
||||
}
|
||||
|
||||
impl SearchWindow {
|
||||
pub fn new(window_id: window::Id) -> Self {
|
||||
let index = SearchIndex::builder()
|
||||
.with_path("/tmp/name_index")
|
||||
.build().expect("Unable to load search index");
|
||||
|
||||
Self {
|
||||
window_id,
|
||||
index,
|
||||
query_input: String::new(),
|
||||
results: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> window::Id {
|
||||
self.window_id
|
||||
}
|
||||
|
||||
pub fn title(&self) -> String {
|
||||
"Graph of Liberty Publisher: Search".to_string()
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: SearchWindowMessage) -> Task<SearchWindowMessage> {
|
||||
match message {
|
||||
SearchWindowMessage::QueryInputUpdated(value) => {
|
||||
self.results = self.index.query(CatalogEntryType::Person, value.as_str()).unwrap();
|
||||
self.query_input = value;
|
||||
}
|
||||
SearchWindowMessage::SearchResultSelected(value) => {
|
||||
info!(value);
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub fn view(&self) -> Element<'_, SearchWindowMessage> {
|
||||
let name_column = table::column("Name", |name: &String| {
|
||||
mouse_area(name.as_str()).on_double_click(SearchWindowMessage::SearchResultSelected(name.clone()))
|
||||
});
|
||||
column![
|
||||
text_input("Query", &self.query_input).on_input(SearchWindowMessage::QueryInputUpdated),
|
||||
table(vec![name_column], &self.results),
|
||||
].into()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user