mapshaper 0.7.22 → 0.7.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/www/modules.js CHANGED
@@ -973,7 +973,7 @@
973
973
  // Buffer instances.
974
974
  Buffer.prototype._isBuffer = true;
975
975
 
976
- function swap$2 (b, n, m) {
976
+ function swap$1 (b, n, m) {
977
977
  var i = b[n];
978
978
  b[n] = b[m];
979
979
  b[m] = i;
@@ -985,7 +985,7 @@
985
985
  throw new RangeError('Buffer size must be a multiple of 16-bits')
986
986
  }
987
987
  for (var i = 0; i < len; i += 2) {
988
- swap$2(this, i, i + 1);
988
+ swap$1(this, i, i + 1);
989
989
  }
990
990
  return this
991
991
  };
@@ -996,8 +996,8 @@
996
996
  throw new RangeError('Buffer size must be a multiple of 32-bits')
997
997
  }
998
998
  for (var i = 0; i < len; i += 4) {
999
- swap$2(this, i, i + 3);
1000
- swap$2(this, i + 1, i + 2);
999
+ swap$1(this, i, i + 3);
1000
+ swap$1(this, i + 1, i + 2);
1001
1001
  }
1002
1002
  return this
1003
1003
  };
@@ -1008,10 +1008,10 @@
1008
1008
  throw new RangeError('Buffer size must be a multiple of 64-bits')
1009
1009
  }
1010
1010
  for (var i = 0; i < len; i += 8) {
1011
- swap$2(this, i, i + 7);
1012
- swap$2(this, i + 1, i + 6);
1013
- swap$2(this, i + 2, i + 5);
1014
- swap$2(this, i + 3, i + 4);
1011
+ swap$1(this, i, i + 7);
1012
+ swap$1(this, i + 1, i + 6);
1013
+ swap$1(this, i + 2, i + 5);
1014
+ swap$1(this, i + 3, i + 4);
1015
1015
  }
1016
1016
  return this
1017
1017
  };
@@ -33430,452 +33430,6 @@
33430
33430
  return pj_init(defn);
33431
33431
  }
33432
33432
 
