Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Socket
Sign inDemoInstall

labelgun

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

labelgun - npm Package Compare versions

Comparing version 5.0.1 to 6.0.0

3

docs/examples/leaflet/example.js

@@ -11,3 +11,3 @@ /* eslint-disable */

var map = L.map("map").setView([0, 0], 6);
L.tileLayer("//tile.osm{s}..org/{z}/{x}/{y}.png", {
L.tileLayer("//tile.osm.org/{z}/{x}/{y}.png", {
attribution: "&copy; <a href=\"https://osm.org/copyright\">OpenStreetMap</a> contributors"

@@ -60,3 +60,2 @@ }).addTo(map);

labelEngine.destroy();
var i = 0;

@@ -63,0 +62,0 @@ markers.eachLayer(function(label){

@@ -71,3 +71,3 @@

if (Object.keys(labelEngine.allLabels).length > 0) {
labelEngine.destroy();
labelEngine.reset();
}

@@ -74,0 +74,0 @@ labels.forEach(function(label, i) {

@@ -258,3 +258,3 @@ (function webpackUniversalModuleDefinition(root, factory) {

/**
* @name destroy
* @name reset
* @memberof labelgun

@@ -267,10 +267,12 @@ * @method

}, {
key: "destroy",
value: function destroy() {
key: "reset",
value: function reset() {
this.tree.clear();
this.allLabels = {};
this.hasChanged = [];
this.allChanged = false;
}
/**
* @name callLabelCallbacks
* @name _callLabelCallbacks
* @memberof labelgun

@@ -281,8 +283,8 @@ * @method

* @returns {undefined}
* @public
* @private
*/
}, {
key: "callLabelCallbacks",
value: function callLabelCallbacks(forceState) {
key: "_callLabelCallbacks",
value: function _callLabelCallbacks(forceState) {
var _this = this;

@@ -313,3 +315,3 @@

/**
* @name compareLabels
* @name _compareLabels
* @memberof labelgun

@@ -319,10 +321,15 @@ * @method

* @returns {undefined}
* @private
*/
}, {
key: "compareLabels",
value: function compareLabels() {
key: "_compareLabels",
value: function _compareLabels() {
var _this2 = this;
this.orderedLabels = Object.values(this.allLabels).sort(this._compare);
// Map all the labels to an array and sort based on weight
// highest to lowest
this.orderedLabels = Object.keys(this.allLabels).map(function (v) {
return _this2.allLabels[v];
}).sort(this._compare);

@@ -370,3 +377,3 @@ this.orderedLabels.forEach(function (label) {

* @param {object} b - Second object to compare
* @summary Sets up the labels depending on whether all have changed or some have changed
* @summary Compares labels weights for sorting
* @returns {number} - The sort value

@@ -386,3 +393,3 @@ * @private

/**
* @name setupLabelStates
* @name _setupLabels
* @memberof labelgun

@@ -392,7 +399,8 @@ * @method

* @returns {undefined}
* @private
*/
}, {
key: "setupLabelStates",
value: function setupLabelStates() {
key: "_setupLabels",
value: function _setupLabels() {
var _this3 = this;

@@ -444,3 +452,4 @@

* @method
* @summary Sets all labels to change and reruns the whole show/hide procedure
* @param {boolean} onlyChanges - Whether to only update the changes made by labelHasChanged
* @summary Sets all or some of the labels to change and reruns the whole show/hide procedure
* @returns {undefined}

@@ -451,30 +460,30 @@ */

key: "update",
value: function update() {
value: function update(onlyChanges) {
this.allChanged = true;
this.setupLabelStates();
this.compareLabels();
this.callLabelCallbacks();
if (onlyChanges) {
this.allChanged = false;
} else {
this.allChanged = true;
}
this._setupLabels();
this._compareLabels();
this._callLabelCallbacks();
}
/**
* @name _removeFromTree
* @name removeLabel
* @memberof labelgun
* @method
* @param {object} label - The label to remove from the tree
* @param {boolean} forceUpdate if true, triggers all labels to be updated
* @summary Removes label from tree
* @param {object} id - The label id for the label to remove from the tree
* @summary Removes label from tree and allLabels object
* @returns {undefined}
* @private
*/
}, {
key: "_removeFromTree",
value: function _removeFromTree(label, forceUpdate) {
var id = label.id;
key: "removeLabel",
value: function removeLabel(id) {
var removelLabel = this.allLabels[id];
this.tree.remove(removelLabel);
delete this.allLabels[id];
if (forceUpdate) this.callLabelCallbacks(true);
}

@@ -533,3 +542,3 @@

var oldLabel = this.allLabels[id];
if (oldLabel) this._removeFromTree(oldLabel);
if (oldLabel) this.removeLabel(oldLabel.id);

@@ -536,0 +545,0 @@ var label = {

@@ -1,1 +0,1 @@

!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("rbush")):"function"==typeof define&&define.amd?define(["rbush"],t):"object"==typeof exports?exports.labelgun=t(require("rbush")):e.labelgun=t(e.rbush)}(this,function(e){return function(e){function t(l){if(a[l])return a[l].exports;var r=a[l]={i:l,l:!1,exports:{}};return e[l].call(r.exports,r,r.exports,t),r.l=!0,r.exports}var a={};return t.m=e,t.c=a,t.d=function(e,a,l){t.o(e,a)||Object.defineProperty(e,a,{configurable:!1,enumerable:!0,get:l})},t.n=function(e){var a=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(a,"a",a),a},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=0)}([function(e,t,a){"use strict";function l(e){if(Array.isArray(e)){for(var t=0,a=Array(e.length);t<e.length;t++)a[t]=e[t];return a}return Array.from(e)}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var i=function(){function e(e,t){for(var a=0;a<t.length;a++){var l=t[a];l.enumerable=l.enumerable||!1,l.configurable=!0,"value"in l&&(l.writable=!0),Object.defineProperty(e,l.key,l)}}return function(t,a,l){return a&&e(t.prototype,a),l&&e(t,l),t}}(),n=a(1),s=function(e){return e&&e.__esModule?e:{default:e}}(n),o=function(){function e(t,a,l){r(this,e);var i=l||10;this.tree=(0,s.default)(i),this.allLabels={},this.hasChanged=new Set,this.allChanged=!1,this.hideLabel=t,this.showLabel=a}return i(e,[{key:"_total",value:function(e){for(var t=0,a=0,l=Object.keys(this.allLabels);a<l.length;a++)this.allLabels[l[a]].state==e&&(t+=1);return t}},{key:"totalShown",value:function(){return this._total("show")}},{key:"totalHidden",value:function(){return this._total("hide")}},{key:"_getLabelsByState",value:function(e){for(var t=[],a=0,l=Object.keys(this.allLabels);a<l.length;a++)this.allLabels[l[a]].state==e&&t.push(this.allLabels[l[a]]);return t}},{key:"getHidden",value:function(){return this._getLabelsByState("hide")}},{key:"getShown",value:function(){return this._getLabelsByState("show")}},{key:"getCollisions",value:function(e){var t=this.allLabels[e];if(void 0===t)throw Error("Label doesn't exist :"+JSON.stringify(e));var a=this.tree.search(t),l=a.indexOf(t);return void 0!==l&&a.splice(l,1),a}},{key:"getLabel",value:function(e){return this.allLabels[e]}},{key:"destroy",value:function(){this.tree.clear(),this.allLabels={}}},{key:"callLabelCallbacks",value:function(e){var t=this;Object.keys(this.allLabels).forEach(function(a){t._callLabelStateCallback(t.allLabels[a],e)})}},{key:"_callLabelStateCallback",value:function(e,t){var a=t||e.state;"show"===a&&this.showLabel(e),"hide"===a&&this.hideLabel(e)}},{key:"compareLabels",value:function(){var e=this;this.orderedLabels=Object.values(this.allLabels).sort(this._compare),this.orderedLabels.forEach(function(t){var a=e.tree.search(t);(0===a.length||e._allLower(a,t)||t.isDragged)&&(e.allLabels[t.id].state="show")})}},{key:"_allLower",value:function(e,t){for(var a=void 0,l=0;l<e.length;l++)if(a=e[l],"show"===a.state||a.weight>t.weight||a.isDragged)return!1;return!0}},{key:"_compare",value:function(e,t){return e.weight>t.weight?-1:e.weight<t.weight?1:0}},{key:"setupLabelStates",value:function(){var e=this;if(this.allChanged)this.allChanged=!1,this.hasChanged.clear(),this.tree.clear(),Object.keys(this.allLabels).forEach(function(t){var a=e.allLabels[t];e.ingestLabel({bottomLeft:[a.minX,a.minY],topRight:[a.maxX,a.maxY]},a.id,a.weight,a.labelObject,a.name,a.isDragged)});else if(this.hasChanged.size){var t=[].concat(l(this.hasChanged));this.hasChanged.clear(),t.forEach(function(t){var a=e.allLabels[t];a&&e.ingestLabel({bottomLeft:[a.minX,a.minY],topRight:[a.maxX,a.maxY]},a.id,a.weight,a.labelObject,a.name,a.isDragged)})}}},{key:"update",value:function(){this.allChanged=!0,this.setupLabelStates(),this.compareLabels(),this.callLabelCallbacks()}},{key:"_removeFromTree",value:function(e,t){var a=e.id,l=this.allLabels[a];this.tree.remove(l),delete this.allLabels[a],t&&this.callLabelCallbacks(!0)}},{key:"_addToTree",value:function(e){this.allLabels[e.id]=e,this.tree.insert(e)}},{key:"ingestLabel",value:function(e,t,a,l,r,i){if(void 0!==a&&null!==a||(a=0),!e||!e.bottomLeft||!e.topRight)throw Error("Bounding box must be defined with bottomLeft and topRight properties");if("string"!=typeof t&&"number"!=typeof t)throw Error("Label IDs must be a string or a number");var n=this.allLabels[t];n&&this._removeFromTree(n);var s={minX:e.bottomLeft[0],minY:e.bottomLeft[1],maxX:e.topRight[0],maxY:e.topRight[1],state:"hide",id:t,weight:a,labelObject:l,name:r,isDragged:i};this._addToTree(s)}},{key:"labelHasChanged",value:function(e){this.hasChanged.add(e)}}]),e}();t.default=o},function(t,a){t.exports=e}])});
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("rbush")):"function"==typeof define&&define.amd?define(["rbush"],t):"object"==typeof exports?exports.labelgun=t(require("rbush")):e.labelgun=t(e.rbush)}(this,function(e){return function(e){function t(l){if(a[l])return a[l].exports;var n=a[l]={i:l,l:!1,exports:{}};return e[l].call(n.exports,n,n.exports,t),n.l=!0,n.exports}var a={};return t.m=e,t.c=a,t.d=function(e,a,l){t.o(e,a)||Object.defineProperty(e,a,{configurable:!1,enumerable:!0,get:l})},t.n=function(e){var a=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(a,"a",a),a},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=0)}([function(e,t,a){"use strict";function l(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var n=function(){function e(e,t){for(var a=0;a<t.length;a++){var l=t[a];l.enumerable=l.enumerable||!1,l.configurable=!0,"value"in l&&(l.writable=!0),Object.defineProperty(e,l.key,l)}}return function(t,a,l){return a&&e(t.prototype,a),l&&e(t,l),t}}(),i=a(1),s=function(e){return e&&e.__esModule?e:{default:e}}(i),r=function(){function e(t,a,n){l(this,e);var i=n||10;this.tree=(0,s.default)(i),this.allLabels={},this.hasChanged=[],this.allChanged=!1,this.hideLabel=t,this.showLabel=a}return n(e,[{key:"_total",value:function(e){for(var t=0,a=0,l=Object.keys(this.allLabels);a<l.length;a++)this.allLabels[l[a]].state==e&&(t+=1);return t}},{key:"totalShown",value:function(){return this._total("show")}},{key:"totalHidden",value:function(){return this._total("hide")}},{key:"_getLabelsByState",value:function(e){for(var t=[],a=0,l=Object.keys(this.allLabels);a<l.length;a++)this.allLabels[l[a]].state==e&&t.push(this.allLabels[l[a]]);return t}},{key:"getHidden",value:function(){return this._getLabelsByState("hide")}},{key:"getShown",value:function(){return this._getLabelsByState("show")}},{key:"getCollisions",value:function(e){var t=this.allLabels[e];if(void 0===t)throw Error("Label doesn't exist :"+JSON.stringify(e));var a=this.tree.search(t),l=a.indexOf(t);return void 0!==l&&a.splice(l,1),a}},{key:"getLabel",value:function(e){return this.allLabels[e]}},{key:"reset",value:function(){this.tree.clear(),this.allLabels={},this.hasChanged=[],this.allChanged=!1}},{key:"_callLabelCallbacks",value:function(e){var t=this;Object.keys(this.allLabels).forEach(function(a){t._callLabelStateCallback(t.allLabels[a],e)})}},{key:"_callLabelStateCallback",value:function(e,t){var a=t||e.state;"show"===a&&this.showLabel(e),"hide"===a&&this.hideLabel(e)}},{key:"_compareLabels",value:function(){var e=this;this.orderedLabels=Object.keys(this.allLabels).map(function(t){return e.allLabels[t]}).sort(this._compare),this.orderedLabels.forEach(function(t){var a=e.tree.search(t);(0===a.length||e._allLower(a,t)||t.isDragged)&&(e.allLabels[t.id].state="show")})}},{key:"_allLower",value:function(e,t){for(var a=void 0,l=0;l<e.length;l++)if(a=e[l],"show"===a.state||a.weight>t.weight||a.isDragged)return!1;return!0}},{key:"_compare",value:function(e,t){return e.weight>t.weight?-1:e.weight<t.weight?1:0}},{key:"_setupLabels",value:function(){var e=this;this.allChanged?(this.allChanged=!1,this.hasChanged=[],this.tree.clear(),Object.keys(this.allLabels).forEach(function(t){e._handleLabelIngestion(t)})):this.hasChanged.length>0&&(this.hasChanged.forEach(function(t){e._handleLabelIngestion(t)}),this.hasChanged=[])}},{key:"_handleLabelIngestion",value:function(e){var t=this.allLabels[e];this.ingestLabel({bottomLeft:[t.minX,t.minY],topRight:[t.maxX,t.maxY]},t.id,t.weight,t.labelObject,t.name,t.isDragged)}},{key:"update",value:function(e){this.allChanged=!e,this._setupLabels(),this._compareLabels(),this._callLabelCallbacks()}},{key:"removeLabel",value:function(e){var t=this.allLabels[e];this.tree.remove(t),delete this.allLabels[e]}},{key:"_addToTree",value:function(e){this.allLabels[e.id]=e,this.tree.insert(e)}},{key:"ingestLabel",value:function(e,t,a,l,n,i){if(void 0!==a&&null!==a||(a=0),!e||!e.bottomLeft||!e.topRight)throw Error("Bounding box must be defined with bottomLeft and topRight properties");if("string"!=typeof t&&"number"!=typeof t)throw Error("Label IDs must be a string or a number");var s=this.allLabels[t];s&&this.removeLabel(s.id);var r={minX:e.bottomLeft[0],minY:e.bottomLeft[1],maxX:e.topRight[0],maxY:e.topRight[1],state:"hide",id:t,weight:a,labelObject:l,name:n,isDragged:i};this._addToTree(r)}},{key:"labelHasChanged",value:function(e){-1===this.hasChanged.indexOf(e)&&this.hasChanged.push(e)}}]),e}();t.default=r},function(t,a){t.exports=e}])});
{
"name": "labelgun",
"version": "5.0.1",
"version": "6.0.0",
"description": "A mapping library agnostic labelling engine",

@@ -5,0 +5,0 @@ "main": "lib/labelgun.js",

@@ -29,7 +29,7 @@ ![labelgun](logo.png)

`https://unpkg.com/labelgun@5.0.0/lib/labelgun.js`
`https://unpkg.com/labelgun@5.0.1/lib/labelgun.js`
### Docs and Demos
Check out the [docs and demos live here](http://tech.geovation.uk/labelgun/)
Check out the [docs and demos live here](http://geovation.github.io/labelgun/)

@@ -87,2 +87,3 @@ A nice interactive way to play with the demos locally is to use a hot reloading web server such as live-server:

- [OL Mapbox Style](https://github.com/boundlessgeo/ol-mapbox-style) - Use Mapbox Style objects with OpenLayers
- [qgis2web](https://github.com/tomchadwin/qgis2web) - A QGIS plugin to export a map to an OpenLayers/Leaflet webmap

@@ -89,0 +90,0 @@ Using Labelgun? Open a pull request and let us know!

@@ -48,3 +48,3 @@ var labelgun = require("../lib/labelgun");

0, //id
parseInt(Math.random() * (5 - 1) + 1), // Weight
1, // Weight
{}, // label object

@@ -60,2 +60,111 @@ "Test",

it("should fail if there is no bounding box", function(){
var hideLabel = function(){ return false; };
var showLabel = function(){ return true; };
var labelEngine = new labelgun.default(hideLabel, showLabel);
var boundingBox = null;
expect(function(){
labelEngine.ingestLabel(
boundingBox,
0, //id
1, // Weight
{}, // label object
"Test",
false
);
}).toThrow(
new Error("Bounding box must be defined with bottomLeft and topRight properties")
);
expect(labelEngine.tree.all().length).toBe(0);
expect(Object.keys(labelEngine.allLabels).length).toBe(0);
});
it("should fail if there is a bounding box but it is incorrectly defined", function(){
var hideLabel = function(){ return false; };
var showLabel = function(){ return true; };
var labelEngine = new labelgun.default(hideLabel, showLabel);
var boundingBox = {
bottomLeft : null,
topRight : null
};
expect(function(){
labelEngine.ingestLabel(
boundingBox,
0, //id
1, // Weight
{}, // label object
"Test",
false
);
}).toThrow(
new Error("Bounding box must be defined with bottomLeft and topRight properties")
);
expect(labelEngine.tree.all().length).toBe(0);
expect(Object.keys(labelEngine.allLabels).length).toBe(0);
});
it("should remove a single label", function(){
var hideLabel = function(){ return false; };
var showLabel = function(){ return true; };
var labelEngine = new labelgun.default(hideLabel, showLabel);
var boundingBox = {
bottomLeft : [0.0, 0.0],
topRight : [1.0, 1.0]
};
labelEngine.ingestLabel(
boundingBox,
0, //id
1, // Weight
{}, // label object
"Test",
false
);
labelEngine.removeLabel(0);
expect(labelEngine.tree.all().length).toBe(0);
expect(Object.keys(labelEngine.allLabels).length).toBe(0);
});
it("should allow for the getting of a single label", function(){
var hideLabel = function(){ return false; };
var showLabel = function(){ return true; };
var labelEngine = new labelgun.default(hideLabel, showLabel);
var boundingBox = {
bottomLeft : [0.0, 0.0],
topRight : [1.0, 1.0]
};
labelEngine.ingestLabel(
boundingBox,
0, //id
1, // Weight
{}, // label object
"Test",
false
);
expect(labelEngine.tree.all().length).toBe(1);
expect(Object.keys(labelEngine.allLabels).length).toBe(1);
expect(typeof labelEngine.getLabel(0)).toBe("object");
expect(labelEngine.getLabel(0).id).toBe(0);
});
it("should ingest many labels (10)", function(){

@@ -184,3 +293,3 @@

it("should destroy labelgun data", function(){
it("should reset labelgun and its internal data (tree and label holding object)", function(){

@@ -212,3 +321,3 @@ var hideLabel = function(){ return false; };

expect(Object.keys(labelEngine.allLabels).length).toBe(10);
labelEngine.destroy();
labelEngine.reset();
expect(Object.keys(labelEngine.allLabels).length).toBe(0);

@@ -543,4 +652,3 @@ expect(labelEngine.tree.all().length).toBe(0);

labelEngine.setupLabelStates();
labelEngine.compareLabels();
labelEngine.update(true);

@@ -593,4 +701,3 @@ expect(labelEngine.getShown().length).toBe(1);

labelEngine.setupLabelStates();
labelEngine.compareLabels();
labelEngine.update(true);

@@ -597,0 +704,0 @@ expect(labelEngine.getShown().length).toBe(2);

@@ -145,3 +145,3 @@

/**
* @name destroy
* @name reset
* @memberof labelgun

@@ -152,9 +152,11 @@ * @method

*/
destroy() {
reset() {
this.tree.clear();
this.allLabels = {};
this.hasChanged = [];
this.allChanged = false;
}
/**
* @name callLabelCallbacks
* @name _callLabelCallbacks
* @memberof labelgun

@@ -165,5 +167,5 @@ * @method

* @returns {undefined}
* @public
* @private
*/
callLabelCallbacks(forceState) {
_callLabelCallbacks(forceState) {
Object.keys(this.allLabels).forEach(id => {

@@ -189,3 +191,3 @@ this._callLabelStateCallback(this.allLabels[id], forceState);

/**
* @name compareLabels
* @name _compareLabels
* @memberof labelgun

@@ -195,6 +197,11 @@ * @method

* @returns {undefined}
* @private
*/
compareLabels() {
_compareLabels() {
this.orderedLabels = Object.values(this.allLabels).sort(this._compare);
// Map all the labels to an array and sort based on weight
// highest to lowest
this.orderedLabels = Object.keys(this.allLabels)
.map(v => this.allLabels[v])
.sort(this._compare);

@@ -245,3 +252,3 @@ this.orderedLabels.forEach((label) => {

* @param {object} b - Second object to compare
* @summary Sets up the labels depending on whether all have changed or some have changed
* @summary Compares labels weights for sorting
* @returns {number} - The sort value

@@ -260,3 +267,3 @@ * @private

/**
* @name setupLabelStates
* @name _setupLabels
* @memberof labelgun

@@ -266,4 +273,5 @@ * @method

* @returns {undefined}
* @private
*/
setupLabelStates() {
_setupLabels() {

@@ -324,11 +332,17 @@ if(this.allChanged) {

* @method
* @summary Sets all labels to change and reruns the whole show/hide procedure
* @param {boolean} onlyChanges - Whether to only update the changes made by labelHasChanged
* @summary Sets all or some of the labels to change and reruns the whole show/hide procedure
* @returns {undefined}
*/
update() {
update(onlyChanges) {
this.allChanged = true;
this.setupLabelStates();
this.compareLabels();
this.callLabelCallbacks();
if (onlyChanges) {
this.allChanged = false;
} else {
this.allChanged = true;
}
this._setupLabels();
this._compareLabels();
this._callLabelCallbacks();

@@ -338,18 +352,13 @@ }

/**
* @name _removeFromTree
* @name removeLabel
* @memberof labelgun
* @method
* @param {object} label - The label to remove from the tree
* @param {boolean} forceUpdate if true, triggers all labels to be updated
* @summary Removes label from tree
* @param {object} id - The label id for the label to remove from the tree
* @summary Removes label from tree and allLabels object
* @returns {undefined}
* @private
*/
_removeFromTree(label, forceUpdate) {
const id = label.id;
removeLabel(id) {
const removelLabel = this.allLabels[id];
this.tree.remove(removelLabel);
delete this.allLabels[id];
if (forceUpdate) this.callLabelCallbacks(true);
}

@@ -403,3 +412,3 @@

const oldLabel = this.allLabels[id];
if (oldLabel) this._removeFromTree(oldLabel);
if (oldLabel) this.removeLabel(oldLabel.id);

@@ -406,0 +415,0 @@ const label = {

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc