From a7bdb02dbf99a5a18be15e56962a4f5190b724a7 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 6 May 2020 07:29:26 +0200 Subject: [PATCH] :sparkles: Adds range tree data structure --- frontend/src/uxbox/util/geom/snap.cljs | 10 +- frontend/src/uxbox/util/range_tree.js | 192 ++++++++++++++++++ .../tests/uxbox/test_util_range_tree.cljs | 140 +++++++++++++ 3 files changed, 337 insertions(+), 5 deletions(-) create mode 100644 frontend/src/uxbox/util/range_tree.js create mode 100644 frontend/tests/uxbox/test_util_range_tree.cljs diff --git a/frontend/src/uxbox/util/geom/snap.cljs b/frontend/src/uxbox/util/geom/snap.cljs index 2a2a0152c..b61e2b056 100644 --- a/frontend/src/uxbox/util/geom/snap.cljs +++ b/frontend/src/uxbox/util/geom/snap.cljs @@ -16,7 +16,7 @@ [uxbox.util.geom.shapes :as gsh] [uxbox.util.geom.point :as gpt])) -(def ^:private snap-accuracy 20) +(def ^:private snap-accuracy 10) (defn mapm "Map over the values of a map" @@ -111,7 +111,7 @@ (filter (fn [[_ data]] (not (empty? data)))))) (defn search-snap-point - "Search snap for a single point" + "Search snap for a single point in the `coord` given" [point coord snap-data filter-shapes] (let [coord-value (get point coord) @@ -134,7 +134,7 @@ (let [snap-points (mapcat #(search-snap-point % coord snap-data filter-shapes) points) result (->> snap-points (apply min-key first) second)] - (or result [0 0]))) + result)) (defn snap-frame-id [shapes] (let [frames (into #{} (map :frame-id shapes))] @@ -162,8 +162,8 @@ [snap-from-x snap-to-x] (search-snap shapes-points :x (get-in snap-data [frame-id :x]) remove-shapes) [snap-from-y snap-to-y] (search-snap shapes-points :y (get-in snap-data [frame-id :y]) remove-shapes) - snapv (gpt/to-vec (gpt/point snap-from-x snap-from-y) - (gpt/point snap-to-x snap-to-y))] + snapv (gpt/to-vec (gpt/point (or snap-from-x 0) (or snap-from-y 0)) + (gpt/point (or snap-to-x 0) (or snap-to-y 0)))] (gpt/add trans-vec snapv))) diff --git a/frontend/src/uxbox/util/range_tree.js b/frontend/src/uxbox/util/range_tree.js new file mode 100644 index 000000000..0b4f6d31b --- /dev/null +++ b/frontend/src/uxbox/util/range_tree.js @@ -0,0 +1,192 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This Source Code Form is "Incompatible With Secondary Licenses", as + * defined by the Mozilla Public License, v. 2.0. + * + * Copyright (c) 2020 UXBOX Labs SL + */ + +"use strict"; + +goog.provide("uxbox.util.range_tree"); +goog.require("cljs.core") + +goog.scope(function() { + const eq = cljs.core._EQ_; + const vec = cljs.core.vec; + const nil = cljs.core.nil; + + class Node { + constructor(value, data) { + this.value = value; + this.data = [ data ]; + this.left = null; + this.right = null; + } + } + + // Will store a map from key to list of data + // value => [ data ] + // The values can be queried in range and the data stored will be retrived whole + // but can be removed/updated individually using clojurescript equality + class RangeTree { + constructor() { + this.root = null; + } + + insert(value, data) { + this.root = recInsert(this.root, value, data); + return this; + } + + remove(value, data) { + this.root = recRemove(this.root, value, data); + return this; + } + + update (value, oldData, newData) { + this.root = recUpdate(this.root, value, oldData, newData); + return this; + } + + get(value) { + return recGet(this.root, value); + } + + rangeQuery (fromValue, toValue) { + return recRangeQuery(this.root, fromValue, toValue, []); + } + } + + // Tree implementation functions + + // Insert recursively in the tree + function recInsert (branch, value, data) { + if (branch === null) { + return new Node(value, data); + } else if (branch.value === value) { + // Find node we'll add to the end of the list + branch.data.push(data); + } else if (branch.value > value) { + // Target value is less than the current value we go left + branch.left = recInsert(branch.left, value, data); + } else if (branch.value < value) { + branch.right = recInsert(branch.right, value, data); + } + return branch; + } + + // Search for the min node + function searchMin(branch) { + if (branch.left === null) { + return branch; + } else { + return searchMin(branch.left); + } + } + + // Remove the lefmost node of the current branch + function recRemoveMin(branch) { + if (branch.left === null) { + return branch.right; + } else { + branch.left = recRemoveMin(branch.left); + return branch; + } + } + + // Remove the data element for the value given + function recRemove(branch, value, data) { + if (branch === null) { + // Not found + return branch; + } else if (branch.value === value) { + // Node found, we remove the data + branch.data = branch.data.filter ((it) => !eq(it, data)); + + if (branch.data.length > 0) { + return branch; + } + + // If the data is empty we need to remove the branch + if (branch.right === null) { + return branch.left; + } else if (branch.left === null) { + return branch.right; + } else { + const oldBranch = branch; + const newBranch = searchMin(branch.right); + newBranch.right = recRemoveMin(oldBranch.right); + newBranch.left = oldBranch.left; + return newBranch; + } + } else if (branch.value > value) { + // Target value is less than the current value we go left + branch.left = recRemove (branch.left, value, data); + return branch; + } else if (branch.value < value) { + branch.right = recRemove (branch.right, value, data); + return branch; + } + } + + // Retrieve all the data related to value + function recGet(branch, value) { + if (branch === null) { + return null; + } else if (branch.value === value) { + return branch.data; + } else if (branch.value > value) { + return recGet(branch.left, value); + } else if (branch.value < value) { + return recGet(branch.right, value); + } + } + + function recUpdate(branch, value, oldData, newData) { + if (branch === null) { + return branch; + } else if (branch.value === value) { + branch.data = branch.data.map((it) => (eq(it, oldData)) ? newData : it); + return branch; + } else if (branch.value > value) { + return recUpdate(branch.left, value, oldData, newData); + } else if (branch.value < value) { + return recUpdate(branch.right, value, oldData, newData); + } + } + + function recRangeQuery(branch, fromValue, toValue, result) { + if (branch === null) { + return result; + } + if (fromValue < branch.value) { + recRangeQuery(branch.left, fromValue, toValue, result); + } + if (fromValue <= branch.value && toValue >= branch.value) { + Array.prototype.push.apply(result, branch.data); + } + if (toValue > branch.value) { + recRangeQuery(branch.right, fromValue, toValue, result); + } + return result; + } + + // External API to CLJS + const self = uxbox.util.range_tree; + self.make_tree = () => new RangeTree(); + self.insert = (tree, value, data) => tree.insert(value, data); + self.remove = (tree, value, data) => tree.remove(value, data); + self.update = (tree, value, oldData, newData) => tree.update(value, oldData, newData); + self.get = (tree, value) => { + const result = tree.get(value); + if (!result) { + return nil; + } + return vec(result); + }; + self.range_query = (tree, from_value, to_value) => vec(tree.rangeQuery(from_value, to_value)); +}); diff --git a/frontend/tests/uxbox/test_util_range_tree.cljs b/frontend/tests/uxbox/test_util_range_tree.cljs new file mode 100644 index 000000000..2031c2af1 --- /dev/null +++ b/frontend/tests/uxbox/test_util_range_tree.cljs @@ -0,0 +1,140 @@ +(ns uxbox.test-util-range-tree + (:require [cljs.test :as t :include-macros true] + [cljs.pprint :refer [pprint]] + [uxbox.util.geom.point :as gpt] + [uxbox.util.range-tree :as rt])) + +(defn check-max-height [tree num-nodes]) +(defn check-sorted [tree]) + +(defn create-random-tree [num-nodes]) + +(t/deftest test-insert-and-retrive-data + (t/testing "Retrieve on empty tree" + (let [tree (rt/make-tree)] + (t/is (= (rt/get tree 100) nil)))) + + (t/testing "First insert/retrieval" + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a))] + (t/is (= (rt/get tree 100) [:a])) + (t/is (= (rt/get tree 200) nil)))) + + (t/testing "Insert best case scenario" + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a) + (rt/insert 50 :b) + (rt/insert 200 :c))] + (t/is (= (rt/get tree 100) [:a])) + (t/is (= (rt/get tree 50) [:b])) + (t/is (= (rt/get tree 200) [:c])))) + + (t/testing "Insert duplicate entry" + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a) + (rt/insert 50 :b) + (rt/insert 200 :c) + (rt/insert 50 :d) + (rt/insert 200 :e))] + (t/is (= (rt/get tree 100) [:a])) + (t/is (= (rt/get tree 50) [:b :d])) + (t/is (= (rt/get tree 200) [:c :e]))))) + +(t/deftest test-remove-elements + (t/testing "Insert and delete data but not the node" + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a) + (rt/insert 100 :b) + (rt/insert 100 :c) + (rt/remove 100 :b))] + (t/is (= (rt/get tree 100) [:a :c])))) + + (t/testing "Try to delete data not in the node is noop" + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a) + (rt/insert 100 :b) + (rt/insert 100 :c) + (rt/remove 100 :xx))] + (t/is (= (rt/get tree 100) [:a :b :c])))) + + (t/testing "Delete data and node" + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a) + (rt/insert 200 :b) + (rt/insert 300 :c) + (rt/remove 200 :b))] + (t/is (= (rt/get tree 200) nil)))) + + (t/testing "Delete root node the new tree should be correct" + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a) + (rt/insert 50 :b) + (rt/insert 150 :c) + (rt/insert 25 :d) + (rt/insert 75 :e) + (rt/insert 125 :f) + (rt/insert 175 :g) + (rt/remove 100 :a))] + + (t/is (= (rt/get tree 100) nil)) + (t/is (= (rt/get tree 50) [:b])) + (t/is (= (rt/get tree 150) [:c])) + (t/is (= (rt/get tree 25) [:d])) + (t/is (= (rt/get tree 75) [:e])) + (t/is (= (rt/get tree 125) [:f])) + (t/is (= (rt/get tree 175) [:g]))))) + +(t/deftest test-update-elements + (t/testing "Updates an element" + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a) + (rt/insert 50 :b) + (rt/insert 150 :c) + (rt/insert 50 :d) + (rt/insert 50 :e) + (rt/update 50 :d :xx))] + (t/is (= (rt/get tree 50) [:b :xx :e])))) + + (t/testing "Try to update non-existing element" + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a) + (rt/insert 50 :b) + (rt/insert 150 :c) + (rt/insert 50 :d) + (rt/insert 50 :e) + (rt/update 50 :zz :xx))] + (t/is (= (rt/get tree 50) [:b :d :e]))))) + +(t/deftest test-range-query + (t/testing "Creates a tree and test different range queries" + (let [tree (-> (rt/make-tree) + (rt/insert 0 :a) + (rt/insert 25 :b) + (rt/insert 50 :c) + (rt/insert 75 :d) + (rt/insert 100 :e) + (rt/insert 100 :f) + (rt/insert 125 :g) + (rt/insert 150 :h) + (rt/insert 175 :i) + (rt/insert 200 :j) + (rt/insert 200 :k))] + (t/is (= (rt/range-query tree 0 200) [:a :b :c :d :e :f :g :h :i :j :k])) + (t/is (= (rt/range-query tree 0 100) [:a :b :c :d :e :f])) + (t/is (= (rt/range-query tree 100 200) [:e :f :g :h :i :j :k])) + (t/is (= (rt/range-query tree 10 60) [:b :c])) + (t/is (= (rt/range-query tree 199.5 200.5) [:j :k])))) + + (t/testing "Empty range query" + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a) + (rt/insert 50 :b) + (rt/insert 150 :c) + (rt/insert 25 :d) + (rt/insert 75 :e) + (rt/insert 125 :f) + (rt/insert 175 :g))] + (t/is (= (rt/range-query tree -100 0) [])) + (t/is (= (rt/range-query tree 200 300) [])) + (t/is (= (rt/range-query tree 200 0) []))))) +