33433
- class FlatQueue {
33434
-
33435
- constructor() {
33436
- this.ids = [];
33437
- this.values = [];
33438
- this.length = 0;
33439
- }
33440
-
33441
- clear() {
33442
- this.length = 0;
33443
- }
33444
-
33445
- push(id, value) {
33446
- let pos = this.length++;
33447
- this.ids[pos] = id;
33448
- this.values[pos] = value;
33449
-
33450
- while (pos > 0) {
33451
- const parent = (pos - 1) >> 1;
33452
- const parentValue = this.values[parent];
33453
- if (value >= parentValue) break;
33454
- this.ids[pos] = this.ids[parent];
33455
- this.values[pos] = parentValue;
33456
- pos = parent;
33457
- }
33458
-
33459
- this.ids[pos] = id;
33460
- this.values[pos] = value;
33461
- }
33462
-
33463
- pop() {
33464
- if (this.length === 0) return undefined;
33465
-
33466
- const top = this.ids[0];
33467
- this.length--;
33468
-
33469
- if (this.length > 0) {
33470
- const id = this.ids[0] = this.ids[this.length];
33471
- const value = this.values[0] = this.values[this.length];
33472
- const halfLength = this.length >> 1;
33473
- let pos = 0;
33474
-
33475
- while (pos < halfLength) {
33476
- let left = (pos << 1) + 1;
33477
- const right = left + 1;
33478
- let bestIndex = this.ids[left];
33479
- let bestValue = this.values[left];
33480
- const rightValue = this.values[right];
33481
-
33482
- if (right < this.length && rightValue < bestValue) {
33483
- left = right;
33484
- bestIndex = this.ids[right];
33485
- bestValue = rightValue;
33486
- }
33487
- if (bestValue >= value) break;
33488
-
33489
- this.ids[pos] = bestIndex;
33490
- this.values[pos] = bestValue;
33491
- pos = left;
33492
- }
33493
-
33494
- this.ids[pos] = id;
33495
- this.values[pos] = value;
33496
- }
33497
-
33498
- return top;
33499
- }
33500
-
33501
- peek() {
33502
- return this.ids[0];
33503
- }
33504
-
33505
- peekValue() {
33506
- return this.values[0];
33507
- }
33508
- }
33509
-
33510
- const ARRAY_TYPES = [
33511
- Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array,
33512
- Int32Array, Uint32Array, Float32Array, Float64Array
33513
- ];
33514
-
33515
- const VERSION = 3; // serialized format version
33516
-
33517
- class Flatbush {
33518
-
33519
- static from(data) {
33520
- if (!(data instanceof ArrayBuffer)) {
33521
- throw new Error('Data must be an instance of ArrayBuffer.');
33522
- }
33523
- const [magic, versionAndType] = new Uint8Array(data, 0, 2);
33524
- if (magic !== 0xfb) {
33525
- throw new Error('Data does not appear to be in a Flatbush format.');
33526
- }
33527
- if (versionAndType >> 4 !== VERSION) {
33528
- throw new Error(`Got v${versionAndType >> 4} data when expected v${VERSION}.`);
33529
- }
33530
- const [nodeSize] = new Uint16Array(data, 2, 1);
33531
- const [numItems] = new Uint32Array(data, 4, 1);
33532
-
33533
- return new Flatbush(numItems, nodeSize, ARRAY_TYPES[versionAndType & 0x0f], data);
33534
- }
33535
-
33536
- constructor(numItems, nodeSize = 16, ArrayType = Float64Array, data) {
33537
- if (numItems === undefined) throw new Error('Missing required argument: numItems.');
33538
- if (isNaN(numItems) || numItems <= 0) throw new Error(`Unpexpected numItems value: ${numItems}.`);
33539
-
33540
- this.numItems = +numItems;
33541
- this.nodeSize = Math.min(Math.max(+nodeSize, 2), 65535);
33542
-
33543
- // calculate the total number of nodes in the R-tree to allocate space for
33544
- // and the index of each tree level (used in search later)
33545
- let n = numItems;
33546
- let numNodes = n;
33547
- this._levelBounds = [n * 4];
33548
- do {
33549
- n = Math.ceil(n / this.nodeSize);
33550
- numNodes += n;
33551
- this._levelBounds.push(numNodes * 4);
33552
- } while (n !== 1);
33553
-
33554
- this.ArrayType = ArrayType || Float64Array;
33555
- this.IndexArrayType = numNodes < 16384 ? Uint16Array : Uint32Array;
33556
-
33557
- const arrayTypeIndex = ARRAY_TYPES.indexOf(this.ArrayType);
33558
- const nodesByteSize = numNodes * 4 * this.ArrayType.BYTES_PER_ELEMENT;
33559
-
33560
- if (arrayTypeIndex < 0) {
33561
- throw new Error(`Unexpected typed array class: ${ArrayType}.`);
33562
- }
33563
-
33564
- if (data && (data instanceof ArrayBuffer)) {
33565
- this.data = data;
33566
- this._boxes = new this.ArrayType(this.data, 8, numNodes * 4);
33567
- this._indices = new this.IndexArrayType(this.data, 8 + nodesByteSize, numNodes);
33568
-
33569
- this._pos = numNodes * 4;
33570
- this.minX = this._boxes[this._pos - 4];
33571
- this.minY = this._boxes[this._pos - 3];
33572
- this.maxX = this._boxes[this._pos - 2];
33573
- this.maxY = this._boxes[this._pos - 1];
33574
-
33575
- } else {
33576
- this.data = new ArrayBuffer(8 + nodesByteSize + numNodes * this.IndexArrayType.BYTES_PER_ELEMENT);
33577
- this._boxes = new this.ArrayType(this.data, 8, numNodes * 4);
33578
- this._indices = new this.IndexArrayType(this.data, 8 + nodesByteSize, numNodes);
33579
- this._pos = 0;
33580
- this.minX = Infinity;
33581
- this.minY = Infinity;
33582
- this.maxX = -Infinity;
33583
- this.maxY = -Infinity;
33584
-
33585
- new Uint8Array(this.data, 0, 2).set([0xfb, (VERSION << 4) + arrayTypeIndex]);
33586
- new Uint16Array(this.data, 2, 1)[0] = nodeSize;
33587
- new Uint32Array(this.data, 4, 1)[0] = numItems;
33588
- }
33589
-
33590
- // a priority queue for k-nearest-neighbors queries
33591
- this._queue = new FlatQueue();
33592
- }
33593
-
33594
- add(minX, minY, maxX, maxY) {
33595
- const index = this._pos >> 2;
33596
- this._indices[index] = index;
33597
- this._boxes[this._pos++] = minX;
33598
- this._boxes[this._pos++] = minY;
33599
- this._boxes[this._pos++] = maxX;
33600
- this._boxes[this._pos++] = maxY;
33601
-
33602
- if (minX < this.minX) this.minX = minX;
33603
- if (minY < this.minY) this.minY = minY;
33604
- if (maxX > this.maxX) this.maxX = maxX;
33605
- if (maxY > this.maxY) this.maxY = maxY;
33606
-
33607
- return index;
33608
- }
33609
-
33610
- finish() {
33611
- if (this._pos >> 2 !== this.numItems) {
33612
- throw new Error(`Added ${this._pos >> 2} items when expected ${this.numItems}.`);
33613
- }
33614
-
33615
- const width = this.maxX - this.minX;
33616
- const height = this.maxY - this.minY;
33617
- const hilbertValues = new Uint32Array(this.numItems);
33618
- const hilbertMax = (1 << 16) - 1;
33619
-
33620
- // map item centers into Hilbert coordinate space and calculate Hilbert values
33621
- for (let i = 0; i < this.numItems; i++) {
33622
- let pos = 4 * i;
33623
- const minX = this._boxes[pos++];
33624
- const minY = this._boxes[pos++];
33625
- const maxX = this._boxes[pos++];
33626
- const maxY = this._boxes[pos++];
33627
- const x = Math.floor(hilbertMax * ((minX + maxX) / 2 - this.minX) / width);
33628
- const y = Math.floor(hilbertMax * ((minY + maxY) / 2 - this.minY) / height);
33629
- hilbertValues[i] = hilbert(x, y);
33630
- }
33631
-
33632
- // sort items by their Hilbert value (for packing later)
33633
- sort(hilbertValues, this._boxes, this._indices, 0, this.numItems - 1);
33634
-
33635
- // generate nodes at each tree level, bottom-up
33636
- for (let i = 0, pos = 0; i < this._levelBounds.length - 1; i++) {
33637
- const end = this._levelBounds[i];
33638
-
33639
- // generate a parent node for each block of consecutive <nodeSize> nodes
33640
- while (pos < end) {
33641
- let nodeMinX = Infinity;
33642
- let nodeMinY = Infinity;
33643
- let nodeMaxX = -Infinity;
33644
- let nodeMaxY = -Infinity;
33645
- const nodeIndex = pos;
33646
-
33647
- // calculate bbox for the new node
33648
- for (let i = 0; i < this.nodeSize && pos < end; i++) {
33649
- const minX = this._boxes[pos++];
33650
- const minY = this._boxes[pos++];
33651
- const maxX = this._boxes[pos++];
33652
- const maxY = this._boxes[pos++];
33653
- if (minX < nodeMinX) nodeMinX = minX;
33654
- if (minY < nodeMinY) nodeMinY = minY;
33655
- if (maxX > nodeMaxX) nodeMaxX = maxX;
33656
- if (maxY > nodeMaxY) nodeMaxY = maxY;
33657
- }
33658
-
33659
- // add the new node to the tree data
33660
- this._indices[this._pos >> 2] = nodeIndex;
33661
- this._boxes[this._pos++] = nodeMinX;
33662
- this._boxes[this._pos++] = nodeMinY;
33663
- this._boxes[this._pos++] = nodeMaxX;
33664
- this._boxes[this._pos++] = nodeMaxY;
33665
- }
33666
- }
33667
- }
33668
-
33669
- search(minX, minY, maxX, maxY, filterFn) {
33670
- if (this._pos !== this._boxes.length) {
33671
- throw new Error('Data not yet indexed - call index.finish().');
33672
- }
33673
-
33674
- let nodeIndex = this._boxes.length - 4;
33675
- let level = this._levelBounds.length - 1;
33676
- const queue = [];
33677
- const results = [];
33678
-
33679
- while (nodeIndex !== undefined) {
33680
- // find the end index of the node
33681
- const end = Math.min(nodeIndex + this.nodeSize * 4, this._levelBounds[level]);
33682
-
33683
- // search through child nodes
33684
- for (let pos = nodeIndex; pos < end; pos += 4) {
33685
- const index = this._indices[pos >> 2] | 0;
33686
-
33687
- // check if node bbox intersects with query bbox
33688
- if (maxX < this._boxes[pos]) continue; // maxX < nodeMinX
33689
- if (maxY < this._boxes[pos + 1]) continue; // maxY < nodeMinY
33690
- if (minX > this._boxes[pos + 2]) continue; // minX > nodeMaxX
33691
- if (minY > this._boxes[pos + 3]) continue; // minY > nodeMaxY
33692
-
33693
- if (nodeIndex < this.numItems * 4) {
33694
- if (filterFn === undefined || filterFn(index)) {
33695
- results.push(index); // leaf item
33696
- }
33697
-
33698
- } else {
33699
- queue.push(index); // node; add it to the search queue
33700
- queue.push(level - 1);
33701
- }
33702
- }
33703
-
33704
- level = queue.pop();
33705
- nodeIndex = queue.pop();
33706
- }
33707
-
33708
- return results;
33709
- }
33710
-
33711
- neighbors(x, y, maxResults = Infinity, maxDistance = Infinity, filterFn) {
33712
- if (this._pos !== this._boxes.length) {
33713
- throw new Error('Data not yet indexed - call index.finish().');
33714
- }
33715
-
33716
- let nodeIndex = this._boxes.length - 4;
33717
- const q = this._queue;
33718
- const results = [];
33719
- const maxDistSquared = maxDistance * maxDistance;
33720
-
33721
- while (nodeIndex !== undefined) {
33722
- // find the end index of the node
33723
- const end = Math.min(nodeIndex + this.nodeSize * 4, upperBound(nodeIndex, this._levelBounds));
33724
-
33725
- // add child nodes to the queue
33726
- for (let pos = nodeIndex; pos < end; pos += 4) {
33727
- const index = this._indices[pos >> 2] | 0;
33728
-
33729
- const dx = axisDist(x, this._boxes[pos], this._boxes[pos + 2]);
33730
- const dy = axisDist(y, this._boxes[pos + 1], this._boxes[pos + 3]);
33731
- const dist = dx * dx + dy * dy;
33732
-
33733
- if (nodeIndex < this.numItems * 4) { // leaf node
33734
- if (filterFn === undefined || filterFn(index)) {
33735
- // put a negative index if it's an item rather than a node, to recognize later
33736
- q.push(-index - 1, dist);
33737
- }
33738
- } else {
33739
- q.push(index, dist);
33740
- }
33741
- }
33742
-
33743
- // pop items from the queue
33744
- while (q.length && q.peek() < 0) {
33745
- const dist = q.peekValue();
33746
- if (dist > maxDistSquared) {
33747
- q.clear();
33748
- return results;
33749
- }
33750
- results.push(-q.pop() - 1);
33751
-
33752
- if (results.length === maxResults) {
33753
- q.clear();
33754
- return results;
33755
- }
33756
- }
33757
-
33758
- nodeIndex = q.pop();
33759
- }
33760
-
33761
- q.clear();
33762
- return results;
33763
- }
33764
- }
33765
-
33766
- function axisDist(k, min, max) {
33767
- return k < min ? min - k : k <= max ? 0 : k - max;
33768
- }
33769
-
33770
- // binary search for the first value in the array bigger than the given
33771
- function upperBound(value, arr) {
33772
- let i = 0;
33773
- let j = arr.length - 1;
33774
- while (i < j) {
33775
- const m = (i + j) >> 1;
33776
- if (arr[m] > value) {
33777
- j = m;
33778
- } else {
33779
- i = m + 1;
33780
- }
33781
- }
33782
- return arr[i];
33783
- }
33784
-
33785
- // custom quicksort that sorts bbox data alongside the hilbert values
33786
- function sort(values, boxes, indices, left, right) {
33787
- if (left >= right) return;
33788
-
33789
- const pivot = values[(left + right) >> 1];
33790
- let i = left - 1;
33791
- let j = right + 1;
33792
-
33793
- while (true) {
33794
- do i++; while (values[i] < pivot);
33795
- do j--; while (values[j] > pivot);
33796
- if (i >= j) break;
33797
- swap$1(values, boxes, indices, i, j);
33798
- }
33799
-
33800
- sort(values, boxes, indices, left, j);
33801
- sort(values, boxes, indices, j + 1, right);
33802
- }
33803
-
33804
- // swap two values and two corresponding boxes
33805
- function swap$1(values, boxes, indices, i, j) {
33806
- const temp = values[i];
33807
- values[i] = values[j];
33808
- values[j] = temp;
33809
-
33810
- const k = 4 * i;
33811
- const m = 4 * j;
33812
-
33813
- const a = boxes[k];
33814
- const b = boxes[k + 1];
33815
- const c = boxes[k + 2];
33816
- const d = boxes[k + 3];
33817
- boxes[k] = boxes[m];
33818
- boxes[k + 1] = boxes[m + 1];
33819
- boxes[k + 2] = boxes[m + 2];
33820
- boxes[k + 3] = boxes[m + 3];
33821
- boxes[m] = a;
33822
- boxes[m + 1] = b;
33823
- boxes[m + 2] = c;
33824
- boxes[m + 3] = d;
33825
-
33826
- const e = indices[i];
33827
- indices[i] = indices[j];
33828
- indices[j] = e;
33829
- }
33830
-
33831
- // Fast Hilbert curve algorithm by http://threadlocalmutex.com/
33832
- // Ported from C++ https://github.com/rawrunprotected/hilbert_curves (public domain)
33833
- function hilbert(x, y) {
33834
- let a = x ^ y;
33835
- let b = 0xFFFF ^ a;
33836
- let c = 0xFFFF ^ (x | y);
33837
- let d = x & (y ^ 0xFFFF);
33838
-
33839
- let A = a | (b >> 1);
33840
- let B = (a >> 1) ^ a;
33841
- let C = ((c >> 1) ^ (b & (d >> 1))) ^ c;
33842
- let D = ((a & (c >> 1)) ^ (d >> 1)) ^ d;
33843
-
33844
- a = A; b = B; c = C; d = D;
33845
- A = ((a & (a >> 2)) ^ (b & (b >> 2)));
33846
- B = ((a & (b >> 2)) ^ (b & ((a ^ b) >> 2)));
33847
- C ^= ((a & (c >> 2)) ^ (b & (d >> 2)));
33848
- D ^= ((b & (c >> 2)) ^ ((a ^ b) & (d >> 2)));
33849
-
33850
- a = A; b = B; c = C; d = D;
33851
- A = ((a & (a >> 4)) ^ (b & (b >> 4)));
33852
- B = ((a & (b >> 4)) ^ (b & ((a ^ b) >> 4)));
33853
- C ^= ((a & (c >> 4)) ^ (b & (d >> 4)));
33854
- D ^= ((b & (c >> 4)) ^ ((a ^ b) & (d >> 4)));
33855
-
33856
- a = A; b = B; c = C; d = D;
33857
- C ^= ((a & (c >> 8)) ^ (b & (d >> 8)));
33858
- D ^= ((b & (c >> 8)) ^ ((a ^ b) & (d >> 8)));
33859
-
33860
- a = C ^ (C >> 1);
33861
- b = D ^ (D >> 1);
33862
-
33863
- let i0 = x ^ y;
33864
- let i1 = b | (0xFFFF ^ (i0 | a));
33865
-
33866
- i0 = (i0 | (i0 << 8)) & 0x00FF00FF;
33867
- i0 = (i0 | (i0 << 4)) & 0x0F0F0F0F;
33868
- i0 = (i0 | (i0 << 2)) & 0x33333333;
33869
- i0 = (i0 | (i0 << 1)) & 0x55555555;
33870
-
33871
- i1 = (i1 | (i1 << 8)) & 0x00FF00FF;
33872
- i1 = (i1 | (i1 << 4)) & 0x0F0F0F0F;
33873
- i1 = (i1 | (i1 << 2)) & 0x33333333;
33874
- i1 = (i1 | (i1 << 1)) & 0x55555555;
33875
-
33876
- return ((i1 << 1) | i0) >>> 0;
33877
- }
33878
-
33879
33433
  function sortKD(ids, coords, nodeSize, left, right, depth) {
33880
33434
  if (right - left <= nodeSize) return;
33881
33435
 
@@ -34213,7 +33767,6 @@
34213
33767
  '@tmcw/togeojson': togeojson,
34214
33768
  'geographiclib-geodesic': GeographicLib,
34215
33769
  mproj: api,
34216
- flatbush: Flatbush,
34217
33770
  kdbush: KDBush,
34218
33771
  buffer
34219
33772
  };
