...
 
Commits (26)
......@@ -10,18 +10,6 @@ GeoPub is written in [[https://clojurescript.org/][ClojureScript]]. [[http://sha
shadow-cljs seamlessly handles JavaScript libraries that are installed via NPM. See also [[https://shadow-cljs.github.io/docs/UsersGuide.html#js-deps][the shadow-cljs documentaiton]].
*** Browserify
The [[https://github.com/rdfjs/N3.js][N3.js]] library for parsing Turtle does requires Node.js specific stuff and needs to be transformed before it can run in the browser. For this we use [[http://browserify.org/][browserify]].
The folder [[../resources/n3.js][resources/n3.js]] contains a npm project that will build a bundle of N3.js that runs in the browser. The bundle is written to [[../src/cljs_rdf/n3.js][src/cljs_rdf/n3.js]] and is checked into git.
Shadow-cljs allows the created bundle to be directly loaded into ClojureScript (see [[https://shadow-cljs.github.io/docs/UsersGuide.html#classpath-js][documentation]]).
To recreate the bundle you need to run ~npm run build~ in the [[../resources/n3.js][resources/n3.js]] folder.
**** TODO Try and use browserify replacement libraries instead of using browserify (https://github.com/browserify/stream-browserify)
** Emacs integration
Emacs Cider integration works (for me) when using the ~shadow-cljs~ tool to
......
This diff is collapsed.
......@@ -6,12 +6,16 @@
"doc": "docs"
},
"dependencies": {
"@rdfjs/data-model": "^1.1.2",
"create-react-class": "^15.6.3",
"leaflet": "^1.6.0",
"n3": "^1.3.5",
"rdf-parse": "^1.2.1",
"react": "~16.9.0",
"react-dom": "~16.9.0",
"react-leaflet": "^2.6.1",
"shadow-cljs": "^2.8.90"
"shadow-cljs": "^2.8.90",
"stream-browserify": "^2.0.2"
},
"devDependencies": {},
"scripts": {
......
This folder contains files required to create a bundle of N3.js that runs in the browser.
module.exports = require('n3')
This diff is collapsed.
{
"name": "geopub-n3.js",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "browserify -s n3 index.js -o ../../src/rdf/n3.js"
},
"author": "",
"license": "GPL-3.0-or-later",
"dependencies": {
"browserify": "^16.5.0",
"n3": "^1.3.5"
}
}
......@@ -6,153 +6,138 @@ body {
#container {
display: flex;
height: 100vh;
flex-direction: column;
}
#sidebar {
height: 100vh;
flex: 0 0 15vw;
#topbar {
width: 100vw;
height: 70px;
/* flex: 0 0 15vw; */
margin: auto;
background-color: darkblue;
background-color: #4782a7;
color: white;
display: flex;
flex-direction: row;
align-items: center;
}
#sidebar header {
#topbar header {
margin-left: 20px;
}
nav ul {
#topbar nav {
margin: 0 0 0 50px;
}
#topbar nav ul {
list-style-type: none;
margin: 50px auto 50px auto;
margin: auto 0 auto 0;
padding: 0;
}
nav li {
margin: 15px;
#topbar nav li {
display: inline;
margin: 0 20px 0 20px;
}
nav li a {
#topbar nav li a {
color: white;
text-decoration: none;
font-size: 1.4em;
}
main {
flex-grow: 7;
max-height: 100vh;
overflow: auto;
}
#timeline {
margin: 50px;
}
#store {
margin: 50px;
}
#mapid {
margin: auto;
}
#liked hr {
color: lightgrey;
.ui-page {
display: flex;
align-items: stretch;
flex-grow: 1;
overflow: auto;
}
#tours hr {
color: lightgrey;
.ui-page main {
}
.activity {
margin: 15px;
padding: 10px;
border: 1px solid grey;
border-radius: 5px;
.ui-page .sidebar {
flex-basis: 15rem;
border-right: 0.2px solid lightgrey;
padding: 20px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.activity .meta {
color: grey;
font-weight: light;
font-size: small;
margin: 10px;
.ui-page .sidebar nav ul {
list-style-type: none;
}
.activity details {
font-weight: light;
font-size: small;
.ui-page .sidebar nav ul li {
margin: 20px 0 20px 0;
font-size: 1.2em;
}
.info-text {
padding: 7px;
margin: 5px 50px 5px 30px;
.ui-page main {
overflow: auto;
margin-top: 20px;
margin-left: 30px;
flex-grow: 1;
}
.selected {
/* border-left: 10px solid grey; */
/* background-color: #e9d2c0; */
.object {
}
.activity:hover {
/* background-color: #e9d2c0; */
.object header {
}
.activity .meta .type {
.object header .raw-id {
color: grey;
font-size: medium;
margin-left: 10px;
}
.activity .meta .published {
float: right;
}
.tour-status.open {
background-color: lightgreen;
}
.tour-status.warning {
background-color: orange;
.object header .raw-id a {
color: grey;
}
.tour-status.closed {
background-color: red;
color: white;
.object header .subtitle {
color: grey;
font-size: medium;
}
.object {
min-height: 50px;
margin: 10px;
.object pre {
overflow: auto;
width: 80%;
padding: 10px;
}
.object .like {
float: right;
.object .object-property-list dd {
margin-bottom: 2em;
}
.tour-image {
width: 100px;
order: 0;
}
#create-note .text-input{
width: 90%;
.activity {
margin: 15px;
padding: 10px;
border: 1px solid grey;
border-radius: 5px;
}
#create-note .latlng {
margin-top: 20px;
.activity .meta {
color: grey;
font-weight: light;
font-size: small;
margin: 10px;
}
#create-note .send-button{
margin-top: 5px;
margin-left: 60%;
width: 35%;
}
dl dt {
font-weight: bold;
.activity details {
font-weight: light;
font-size: small;
}
.actions-list {
list-style-type: none;
.activity .meta .type {
margin-left: 10px;
}
.actions-list li {
margin: 5px;
.activity .meta .published {
float: right;
}
This diff is collapsed.
(ns geopub.data.activitypub
(ns activitypub.core
"Helpers to deal with ActivityPub data"
(:require-macros [cljs.core.logic :refer [run* fresh]])
(:require [rdf.core :as rdf]
......
......@@ -16,20 +16,15 @@
;; along with GeoPub. If not, see <https://www.gnu.org/licenses/>.
(ns geopub.core
(:require-macros [cljs.core.async :refer [go]]
[cljs.core.logic :refer [run* fresh run]])
(:require [reagent.core :as r]
[cljs.core.async :refer [<!]]
[cljs.core.logic :as l]
[clojure.set :refer [intersection]]
[geopub.ns :refer [as rdfs]]
[goog.Uri :as uri]
[geopub.state]
[geopub.ui.map]
[geopub.ui.store]
[geopub.ui.timeline]
[geopub.cpub.core :as cpub]
[geopub.data.activitypub :as activitypub]
[rdf.core :as rdf]
[rdf.graph.map]
[geopub.ui.activity]
[geopub.ui.browse]
[geopub.data.rdf :refer [get-rdf]]
[geopub.cpub :as cpub]
[reitit.core :as rc]
[reitit.frontend :as rf]
[reitit.frontend.easy :as rfe]))
......@@ -37,67 +32,59 @@
;; ====================== Config =================
;; (def server-url "https://ap-dev.miaengiadina.ch/")
(def server-url "http://localhost:4000/")
(def server-url
(uri/parse "http://localhost:4000/"))
;; Currently the actor and auth is hardcoded.
(def actor-id (str server-url "users/alice"))
(def auth {:username "alice" :password "123"})
;; ============== State and helpers ==============
(defonce state (r/atom {:store (rdf.graph.map/graph)}))
(defn state-store [state]
"Return the datastore"
(:store @state))
(defn- add-triples-to-store! [state triples]
"Helper to add triples to the store"
(swap! state
(fn [s]
(assoc s :store
(reduce rdf/graph-add (:store s) triples)))))
(defn- go-add-triples-to-store! [chan]
"Add triples from a channel to the store"
(go (add-triples-to-store! state (<! chan))))
(defn reset-store []
"Helper to reset the store"
(swap! state #(assoc % :store (rdf.graph.map/graph))))
(defonce state (geopub.state/init))
;; ============== Start fetching data ============
(defn load-ontologies []
(go-add-triples-to-store! (cpub/get-activitystreams-ontology)))
(geopub.state/add-triples! state
(get-rdf "activitystreams2.ttl"
{:content-type "text/turtle"}))
(geopub.state/add-triples! state
(get-rdf "schema.ttl" {:content-type "text/turtle"})))
(defn cpub-get-data! []
"Get data from CPub server"
;; get public timeline
(go-add-triples-to-store! (cpub/get-public-timeline server-url))
(geopub.state/add-triples! state
(cpub/get-public-timeline server-url))
;; get actor profile
(go-add-triples-to-store! (cpub/get-rdf actor-id auth))
(geopub.state/add-triples! state
(get-rdf actor-id {:auth auth}))
;; get actor inbox TODO: figure out outbox from actor object
(go-add-triples-to-store! (cpub/get-rdf (str actor-id "/inbox") auth))
(geopub.state/add-triples! state (get-rdf (str actor-id "/inbox") {:basic-auth auth}))
;; get actor outbox
(go-add-triples-to-store! (cpub/get-rdf (str actor-id "/outbox") auth)))
(geopub.state/add-triples! state (get-rdf (str actor-id "/outbox") {:basic-auth auth})))
;; (defonce refresher
;; "aka the cpu killer"
;; (js/setInterval #(cpub-get-data!) 2000))
;; ==================== UI =======================
(def default-view geopub.ui.timeline/view)
(def default-view geopub.ui.activity/view)
(def routes
[["timeline"
{:name ::timeline
:view geopub.ui.timeline/view}]
[["activity"
{:name ::activity
:view geopub.ui.activity/view}]
["browse/description/:iri"
{:name ::browse
:view geopub.ui.browse/description-view
:parameters {:path {:iri string?}}}]
["browse/type/:iri"
{:name ::browse-type
:view geopub.ui.browse/type-view
:parameters {:path {:iri string?}}}]
["store"
{:name ::store
......@@ -109,40 +96,29 @@
(defn ui [state]
[:div#container
[:div#sidebar
[:div#topbar
[:header
[:h1 "GeoPub"]]
[:nav
[:ul
[:li [:a {:href "#timeline"} "Timeline"]]
[:li [:a {:href "#store"} "Store"]]
[:li [:a {:href "#map"} "Map"]]]
;; [:hr]
[:li [:a {:href (rfe/href ::activity)} "Activity"]]
[:li [:a {:href (rfe/href ::store)} "Store"]]
[:li [:a {:href (rfe/href ::map)} "Map"]]]]]
;; [:ul
;; [:li [:a {:href "#settings"} "Settings"]]]
]
;; [:div#debug
;; [:code
;; (str @state)]]
]
[:main
(let [view (:view (:current-route @state))]
(let [view (get-in @state [:current-route :data :view])]
(if view
[view state]
[default-view state]))]])
[default-view state]))])
(defn init! []
(load-ontologies)
(rfe/start!
(rf/router routes)
(fn [match]
(swap! state #(assoc % :current-route (:data match))))
(swap! state #(assoc % :current-route match)))
;; set to false to enable HistoryAPI
{:use-fragment true})
(r/render [ui state]
......@@ -150,13 +126,14 @@
cpub-get-data!))
;; how to like something:
;; (-> (activitypub/like (rdf/iri "http://openengiadina.net/"))
;; (cpub/post-rdf (str actor-id "/outbox") auth))
;; (reset-store)
;; (cpub-get-data!)
;; NOTE: The mystery why the size of the store increases when loading the ontology: Blank Nodes. N3.js gives new ids so blank nodes (and thing refering those blank nodes) are duplicted...need metadata
;; (load-ontologies)
;; (geopub.state/reset-graph! state)
;; (geopub.state/add-triples! state (get-rdf "https://www.rubensworks.net/"))
......@@ -15,49 +15,31 @@
;; You should have received a copy of the GNU General Public License
;; along with GeoPub. If not, see <https://www.gnu.org/licenses/>.
(ns geopub.cpub.core
(ns geopub.cpub
"Helpers for interacting with CPub server"
(:require-macros [cljs.core.async.macros :refer [go]])
(:require [cljs-http.client :as http]
[cljs.core.async :refer [<!]]
[rdf.n3 :as n3]
(:require [cljs.core.async :refer [<!]]
[rdf.core :as rdf]
;; [cljs-rdf.turtle :as turtle]
[clojure.pprint :refer [pprint]]))
[geopub.data.rdf :refer [get-rdf]]
[goog.Uri :as uri]))
(defn get-activitystreams-ontology []
"Retrieve ontology and return triples in a channel"
(go
(let
[body
(:body (<! (http/get "activitystreams2.ttl"
{:with-credentials? false
:headers {"Accept" "text/turtle"}
})))]
(n3/parse body))))
(defn get-rdf [url auth]
"Gets a sequence of triples from an url and returns them in a channel"
(go
(let
[body
(:body (<! (http/get url
{:with-credentials? false
:basic-auth auth
:headers {"Accept" "text/turtle"}
})))]
(n3/parse body))))
(defn public-timeline-url [server-url]
(if (uri? server-url)
(.setPath server-url "public")
(public-timeline-url (uri/parse server-url))))
(defn get-public-timeline [server-url]
"Returns a channel holding the content of the public timeline"
(get-rdf (str server-url "public") nil))
(-> server-url
(public-timeline-url)
(get-rdf)))
(defn post-rdf [data url auth]
(go
(let [body (<! (n3/encode data))]
(http/post url
{:with-credentials? false
:basic-auth auth
:headers {"Content-type" "text/turtle"}
:body body}))))
;; (defn post-rdf [data url auth]
;; (go
;; (let [body (<! (n3/encode data))]
;; (http/post url
;; {:with-credentials? false
;; :basic-auth auth
;; :headers {"Content-type" "text/turtle"}
;; :body body}))))
(ns geopub.data.rdf
"Helpers for displaying RDF data"
(:require [rdf.core :as rdf]
[rdf.description :refer [description-get
description-subject]]
[rdf.ns :as rdf-ns]
[rdf.n3 :as n3]
[rdf.parse :as rdf-parse]
[reagent.core :as r]
[cljs.core.async :as async :refer [<!]]
[cljs-http.client :as http]
[geopub.ns :as ns :refer [as rdfs schema]]
[goog.string]
[reitit.frontend.easy :as rfe])
(:require-macros [cljs.core.async :refer [go]]))
;; Data fetching
;; TODO Parser is capable of parsing streams, but cljs-http returns entire body in one go. Explore using the Streams API (https://developer.mozilla.org/en-US/docs/Web/API/Streams_API). This might also explain the amount of plumbing.
(defn- get-content-type [response]
(first
(clojure.string/split
(get-in response [:headers "content-type"]) ";")))
(defn- parse-http-request [url request & [opts]]
;; TODO Error handling
;; TODO This includes a lot of plumbing. The reason is that the content-type (required by the parser) is in the response and needs to be taken out before creating the parser. The interface for rdf-parse/parse does not seem right. Figure out how to do this nicer.
(let [parser-input (async/chan)
parser-output (async/chan)]
;; take response from request
(async/take! request
(fn [response]
;; put body into parser-input channel
(async/put! parser-input
(:body response)
;; and close the parser-input channel.
#(async/close! parser-input))
;; parse body and pipe to parser-output
(async/pipe
(rdf-parse/parse parser-input
:content-type (or (:content-type opts)
(get-content-type response))
:base-iri (str url)
:path (str url))
parser-output)))
;; return channel with parsed triples
parser-output))
(defn get-rdf [url & [opts]]
(let [request-opts (merge {:with-credentials? false
:headers {"Accept"
(clojure.string/join
", " (rdf-parse/content-types))}} opts)
request (http/get url request-opts)]
(parse-http-request url request opts)))
;; Reagent components
(defn iri-component
"Render an IRI as a link that can be followed in the internal browser."
[iri & {:keys [class]}]
(let [class (or class "iri")]
(cond
(rdf/iri? iri)
[:span
{:class class}
[:a
{:href (rfe/href :geopub.core/browse
{:iri (goog.string.urlEncode (rdf/iri-value iri))})}
(rdf/iri-value iri)]]
(seq? iri)
(iri-component (first iri))
:else
[:span.iri "-"])))
(defn literal-component [literal]
(rdf/literal-value literal))
(defn blank-node-component [bnode]
(str "_:" (rdf/blank-node-id bnode)))
(defn rdf-term-component [term]
(cond
(rdf/iri? term) [iri-component term]
(rdf/literal? term) [literal-component term]
(rdf/blank-node? term) [blank-node-component term]
(seq? term) [rdf-term-component (first term)]
:else "-"))
;; Description
(defn- description-type
"Helper to get type of subject being described. This defines what multimethod is used to render the description."
[object]
;; TODO the described object can have multiple types. Currently we use the first type. Allow a preference to be given.
(first (description-get object (rdf-ns/rdf :type))))
(defmulti description-header-component
"Render an appropriate title for a description"
(fn [object] (description-type object)))
(defmethod description-header-component
:default
[object]
[:header [:h1 (rdf-term-component (description-subject object))]])
(defmethod description-header-component
(rdfs "Class")
[object]
(let [title (or (first (description-get object (rdfs "label")))
(description-subject object))
sub-title (first (description-get object (rdfs "comment")))]
[:header
[:h1 [rdf-term-component title]
[:span.raw-id "(" (rdf-term-component (description-subject object)) ")"]]
(if sub-title [:p.subtitle [rdf-term-component sub-title]])]
))
(defn description-turtle-component [object]
(let [as-turtle (r/atom "")]
(fn []
;; encode description as RDF/Turtle
(go (swap! as-turtle (constantly (<! (n3/encode object)))))
[:code.turtle [:pre @as-turtle]])))
(defn description-property-list-component [object]
[:dl
(for
[triple (rdf/triple-seq object)]
;; TODO add unique key
^{:key (prn-str (rdf/triple-predicate triple))}
[:div.object-property-list
;; TODO: add key
[:dt [rdf-term-component (rdf/triple-predicate triple)]]
[:dd [rdf-term-component (rdf/triple-object triple)]]])])
(defmulti description-body-component
"Takes an rdf description and tries to create a nice view."
(fn [object] (description-type object)))
(defmethod description-body-component
:default
[object]
[:div.object-body
[description-property-list-component object]
;; [:details
;; [:summary "Turtle"]
;; [description-turtle-component object]]
])
(defn description-component
[object]
[:section.object
[description-header-component object]
[description-body-component object]])
;; (defmethod description-component
;; (as :Note)
;; [object]
;; [:div.object
;; (for [content (description-get object (as :content))]
;; [:p [literal-component content]])])
;; (defmethod description-component
;; (schema "Event")
;; [object]
;; [:div.object "I'm an event"])
;; Copyright © 2019 pukkamustard <[email protected]>
;;
;; This file is part of GeoPub.
;;
;; GeoPub is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; GeoPub is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GeoPub. If not, see <https://www.gnu.org/licenses/>.
(ns geopub.tours
(:require-macros [cljs.core.async.macros :refer [go]])
(:require [cljs.core.async :refer [<!]]
[reagent.core :as r]
[clojure.string :as str]
[cljs-http.client :as http]))
(defn post-sample-data [post-activity! url]
"Load sample data from discover.swiss and post to ActivityPub public collection"
(go
(let
[response (<! (http/get url {:with-credentials? false}))
tours (get-in response [:body])
as-activity (fn [object] {:type "Create"
:to ["https://www.w3.org/ns/activitystreams#Public"]
:object object})]
(doseq [tour tours]
(post-activity! (as-activity tour))))))
(defn tour-line [tour]
(let
[line-string (get-in tour [:geoShape :line])
geometry-coordinates (get-in tour [:geometry :coordinates])]
(cond
line-string
(map (fn [x] [(second x) (first x)])
(map #(str/split % #",")
(str/split line-string #" ")))
geometry-coordinates (map (fn [x] [(second x) (first x)])
geometry-coordinates)
:else nil)))
(defn tour? [object]
"Is the object a tour"
(or
(= "discover.swiss/Tour" (:type object))
(= "DownhillPiste" (:type object))
(= "AerialWayThing" (:type object))))
(defn tour-id [tour]
(get tour (keyword "@id")))
(defn tour-image [tour]
(when (get-in tour [:image :contentUrl])
[:img.tour-image {:src (get-in tour [:image :contentUrl])}]))
(defn tour-status [status-objects tour]
"Compute current status of tour from a list of activities (that may include Status targeting the tour)"
(let
[status-updates
(reverse
;; TODO: This is a hack as I don't seem capable of sorting by date
;; id is monotonically increasing, so this works...hackey
(sort-by :id
(filter (fn [status-object] (and (= (:type status-object) "Status")
(= (:target status-object) (tour-id tour))))
status-objects)))]
(first status-updates)))
(defn status-component [status-object timeline?]
(if status-object
[:span {:class ["tour-status" (:status status-object)]}
(if timeline?
(str (:target status-object)
" set to "
(:status status-object)
" by "
(:attributedTo status-object))
(str (:status status-object)
" (updated " (.fromNow (js/moment (:date status-object)))
" by "
(:attributedTo status-object)
")"))]
"-"))
(defn status-update-component [tour submit-status]
(let
[status-update-content (r/atom {:status "open"
:type "Status"
:date (.toJSON (js/moment))
:target (tour-id tour)})
create-activity (fn [object] {:type "Create"
:to "https://www.w3.org/ns/activitystreams#Public"
:object object})
submit #(do (submit-status (create-activity @status-update-content)))]
(fn [submit-status]
[:div.status-update
[:h3 "Status update"]
[:select
{:selected (:status @status-update-content)
:on-change (fn [event] (swap!
status-update-content
#(assoc % :status (-> event .-target .-value))))}
[:option {:value "open"} "open"]
[:option {:value "warning"} "warning"]
[:option {:value "closed"} "closed"]]
[:input {:type "button"
:value "Update"
:on-click submit}]])))
(defn tour-component [tour submit-status tour-status]
(when (tour? tour)
[:div.tour
[:h3 (get tour :type)]
[tour-image tour]
[:dl
[:dt "Name"]
[:dd (or (get tour :name) "-")]
[:dt "ID"]
[:dd (tour-id tour)]
[:dt "Description"]
[:dd (or (get tour :description) "-")]
(when tour-status
[:div
[:dt "Status"]
[:dd
[status-component tour-status]]])]
(when submit-status
[status-update-component tour submit-status])]))
(defn tours-component [objects is-selected? set-selected! set-hovered! submit-status tour-status]
[:div
(for [tour (filter #(tour? %) objects)]
[:div
[tour-component tour submit-status (tour-status tour)]
[:hr]])])
......@@ -16,7 +16,8 @@
;; along with GeoPub. If not, see <https://www.gnu.org/licenses/>.
(ns geopub.ns
(:require-macros [rdf.core :refer [defns]]))
(:require-macros [rdf.core :refer [defns]])
(:require [rdf.core]))
;; Commonly used namespaces
......
(ns geopub.state
"Helper for managing application state"
(:require-macros [cljs.core.async :refer [go]])
(:require [reagent.core :as r]
[rdf.graph.map]
[rdf.core :as rdf]
[cljs.core.async :as async :refer [<!]]))
(defn init []
(r/atom
{:graph (rdf.graph.map/graph)}))
(defn add-triples!
"Takes triples from a channel and adds them to the graph. The input channel must be closed before changes are applied."
[state chan]
(go
(let [input-graph (<! (async/reduce
;; read sequence of triples and add to graph
(fn [graph input]
(reduce rdf/graph-add graph (rdf/triple-seq input)))
;; initialize a fresh graph
(rdf.graph.map/graph)
chan))
;; merge graph in state with input-graph
new-graph (rdf/graph-merge (:graph @state) input-graph)]
;; swap in new graph
(swap! state (fn [state]
(assoc state :graph new-graph))))))
(defn reset-graph! [state]
(swap! state #(assoc % :graph (rdf.graph.map/graph))))
......@@ -15,38 +15,20 @@
;; You should have received a copy of the GNU General Public License
;; along with GeoPub. If not, see <https://www.gnu.org/licenses/>.
(ns geopub.ui.timeline
(ns geopub.ui.activity
(:require [geopub.ns :refer [as rdfs schema]]
[geopub.ui.utils :refer [iri-component literal-component]]
[geopub.data.activitypub :as activitypub]
[geopub.state]
[geopub.data.rdf :refer [iri-component
literal-component
description-component]]
[activitypub.core :as activitypub]
[rdf.core :as rdf]
[rdf.description :as rd]
[rdf.ns :as ns]
[rdf.graph.map]))
(defmulti object-component
(fn [object] (first (rd/description-get object (ns/rdf :type)))))
(defmethod object-component
(as :Note)
[object]
[:div.object
(for [content (rd/description-get object (as :content))]
[:p [literal-component content]])])
(defmethod object-component
(schema "Event")
[object]
[:div.object "I'm an event"])
(defmethod object-component
:default
[object]
[:div.object "I'm a object"])
(defn activity-component [activity]
[:div.activity
;; render object
(for
[object
......@@ -54,16 +36,28 @@
(rd/description-get activity (as :object)))]
^{:key (prn-str (rd/description-subject object))}
[object-component object])
[description-component object])
[:div.meta
[iri-component (rd/description-get activity (as :actor))]
[:br]
[iri-component (rd/description-get activity (ns/rdf :type))]]])
(defn sidebar []
[:div.sidebar
[:nav
[:ul
[:li [:a {:href "#"} "you"]]
[:li [:a {:href "#"} "people you follow"]]
[:li [:a {:href "#"} "group xyz"]]
[:li [:a {:href "#"} "all"]]
]]])
(defn view [state]
[:div#timeline
[:h1 "Timeline"]
(for [activity (activitypub/get-activities (:store @state))]
^{:key (prn-str (rd/description-subject activity))}
[activity-component activity])])
[:div.ui-page
[sidebar]
[:main
[:h1 "Activity"]
(for [activity (activitypub/get-activities (:graph @state))]
^{:key (prn-str (rd/description-subject activity))}
[activity-component activity])]])
(ns geopub.ui.browse
(:require [rdf.core :as rdf]
[rdf.description :as rdf-description]
[reagent.core :as r]
[goog.string]
[geopub.data.rdf :refer [description-header-component
description-component]]))
(defn sidebar []
[:div.sidebar
[:nav
[:h3 "Types"]
[:p "TODO Here you can select type of data to browse."]
[:ul
[:li [:a {:href "#"} "Notes"]]
[:li [:a {:href "#"} "Events"]]
[:li [:a {:href "#"} "Websites"]]
[:li [:a {:href "#"} "Actors"]]
[:li [:a {:href "#"} "Ontologies"]]]]])
(defn get-iri [state]
(-> @state
(get-in [:current-route :path-params :iri])
(goog.string.urlDecode)
(rdf/iri)))
(defn description-view [state]
(let [iri (get-iri state)
description (r/track #(rdf-description/description iri (:graph @state)))]
[:div.ui-page
[sidebar]
[:main [description-component @description]]]))
(defn type-view [state]
[:div.ui-page
[sidebar]
[:main "ylo"]])
......@@ -31,11 +31,11 @@
(defn get-geo-object [state]
"Returns a list of activities that have a latitude and longitude"
;; TODO only store the relevant subgraph in the description
(map #(rd/description % (:store @state))
(map #(rd/description % (:graph @state))
(run* [s]
(fresh [x y]
(rl/graph-tripleo (:store @state) (rdf/triple s (geo "long") x))
(rl/graph-tripleo (:store @state) (rdf/triple s (geo "lat") y))))))
(rl/graph-tripleo (:graph @state) (rdf/triple s (geo "long") x))
(rl/graph-tripleo (:graph @state) (rdf/triple s (geo "lat") y))))))
(defn get-location [object]
(let
......@@ -54,7 +54,7 @@
[Marker {:position (get-location object)}])
(defn view [state]
[:div#map
[:main#map
[Map
{:style {:min-height "100vh"}
:center default-center
......
(ns geopub.ui.store
(:require [rdf.core :as rdf]
[clojure.core.logic :as l]
[geopub.ui.utils :refer [rdf-term-component]]))
[geopub.data.rdf :refer [rdf-term-component]]))
(defn triple-table [triples]
[:table
......@@ -15,8 +15,8 @@
]])
(defn view [state]
[:div#store
[:h1 "Store"]
[triple-table (rdf/triple-seq
(:store @state))]
])
[:div.ui-page
[:main
[:h1 "Store"]
[triple-table (rdf/triple-seq
(:graph @state))]]])
(ns geopub.ui.utils
(:require [rdf.core :as rdf]))
(defn iri-component [iri & {:keys [class]}]
(let [class (or class "iri")]
(cond
(rdf/iri? iri)
[:span
{:class class}
(rdf/iri-value iri)]
(seq? iri)
(iri-component (first iri))
:else
[:span.iri "-"])))
(defn literal-component [literal]
(rdf/literal-value literal))
(defn blank-node-component [bnode]
(str "_:" (rdf/blank-node-id bnode)))
(defn rdf-term-component [term]
(cond
(rdf/iri? term) [iri-component term]
(rdf/literal? term) [literal-component term]
(rdf/blank-node? term) [blank-node-component term]
:else "-"))
(ns rdf.core
(:require [rdf.core :as rdf]))
(defmacro defns
"Macro for defining a custom new namespace"
[prefix namespace]
(let
[relative (gensym "relative-")]
`(defn ~prefix
[~relative]
(rdf/iri
(str ~namespace (name ~relative))))))
(ns rdf.core
"RDF in Clojure")
(declare iri)
(defmacro defns
"Macro for defining a custom new namespace"
[prefix namespace]
(let
[relative (gensym "relative-")]
`(defn ~prefix
[~relative]
(iri
(str ~namespace (name ~relative))))))
;; IRI
(defprotocol IIRI
......@@ -105,11 +93,25 @@
(defprotocol ITripleConvert
(-as-triple [x] "Converts value to Triple"))
(defprotocol ITripleSeq
"Protocol for converting anything to a sequence of triples"
(triple-seq [x]))
(extend-type default
ITripleSeq
(triple-seq [x]
(cond
(seqable? x) (seq x)
(satisfies? ITriple x) (list x))))
(defrecord Triple [subject predicate object]
ITriple
(triple-subject [x] (:subject x))
(triple-predicate [x] (:predicate x))
(triple-object [x] (:object x)))
(triple-object [x] (:object x))
ITripleSeq
(triple-seq [x] (list x)))
(defn triple? [t]
(instance? Triple t))
......@@ -160,7 +162,7 @@
;; TODO quads
;; Protocols
;; Graph Protocols
(defprotocol IGraph
"Protocol for accessing a graph"
......@@ -172,8 +174,6 @@
(defprotocol IGraphUpdate
"Protocol for updating graph"
(graph-add [x triple] "Add a triple to the graph.")
(graph-merge [x y] "Merge two graphs.")
(graph-delete [x triple] "Delete a triple from the graph."))
(defprotocol ITripleSeq
"Protocol for converting anything to a sequence of triples"
(triple-seq [x]))
......@@ -8,7 +8,12 @@
[rdf.logic :as rl]
[clojure.core.logic :as l]))
(defrecord Description [subject graph])
(defrecord Description [subject graph]
rdf/ITripleSeq
(triple-seq [description]
(run* [t]
(rl/graph-collecto (:graph description) 1 (:subject description) t))))
(defn description [subject graph]
(->Description subject graph))
......
......@@ -56,6 +56,11 @@
(add-to-index (:spo graph) s p o)
(add-to-index (:ops graph) o p s))))
(rdf/graph-merge [graph-x graph-y]
(->Graph
(index-merge (:spo graph-x) (:spo graph-y))
(index-merge (:ops graph-x) (:ops graph-y))))
(rdf/graph-delete [graph triple]
(let [s (rdf/triple-subject triple)
p (rdf/triple-predicate triple)
......
(ns rdf.js.data-model
"Bindings to the RDF/JS Data model specification (http://rdf.js.org/data-model-spec/).
Note that the bindings use the explicit class defined in the @rdfjs/data-model library instead of checking the `termType` attribute of values. This means that this binding will only work with libraries that use the @rdfjs/data-model library (i.e. not N3.js)."
(:require [rdf.core :as rdf]
["@rdfjs/data-model" :as rdf-js]))
;; Note: I understand way too little about the JavaScript prototype thing. But this works...somehow (?!?)
;; NamedNode (IRI)
(def named-node-type (type (.namedNode rdf-js)))
(extend-type named-node-type
rdf/IIRIConvert
(-as-iri [x] (rdf/iri (.-value x))))
;; BlankNode
(def blank-node-type (type (.blankNode rdf-js)))
(extend-type blank-node-type
rdf/IBlankNodeConvert
(-as-blank-node [x] (rdf/blank-node (.-value x))))
;; Literal
(def literal-type (type (.literal rdf-js)))
(extend-type literal-type
rdf/ILiteralConvert
(rdf/-as-literal [x]
(rdf/literal (.-value x)
:language (if (not (clojure.string/blank? (.-language x)))
(.-language x))
:datatype (if (.-datatype x)
(rdf/iri (.-datatype x))))))
;; Quad (Triple)
(def quad-type (type (.quad rdf-js)))
(extend-type quad-type
rdf/ITripleConvert
(rdf/-as-triple [x]
(rdf/triple (.-subject x)
(.-predicate x)
(.-object x))))
......@@ -166,7 +166,7 @@
l/fail))
(defn collecto
(defn graph-collecto
"Collect triples starting at a given subject."
[graph n from t]
(if (> n 0)
......@@ -182,6 +182,6 @@
(triple-subjecto t2 from)
(triple-objecto t2 o)
(graph-tripleo graph t2)
(collecto graph (dec n) o t))])
(graph-collecto graph (dec n) o t))])
l/fail))
(ns rdf.n3
"Bindings to the N3.js library for parsing Turtle"
(:require [rdf.core :as rdf]
["./n3.js" :as n3]
["n3" :as n3]
[cljs.core.async :as async :refer [<! >!]])
(:require-macros [cljs.core.async.macros :refer [go]]))
......@@ -110,12 +110,21 @@
(implements? ILiteralConvert o) (-as-literal o))))))
;; Parsing
;;
(defn parse
"Parse RDF/Turtle to a sequence of Triples. Returns a transducer if no input is given."
([]
(fn [rf]
(fn
([] (rf))
([result] (rf result))
([result input] (rf result (parse input))))))
([input]
(map rdf/triple
;; convert to Clojure list before mapping
(js->clj (.parse (new n3/Parser) input)))))
(defn parse [input]
"Parse RDF/Turtle to a sequence of Triples"
(map rdf/triple
;; convert to Clojure list before mapping
(js->clj (.parse (new n3/Parser) input))))
;; Encoding
......
This diff is collapsed.
(ns rdf.parse
"Bindings to the rdf-parse.js library for parsing RDF from B"
(:require-macros [cljs.core.async :refer [go go-loop]])
(:require [rdf.core :as rdf]
[cljs.core.async :as async :refer [<! >!]]
["rdf-parse" :as rdf-parse]
["stream" :as stream]
[rdf.js.data-model]))
;; The rdf-parse.js parser
(def parser (.-default rdf-parse))
;; Helpers to get data between streams and core.async
(defn put-stream!
"Reads values from a readable stream and puts them on a channel. When the stream ends, the channel is closed."
[ch stream]
(.on stream "data" (fn [data]
(.pause stream)
(async/put! ch data #(.resume stream))))
(.on stream "end" #(async/close! ch))
ch)
(defn pipe-stream
"Takes elements from the from channel and supplies them to the writable stream. When the channel is closed, the stream is ended. "
[from to-stream]
(go-loop []
(let [data (<! from)]
(if (some? data)
(do (.write to-stream data)
(recur))
(.end to-stream data))))
stream)
;; Parser
(defn content-types
"Returns all content-types that can be parsed."
[]
;; This is copied from the output of parser.contentTypes(). See also https://github.com/rubensworks/rdf-parse.js#getting-all-known-content-types.
'("application/n-quads"
"application/trig"
"application/n-triples"
"text/turtle"
"text/n3"
"application/rdf+xml"
"application/xml"
"text/xml"
"image/svg+xml"
"text/html"
"application/xhtml+xml"
"application/ld+json"
"application/json"))
(defn parse
"Takes data from the input-channel and parses them to Triples. Returns a channel that contains triples. input-channel needs to be closed before all triples are emitted."
[input-channel & {:keys [content-type base-iri]}]