package/www/page.css CHANGED
@@ -23,6 +23,7 @@
23
23
  --colored-text: #10699b;
24
24
  --normal-text: #333;
25
25
  --left-sidebar-width: 420px;
26
+ --sidebar-panels-separator-position: 50%;
26
27
  --sidebar-resize-col: #f2c65a; /* #d6a51d; */
27
28
  }
28
29
 
@@ -788,7 +789,7 @@ input[type="checkbox"]
788
789
 
789
790
  /* --- Sidebar panels ------------ */
790
791
 
791
- .sidebar-panel {
792
+ .sidebar-panels {
792
793
  right: auto;
793
794
  width: var(--left-sidebar-width);
794
795
  max-width: 100%;
@@ -800,6 +801,31 @@ input[type="checkbox"]
800
801
  display: none;
801
802
  }
802
803
 
804
+ .sidebar-panel {
805
+ position: relative;
806
+ }
807
+
808
+ .sidebar-panel:first-child {
809
+ height: var(--sidebar-panels-separator-position);
810
+ }
811
+
812
+ .sidebar-panel:last-child {
813
+ height: calc(100% - var(--sidebar-panels-separator-position));
814
+ }
815
+
816
+ .sidebar-panels:not(:has(> .sidebar-panel.open ~ .sidebar-panel.open)) > .sidebar-panel.open {
817
+ height: 100%;
818
+ }
819
+
820
+ .sidebar-panels:not(:has(> .sidebar-panel.open ~ .sidebar-panel.open)) .sidebar-panels-resize-handle {
821
+ display: none;
822
+ }
823
+
824
+ body.sidebar-open .sidebar-panels {
825
+ display: flex;
826
+ flex-direction: column;
827
+ }
828
+
803
829
  .console-area {
804
830
  font-size: 12px;
805
831
  }
@@ -845,6 +871,39 @@ body.sidebar-resizing .sidebar-resize-handle::before,
845
871
  background-color: var(--sidebar-resize-col);
846
872
  }
847
873
 
874
+ .sidebar-panels-resize-handle {
875
+ display: block;
876
+ position: absolute;
877
+ left: 0;
878
+ right: 0;
879
+ top: -4px;
880
+ height: 8px;
881
+ z-index: 35;
882
+ cursor: ns-resize;
883
+ pointer-events: auto;
884
+ }
885
+
886
+ .sidebar-panels-resize-handle::before {
887
+ content: "";
888
+ position: absolute;
889
+ left: 0;
890
+ right: 0;
891
+ top: 3px;
892
+ height: 2px;
893
+ background-color: transparent;
894
+ }
895
+
896
+ body.sidebar-panels-resizing,
897
+ body.sidebar-panels-resizing * {
898
+ cursor: ns-resize !important;
899
+ user-select: none;
900
+ }
901
+
902
+ body.sidebar-panels-resizing .sidebar-panels-resize-handle::before,
903
+ .sidebar-panels-resize-handle:hover::before {
904
+ background-color: var(--sidebar-resize-col);
905
+ }
906
+
848
907
  /* Give the console a little more line length on wide screens. */
849
908
  @media screen and (max-width: 525px) {
850
909
  body.sidebar-open .map-area {
package/www/sponsor.html CHANGED
@@ -36,6 +36,7 @@
36
36
 
37
37
  <h2>Recent work</h2>
38
38
  <ul class="support-list">
39
+ <li><strong>Buffer command</strong> &mdash; Geodesic and planar buffering for points, lines and polygons</li>
39
40
  <li><strong>Raster importing</strong> &mdash; GeoTIFF + georeferenced JPEG and PNG</li>
40
41
  <li><strong>Undo/redo</strong> in the browser &mdash; the most requested feature</li>
41
42
  <li><strong>New vector formats</strong> &mdash; GeoParquet, GeoPackage and FlatGeobuf</li>