v 0.8.0b
16
icons.css
|
|
@ -209,4 +209,18 @@
|
||||||
.icon-smooth:before {font-weight: bold; content: '∼'; }
|
.icon-smooth:before {font-weight: bold; content: '∼'; }
|
||||||
.icon-disrupt:before {font-weight: bold; content: '෴'; }
|
.icon-disrupt:before {font-weight: bold; content: '෴'; }
|
||||||
.icon-if:before {font-style: italic; font-weight: bold; content: 'if'; }
|
.icon-if:before {font-style: italic; font-weight: bold; content: 'if'; }
|
||||||
.icon-arc:before {font-weight: bold; font-size: 1.2em; content: '⌒'; }
|
|
||||||
|
.icon-ruler:before {content: 'I'; }
|
||||||
|
.icon-curve:before {content: 'C'; }
|
||||||
|
.icon-area:before {content: 'O'; }
|
||||||
|
|
||||||
|
.icon-ruler:before,
|
||||||
|
.icon-curve:before,
|
||||||
|
.icon-area:before {
|
||||||
|
font-size: 1.5em;
|
||||||
|
padding: 0;
|
||||||
|
writing-mode: tb-rl;
|
||||||
|
margin-left: 1px;
|
||||||
|
width: 10px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
|
||||||
BIN
images/textures/antique-big.jpg
Normal file
|
After Width: | Height: | Size: 840 KiB |
BIN
images/textures/antique-small.jpg
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
images/textures/iran-small.jpg
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
images/textures/marble-big.jpg
Normal file
|
After Width: | Height: | Size: 994 KiB |
BIN
images/textures/marble-blue-big.jpg
Normal file
|
After Width: | Height: | Size: 627 KiB |
BIN
images/textures/marble-blue-small.jpg
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
images/textures/marble-small.jpg
Normal file
|
After Width: | Height: | Size: 223 KiB |
BIN
images/textures/mars-big.jpg
Normal file
|
After Width: | Height: | Size: 733 KiB |
BIN
images/textures/mars-small.jpg
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
images/textures/mauritania-small.jpg
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
images/textures/mercury-big.jpg
Normal file
|
After Width: | Height: | Size: 852 KiB |
BIN
images/textures/mercury-small.jpg
Normal file
|
After Width: | Height: | Size: 213 KiB |
BIN
images/textures/pergamena-small.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
images/textures/spain-small.jpg
Normal file
|
After Width: | Height: | Size: 221 KiB |
BIN
images/textures/stone-big.jpg
Normal file
|
After Width: | Height: | Size: 498 KiB |
BIN
images/textures/stone-small.jpg
Normal file
|
After Width: | Height: | Size: 154 KiB |
12
images/textures/textures-attribution.txt
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
All used textures should be distrubuted under free license
|
||||||
|
marble.jpg https://www.rawpixel.com/image/327647/closeup-marble-textured-background by Teddy Rawpixel
|
||||||
|
marble-blue.jpg https://www.pexels.com/photo/gray-and-blue-surface-988871/
|
||||||
|
timbercut.jpg https://www.pexels.com/photo/brown-close-up-hd-wallpaper-surface-172289
|
||||||
|
antique.jpg https://www.pexels.com/photo/abstract-ancient-antique-art-235985/
|
||||||
|
pergamena.jpg https://www.freepik.com/free-photo/old-paper-texture-background_1007802.htm
|
||||||
|
stone.jpg https://www.wildtextures.com/free-textures/grungy-yet-elegant-elevation-stone-ii
|
||||||
|
mars.jpg https://www.solarsystemscope.com/textures/download/2k_mars.jpg
|
||||||
|
mercury.jpg https://www.solarsystemscope.com/textures/download/2k_mercury.jpg
|
||||||
|
mauritania.jpg https://go.nasa.gov/2Ugu9M8 NASA Worldview (Jan 24, 2019)
|
||||||
|
iran.jpg https://go.nasa.gov/2FNmiT5 NASA Worldview (Jan 24, 2019)
|
||||||
|
spain.jpg https://go.nasa.gov/2FMDPuu NASA Worldview (Jan 24, 2019)
|
||||||
BIN
images/textures/timbercut-big.jpg
Normal file
|
After Width: | Height: | Size: 893 KiB |
BIN
images/textures/timbercut-small.jpg
Normal file
|
After Width: | Height: | Size: 214 KiB |
1198
index.css
2279
index.html
2
libs/d3-scale-chromatic.v1.min.js
vendored
2
libs/d3.min.js
vendored
Normal file
2
libs/d3.v4.min.js
vendored
1
libs/delaunator.min.js
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):t.Delaunator=i()}(this,function(){"use strict";var t=Math.pow(2,-52),i=function(i){var n=i.length>>1;if(n>0&&"number"!=typeof i[0])throw new Error("Expected coords to contain numbers.");this.coords=i;var s=2*n-5,l=this.triangles=new Uint32Array(3*s),o=this.halfedges=new Int32Array(3*s);this._hashSize=Math.ceil(Math.sqrt(n));for(var f=this.hullPrev=new Uint32Array(n),u=this.hullNext=new Uint32Array(n),v=this.hullTri=new Uint32Array(n),d=new Int32Array(this._hashSize).fill(-1),_=new Uint32Array(n),y=1/0,g=1/0,c=-1/0,w=-1/0,p=0;p<n;p++){var b=i[2*p],x=i[2*p+1];b<y&&(y=b),x<g&&(g=x),b>c&&(c=b),x>w&&(w=x),_[p]=p}for(var z,S,k,A=(y+c)/2,T=(g+w)/2,M=1/0,K=0;K<n;K++){var m=r(A,T,i[2*K],i[2*K+1]);m<M&&(z=K,M=m)}var U=i[2*z],L=i[2*z+1];M=1/0;for(var N=0;N<n;N++)if(N!==z){var E=r(U,L,i[2*N],i[2*N+1]);E<M&&E>0&&(S=N,M=E)}for(var D=i[2*S],F=i[2*S+1],I=1/0,P=0;P<n;P++)if(P!==z&&P!==S){var j=h(U,L,D,F,i[2*P],i[2*P+1]);j<I&&(k=P,I=j)}var q=i[2*k],B=i[2*k+1];if(I===1/0)throw new Error("No Delaunay triangulation exists for this input.");if(a(U,L,D,F,q,B)){var C=S,G=D,H=F;S=k,D=q,F=B,k=C,q=G,B=H}var J=function(t,i,r,a,h,e){var n=r-t,s=a-i,l=h-t,o=e-i,f=n*n+s*s,u=l*l+o*o,v=.5/(n*o-s*l);return{x:t+(o*f-s*u)*v,y:i+(n*u-l*f)*v}}(U,L,D,F,q,B);this._cx=J.x,this._cy=J.y;for(var O=new Float64Array(n),Q=0;Q<n;Q++)O[Q]=r(i[2*Q],i[2*Q+1],J.x,J.y);!function t(i,r,a,h){if(h-a<=20)for(var n=a+1;n<=h;n++){for(var s=i[n],l=r[s],o=n-1;o>=a&&r[i[o]]>l;)i[o+1]=i[o--];i[o+1]=s}else{var f=a+h>>1,u=a+1,v=h;e(i,f,u),r[i[a]]>r[i[h]]&&e(i,a,h),r[i[u]]>r[i[h]]&&e(i,u,h),r[i[a]]>r[i[u]]&&e(i,a,u);for(var d=i[u],_=r[d];;){do{u++}while(r[i[u]]<_);do{v--}while(r[i[v]]>_);if(v<u)break;e(i,u,v)}i[a+1]=i[v],i[v]=d,h-u+1>=v-a?(t(i,r,u,h),t(i,r,a,v-1)):(t(i,r,a,v-1),t(i,r,u,h))}}(_,O,0,n-1),this.hullStart=z;var R=3;u[z]=f[k]=S,u[S]=f[z]=k,u[k]=f[S]=z,v[z]=0,v[S]=1,v[k]=2,d[this._hashKey(U,L)]=z,d[this._hashKey(D,F)]=S,d[this._hashKey(q,B)]=k,this.trianglesLen=0,this._addTriangle(z,S,k,-1,-1,-1);for(var V=0,W=void 0,X=void 0;V<_.length;V++){var Y=_[V],Z=i[2*Y],$=i[2*Y+1];if(!(V>0&&Math.abs(Z-W)<=t&&Math.abs($-X)<=t)&&(W=Z,X=$,Y!==z&&Y!==S&&Y!==k)){for(var tt=0,it=0,rt=this._hashKey(Z,$);it<this._hashSize&&(-1===(tt=d[(rt+it)%this._hashSize])||tt===u[tt]);it++);for(var at=tt=f[tt],ht=void 0;ht=u[at],!a(Z,$,i[2*at],i[2*at+1],i[2*ht],i[2*ht+1]);)if((at=ht)===tt){at=-1;break}if(-1!==at){var et=this._addTriangle(at,Y,u[at],-1,-1,v[at]);v[Y]=this._legalize(et+2),v[at]=et,R++;for(var nt=u[at];ht=u[nt],a(Z,$,i[2*nt],i[2*nt+1],i[2*ht],i[2*ht+1]);)et=this._addTriangle(nt,Y,ht,v[Y],-1,v[nt]),v[Y]=this._legalize(et+2),u[nt]=nt,R--,nt=ht;if(at===tt)for(;a(Z,$,i[2*(ht=f[at])],i[2*ht+1],i[2*at],i[2*at+1]);)et=this._addTriangle(ht,Y,at,-1,v[at],v[ht]),this._legalize(et+2),v[ht]=et,u[at]=at,R--,at=ht;this.hullStart=f[Y]=at,u[at]=f[nt]=Y,u[Y]=nt,d[this._hashKey(Z,$)]=Y,d[this._hashKey(i[2*at],i[2*at+1])]=at}}}this.hull=new Uint32Array(R);for(var st=0,lt=this.hullStart;st<R;st++)this.hull[st]=lt,lt=u[lt];this.hullPrev=this.hullNext=this.hullTri=null,this.triangles=l.subarray(0,this.trianglesLen),this.halfedges=o.subarray(0,this.trianglesLen)};function r(t,i,r,a){var h=t-r,e=i-a;return h*h+e*e}function a(t,i,r,a,h,e){return(a-i)*(h-r)-(r-t)*(e-a)<0}function h(t,i,r,a,h,e){var n=r-t,s=a-i,l=h-t,o=e-i,f=n*n+s*s,u=l*l+o*o,v=.5/(n*o-s*l),d=(o*f-s*u)*v,_=(n*u-l*f)*v;return d*d+_*_}function e(t,i,r){var a=t[i];t[i]=t[r],t[r]=a}function n(t){return t[0]}function s(t){return t[1]}return i.from=function(t,r,a){void 0===r&&(r=n),void 0===a&&(a=s);for(var h=t.length,e=new Float64Array(2*h),l=0;l<h;l++){var o=t[l];e[2*l]=r(o),e[2*l+1]=a(o)}return new i(e)},i.prototype._hashKey=function(t,i){return Math.floor((r=t-this._cx,a=i-this._cy,h=r/(Math.abs(r)+Math.abs(a)),(a>0?3-h:1+h)/4*this._hashSize))%this._hashSize;var r,a,h},i.prototype._legalize=function(t){var i=this.triangles,r=this.coords,a=this.halfedges,h=a[t],e=t-t%3,n=h-h%3,s=e+(t+1)%3,l=e+(t+2)%3,o=n+(h+2)%3;if(-1===h)return l;var f,u,v,d,_,y,g,c,w,p,b,x,z,S,k,A,T=i[l],M=i[t],K=i[s],m=i[o];if(f=r[2*T],u=r[2*T+1],v=r[2*M],d=r[2*M+1],_=r[2*K],y=r[2*K+1],g=r[2*m],c=r[2*m+1],(w=f-g)*((x=d-c)*(A=(z=_-g)*z+(S=y-c)*S)-(k=(b=v-g)*b+x*x)*S)-(p=u-c)*(b*A-k*z)+(w*w+p*p)*(b*S-x*z)<0){i[t]=m,i[h]=T;var U=a[o];if(-1===U){var L=this.hullStart;do{if(this.hullTri[L]===o){this.hullTri[L]=t;break}L=this.hullNext[L]}while(L!==this.hullStart)}this._link(t,U),this._link(h,a[l]),this._link(l,o);var N=n+(h+1)%3;return this._legalize(t),this._legalize(N)}return l},i.prototype._link=function(t,i){this.halfedges[t]=i,-1!==i&&(this.halfedges[i]=t)},i.prototype._addTriangle=function(t,i,r,a,h,e){var n=this.trianglesLen;return this.triangles[n]=t,this.triangles[n+1]=i,this.triangles[n+2]=r,this._link(n,a),this._link(n+1,h),this._link(n+2,e),this.trianglesLen+=3,n},i});
|
||||||
23
libs/jquery-ui.css
vendored
|
|
@ -314,13 +314,13 @@ body .ui-dialog {
|
||||||
left: 0;
|
left: 0;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 12px;
|
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
}
|
}
|
||||||
.ui-dialog .ui-dialog-titlebar {
|
.ui-dialog .ui-dialog-titlebar {
|
||||||
padding: .4em 1em;
|
padding: .4em 1em;
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: 14px;
|
font-size: 1.2em;
|
||||||
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
.ui-dialog .ui-dialog-title {
|
.ui-dialog .ui-dialog-title {
|
||||||
float: left;
|
float: left;
|
||||||
|
|
@ -330,18 +330,33 @@ body .ui-dialog {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.ui-dialog .ui-dialog-titlebar-close {
|
|
||||||
|
.ui-dialog .ui-dialog-titlebar button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: .5em;
|
right: .5em;
|
||||||
top: 53%;
|
top: 53%;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
margin: -10px 0 0 0;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
background: none;
|
background: none;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
border: 1px solid #c5c5c5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-dialog .ui-dialog-titlebar button.ui-dialog-titlebar-collapse {
|
||||||
|
margin: -10px 22px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-dialog .ui-dialog-titlebar button.ui-dialog-titlebar-close {
|
||||||
|
margin: -10px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-dialog .ui-dialog-titlebar button:active {
|
||||||
|
border: 1px solid #5d4651;
|
||||||
|
color: #5d4651;
|
||||||
|
}
|
||||||
|
|
||||||
.ui-dialog .ui-dialog-content {
|
.ui-dialog .ui-dialog-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|
|
||||||
10
libs/jquery-ui.min.js
vendored
|
|
@ -1,232 +0,0 @@
|
||||||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.polylabel = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var Queue = require('tinyqueue');
|
|
||||||
|
|
||||||
module.exports = polylabel;
|
|
||||||
|
|
||||||
function polylabel(polygon, precision, debug) {
|
|
||||||
precision = precision || 1.0;
|
|
||||||
|
|
||||||
// find the bounding box of the outer ring
|
|
||||||
var minX, minY, maxX, maxY;
|
|
||||||
for (var i = 0; i < polygon[0].length; i++) {
|
|
||||||
var p = polygon[0][i];
|
|
||||||
if (!i || p[0] < minX) minX = p[0];
|
|
||||||
if (!i || p[1] < minY) minY = p[1];
|
|
||||||
if (!i || p[0] > maxX) maxX = p[0];
|
|
||||||
if (!i || p[1] > maxY) maxY = p[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
var width = maxX - minX;
|
|
||||||
var height = maxY - minY;
|
|
||||||
var cellSize = Math.min(width, height);
|
|
||||||
var h = cellSize / 2;
|
|
||||||
|
|
||||||
// a priority queue of cells in order of their "potential" (max distance to polygon)
|
|
||||||
var cellQueue = new Queue(null, compareMax);
|
|
||||||
|
|
||||||
// cover polygon with initial cells
|
|
||||||
for (var x = minX; x < maxX; x += cellSize) {
|
|
||||||
for (var y = minY; y < maxY; y += cellSize) {
|
|
||||||
cellQueue.push(new Cell(x + h, y + h, h, polygon));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// take centroid as the first best guess
|
|
||||||
var bestCell = getCentroidCell(polygon);
|
|
||||||
var numProbes = cellQueue.length;
|
|
||||||
|
|
||||||
while (cellQueue.length) {
|
|
||||||
// pick the most promising cell from the queue
|
|
||||||
var cell = cellQueue.pop();
|
|
||||||
|
|
||||||
// update the best cell if we found a better one
|
|
||||||
if (cell.d > bestCell.d) {
|
|
||||||
bestCell = cell;
|
|
||||||
if (debug) console.log('found best %d after %d probes', Math.round(1e4 * cell.d) / 1e4, numProbes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// do not drill down further if there's no chance of a better solution
|
|
||||||
if (cell.max - bestCell.d <= precision) continue;
|
|
||||||
|
|
||||||
// split the cell into four cells
|
|
||||||
h = cell.h / 2;
|
|
||||||
cellQueue.push(new Cell(cell.x - h, cell.y - h, h, polygon));
|
|
||||||
cellQueue.push(new Cell(cell.x + h, cell.y - h, h, polygon));
|
|
||||||
cellQueue.push(new Cell(cell.x - h, cell.y + h, h, polygon));
|
|
||||||
cellQueue.push(new Cell(cell.x + h, cell.y + h, h, polygon));
|
|
||||||
numProbes += 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
console.log('num probes: ' + numProbes);
|
|
||||||
console.log('best distance: ' + bestCell.d);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [bestCell.x, bestCell.y];
|
|
||||||
}
|
|
||||||
|
|
||||||
function compareMax(a, b) {
|
|
||||||
return b.max - a.max;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Cell(x, y, h, polygon) {
|
|
||||||
this.x = x; // cell center x
|
|
||||||
this.y = y; // cell center y
|
|
||||||
this.h = h; // half the cell size
|
|
||||||
this.d = pointToPolygonDist(x, y, polygon); // distance from cell center to polygon
|
|
||||||
this.max = this.d + this.h * Math.SQRT2; // max distance to polygon within a cell
|
|
||||||
}
|
|
||||||
|
|
||||||
// signed distance from point to polygon outline (negative if point is outside)
|
|
||||||
function pointToPolygonDist(x, y, polygon) {
|
|
||||||
var inside = false;
|
|
||||||
var minDistSq = Infinity;
|
|
||||||
|
|
||||||
for (var k = 0; k < polygon.length; k++) {
|
|
||||||
var ring = polygon[k];
|
|
||||||
|
|
||||||
for (var i = 0, len = ring.length, j = len - 1; i < len; j = i++) {
|
|
||||||
var a = ring[i];
|
|
||||||
var b = ring[j];
|
|
||||||
|
|
||||||
if ((a[1] > y !== b[1] > y) &&
|
|
||||||
(x < (b[0] - a[0]) * (y - a[1]) / (b[1] - a[1]) + a[0])) inside = !inside;
|
|
||||||
|
|
||||||
minDistSq = Math.min(minDistSq, getSegDistSq(x, y, a, b));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (inside ? 1 : -1) * Math.sqrt(minDistSq);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get polygon centroid
|
|
||||||
function getCentroidCell(polygon) {
|
|
||||||
var area = 0;
|
|
||||||
var x = 0;
|
|
||||||
var y = 0;
|
|
||||||
var points = polygon[0];
|
|
||||||
|
|
||||||
for (var i = 0, len = points.length, j = len - 1; i < len; j = i++) {
|
|
||||||
var a = points[i];
|
|
||||||
var b = points[j];
|
|
||||||
var f = a[0] * b[1] - b[0] * a[1];
|
|
||||||
x += (a[0] + b[0]) * f;
|
|
||||||
y += (a[1] + b[1]) * f;
|
|
||||||
area += f * 3;
|
|
||||||
}
|
|
||||||
return new Cell(x / area, y / area, 0, polygon);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get squared distance from a point to a segment
|
|
||||||
function getSegDistSq(px, py, a, b) {
|
|
||||||
|
|
||||||
var x = a[0];
|
|
||||||
var y = a[1];
|
|
||||||
var dx = b[0] - x;
|
|
||||||
var dy = b[1] - y;
|
|
||||||
|
|
||||||
if (dx !== 0 || dy !== 0) {
|
|
||||||
|
|
||||||
var t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy);
|
|
||||||
|
|
||||||
if (t > 1) {
|
|
||||||
x = b[0];
|
|
||||||
y = b[1];
|
|
||||||
|
|
||||||
} else if (t > 0) {
|
|
||||||
x += dx * t;
|
|
||||||
y += dy * t;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dx = px - x;
|
|
||||||
dy = py - y;
|
|
||||||
|
|
||||||
return dx * dx + dy * dy;
|
|
||||||
}
|
|
||||||
|
|
||||||
},{"tinyqueue":2}],2:[function(require,module,exports){
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = TinyQueue;
|
|
||||||
|
|
||||||
function TinyQueue(data, compare) {
|
|
||||||
if (!(this instanceof TinyQueue)) return new TinyQueue(data, compare);
|
|
||||||
|
|
||||||
this.data = data || [];
|
|
||||||
this.length = this.data.length;
|
|
||||||
this.compare = compare || defaultCompare;
|
|
||||||
|
|
||||||
if (data) for (var i = Math.floor(this.length / 2); i >= 0; i--) this._down(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultCompare(a, b) {
|
|
||||||
return a < b ? -1 : a > b ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
TinyQueue.prototype = {
|
|
||||||
|
|
||||||
push: function (item) {
|
|
||||||
this.data.push(item);
|
|
||||||
this.length++;
|
|
||||||
this._up(this.length - 1);
|
|
||||||
},
|
|
||||||
|
|
||||||
pop: function () {
|
|
||||||
var top = this.data[0];
|
|
||||||
this.data[0] = this.data[this.length - 1];
|
|
||||||
this.length--;
|
|
||||||
this.data.pop();
|
|
||||||
this._down(0);
|
|
||||||
return top;
|
|
||||||
},
|
|
||||||
|
|
||||||
peek: function () {
|
|
||||||
return this.data[0];
|
|
||||||
},
|
|
||||||
|
|
||||||
_up: function (pos) {
|
|
||||||
var data = this.data,
|
|
||||||
compare = this.compare;
|
|
||||||
|
|
||||||
while (pos > 0) {
|
|
||||||
var parent = Math.floor((pos - 1) / 2);
|
|
||||||
if (compare(data[pos], data[parent]) < 0) {
|
|
||||||
swap(data, parent, pos);
|
|
||||||
pos = parent;
|
|
||||||
|
|
||||||
} else break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_down: function (pos) {
|
|
||||||
var data = this.data,
|
|
||||||
compare = this.compare,
|
|
||||||
len = this.length;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
var left = 2 * pos + 1,
|
|
||||||
right = left + 1,
|
|
||||||
min = pos;
|
|
||||||
|
|
||||||
if (left < len && compare(data[left], data[min]) < 0) min = left;
|
|
||||||
if (right < len && compare(data[right], data[min]) < 0) min = right;
|
|
||||||
|
|
||||||
if (min === pos) return;
|
|
||||||
|
|
||||||
swap(data, min, pos);
|
|
||||||
pos = min;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function swap(data, i, j) {
|
|
||||||
var tmp = data[i];
|
|
||||||
data[i] = data[j];
|
|
||||||
data[j] = tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
},{}]},{},[1])(1)
|
|
||||||
});
|
|
||||||
|
|
@ -1,387 +0,0 @@
|
||||||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.PriorityQueue = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
|
|
||||||
var AbstractPriorityQueue, ArrayStrategy, BHeapStrategy, BinaryHeapStrategy, PriorityQueue,
|
|
||||||
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
|
|
||||||
hasProp = {}.hasOwnProperty;
|
|
||||||
|
|
||||||
AbstractPriorityQueue = _dereq_('./PriorityQueue/AbstractPriorityQueue');
|
|
||||||
|
|
||||||
ArrayStrategy = _dereq_('./PriorityQueue/ArrayStrategy');
|
|
||||||
|
|
||||||
BinaryHeapStrategy = _dereq_('./PriorityQueue/BinaryHeapStrategy');
|
|
||||||
|
|
||||||
BHeapStrategy = _dereq_('./PriorityQueue/BHeapStrategy');
|
|
||||||
|
|
||||||
PriorityQueue = (function(superClass) {
|
|
||||||
extend(PriorityQueue, superClass);
|
|
||||||
|
|
||||||
function PriorityQueue(options) {
|
|
||||||
options || (options = {});
|
|
||||||
options.strategy || (options.strategy = BinaryHeapStrategy);
|
|
||||||
options.comparator || (options.comparator = function(a, b) {
|
|
||||||
return (a || 0) - (b || 0);
|
|
||||||
});
|
|
||||||
PriorityQueue.__super__.constructor.call(this, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
return PriorityQueue;
|
|
||||||
|
|
||||||
})(AbstractPriorityQueue);
|
|
||||||
|
|
||||||
PriorityQueue.ArrayStrategy = ArrayStrategy;
|
|
||||||
|
|
||||||
PriorityQueue.BinaryHeapStrategy = BinaryHeapStrategy;
|
|
||||||
|
|
||||||
PriorityQueue.BHeapStrategy = BHeapStrategy;
|
|
||||||
|
|
||||||
module.exports = PriorityQueue;
|
|
||||||
|
|
||||||
|
|
||||||
},{"./PriorityQueue/AbstractPriorityQueue":2,"./PriorityQueue/ArrayStrategy":3,"./PriorityQueue/BHeapStrategy":4,"./PriorityQueue/BinaryHeapStrategy":5}],2:[function(_dereq_,module,exports){
|
|
||||||
var AbstractPriorityQueue;
|
|
||||||
|
|
||||||
module.exports = AbstractPriorityQueue = (function() {
|
|
||||||
function AbstractPriorityQueue(options) {
|
|
||||||
var ref;
|
|
||||||
if ((options != null ? options.strategy : void 0) == null) {
|
|
||||||
throw 'Must pass options.strategy, a strategy';
|
|
||||||
}
|
|
||||||
if ((options != null ? options.comparator : void 0) == null) {
|
|
||||||
throw 'Must pass options.comparator, a comparator';
|
|
||||||
}
|
|
||||||
this.priv = new options.strategy(options);
|
|
||||||
this.length = (options != null ? (ref = options.initialValues) != null ? ref.length : void 0 : void 0) || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
AbstractPriorityQueue.prototype.queue = function(value) {
|
|
||||||
this.length++;
|
|
||||||
this.priv.queue(value);
|
|
||||||
return void 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
AbstractPriorityQueue.prototype.dequeue = function(value) {
|
|
||||||
if (!this.length) {
|
|
||||||
throw 'Empty queue';
|
|
||||||
}
|
|
||||||
this.length--;
|
|
||||||
return this.priv.dequeue();
|
|
||||||
};
|
|
||||||
|
|
||||||
AbstractPriorityQueue.prototype.peek = function(value) {
|
|
||||||
if (!this.length) {
|
|
||||||
throw 'Empty queue';
|
|
||||||
}
|
|
||||||
return this.priv.peek();
|
|
||||||
};
|
|
||||||
|
|
||||||
AbstractPriorityQueue.prototype.clear = function() {
|
|
||||||
this.length = 0;
|
|
||||||
return this.priv.clear();
|
|
||||||
};
|
|
||||||
|
|
||||||
return AbstractPriorityQueue;
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
||||||
|
|
||||||
},{}],3:[function(_dereq_,module,exports){
|
|
||||||
var ArrayStrategy, binarySearchForIndexReversed;
|
|
||||||
|
|
||||||
binarySearchForIndexReversed = function(array, value, comparator) {
|
|
||||||
var high, low, mid;
|
|
||||||
low = 0;
|
|
||||||
high = array.length;
|
|
||||||
while (low < high) {
|
|
||||||
mid = (low + high) >>> 1;
|
|
||||||
if (comparator(array[mid], value) >= 0) {
|
|
||||||
low = mid + 1;
|
|
||||||
} else {
|
|
||||||
high = mid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return low;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = ArrayStrategy = (function() {
|
|
||||||
function ArrayStrategy(options) {
|
|
||||||
var ref;
|
|
||||||
this.options = options;
|
|
||||||
this.comparator = this.options.comparator;
|
|
||||||
this.data = ((ref = this.options.initialValues) != null ? ref.slice(0) : void 0) || [];
|
|
||||||
this.data.sort(this.comparator).reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
ArrayStrategy.prototype.queue = function(value) {
|
|
||||||
var pos;
|
|
||||||
pos = binarySearchForIndexReversed(this.data, value, this.comparator);
|
|
||||||
this.data.splice(pos, 0, value);
|
|
||||||
return void 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
ArrayStrategy.prototype.dequeue = function() {
|
|
||||||
return this.data.pop();
|
|
||||||
};
|
|
||||||
|
|
||||||
ArrayStrategy.prototype.peek = function() {
|
|
||||||
return this.data[this.data.length - 1];
|
|
||||||
};
|
|
||||||
|
|
||||||
ArrayStrategy.prototype.clear = function() {
|
|
||||||
this.data.length = 0;
|
|
||||||
return void 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
return ArrayStrategy;
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
||||||
|
|
||||||
},{}],4:[function(_dereq_,module,exports){
|
|
||||||
var BHeapStrategy;
|
|
||||||
|
|
||||||
module.exports = BHeapStrategy = (function() {
|
|
||||||
function BHeapStrategy(options) {
|
|
||||||
var arr, i, j, k, len, ref, ref1, shift, value;
|
|
||||||
this.comparator = (options != null ? options.comparator : void 0) || function(a, b) {
|
|
||||||
return a - b;
|
|
||||||
};
|
|
||||||
this.pageSize = (options != null ? options.pageSize : void 0) || 512;
|
|
||||||
this.length = 0;
|
|
||||||
shift = 0;
|
|
||||||
while ((1 << shift) < this.pageSize) {
|
|
||||||
shift += 1;
|
|
||||||
}
|
|
||||||
if (1 << shift !== this.pageSize) {
|
|
||||||
throw 'pageSize must be a power of two';
|
|
||||||
}
|
|
||||||
this._shift = shift;
|
|
||||||
this._emptyMemoryPageTemplate = arr = [];
|
|
||||||
for (i = j = 0, ref = this.pageSize; 0 <= ref ? j < ref : j > ref; i = 0 <= ref ? ++j : --j) {
|
|
||||||
arr.push(null);
|
|
||||||
}
|
|
||||||
this._memory = [];
|
|
||||||
this._mask = this.pageSize - 1;
|
|
||||||
if (options.initialValues) {
|
|
||||||
ref1 = options.initialValues;
|
|
||||||
for (k = 0, len = ref1.length; k < len; k++) {
|
|
||||||
value = ref1[k];
|
|
||||||
this.queue(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BHeapStrategy.prototype.queue = function(value) {
|
|
||||||
this.length += 1;
|
|
||||||
this._write(this.length, value);
|
|
||||||
this._bubbleUp(this.length, value);
|
|
||||||
return void 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
BHeapStrategy.prototype.dequeue = function() {
|
|
||||||
var ret, val;
|
|
||||||
ret = this._read(1);
|
|
||||||
val = this._read(this.length);
|
|
||||||
this.length -= 1;
|
|
||||||
if (this.length > 0) {
|
|
||||||
this._write(1, val);
|
|
||||||
this._bubbleDown(1, val);
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
BHeapStrategy.prototype.peek = function() {
|
|
||||||
return this._read(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
BHeapStrategy.prototype.clear = function() {
|
|
||||||
this.length = 0;
|
|
||||||
this._memory.length = 0;
|
|
||||||
return void 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
BHeapStrategy.prototype._write = function(index, value) {
|
|
||||||
var page;
|
|
||||||
page = index >> this._shift;
|
|
||||||
while (page >= this._memory.length) {
|
|
||||||
this._memory.push(this._emptyMemoryPageTemplate.slice(0));
|
|
||||||
}
|
|
||||||
return this._memory[page][index & this._mask] = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
BHeapStrategy.prototype._read = function(index) {
|
|
||||||
return this._memory[index >> this._shift][index & this._mask];
|
|
||||||
};
|
|
||||||
|
|
||||||
BHeapStrategy.prototype._bubbleUp = function(index, value) {
|
|
||||||
var compare, indexInPage, parentIndex, parentValue;
|
|
||||||
compare = this.comparator;
|
|
||||||
while (index > 1) {
|
|
||||||
indexInPage = index & this._mask;
|
|
||||||
if (index < this.pageSize || indexInPage > 3) {
|
|
||||||
parentIndex = (index & ~this._mask) | (indexInPage >> 1);
|
|
||||||
} else if (indexInPage < 2) {
|
|
||||||
parentIndex = (index - this.pageSize) >> this._shift;
|
|
||||||
parentIndex += parentIndex & ~(this._mask >> 1);
|
|
||||||
parentIndex |= this.pageSize >> 1;
|
|
||||||
} else {
|
|
||||||
parentIndex = index - 2;
|
|
||||||
}
|
|
||||||
parentValue = this._read(parentIndex);
|
|
||||||
if (compare(parentValue, value) < 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
this._write(parentIndex, value);
|
|
||||||
this._write(index, parentValue);
|
|
||||||
index = parentIndex;
|
|
||||||
}
|
|
||||||
return void 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
BHeapStrategy.prototype._bubbleDown = function(index, value) {
|
|
||||||
var childIndex1, childIndex2, childValue1, childValue2, compare;
|
|
||||||
compare = this.comparator;
|
|
||||||
while (index < this.length) {
|
|
||||||
if (index > this._mask && !(index & (this._mask - 1))) {
|
|
||||||
childIndex1 = childIndex2 = index + 2;
|
|
||||||
} else if (index & (this.pageSize >> 1)) {
|
|
||||||
childIndex1 = (index & ~this._mask) >> 1;
|
|
||||||
childIndex1 |= index & (this._mask >> 1);
|
|
||||||
childIndex1 = (childIndex1 + 1) << this._shift;
|
|
||||||
childIndex2 = childIndex1 + 1;
|
|
||||||
} else {
|
|
||||||
childIndex1 = index + (index & this._mask);
|
|
||||||
childIndex2 = childIndex1 + 1;
|
|
||||||
}
|
|
||||||
if (childIndex1 !== childIndex2 && childIndex2 <= this.length) {
|
|
||||||
childValue1 = this._read(childIndex1);
|
|
||||||
childValue2 = this._read(childIndex2);
|
|
||||||
if (compare(childValue1, value) < 0 && compare(childValue1, childValue2) <= 0) {
|
|
||||||
this._write(childIndex1, value);
|
|
||||||
this._write(index, childValue1);
|
|
||||||
index = childIndex1;
|
|
||||||
} else if (compare(childValue2, value) < 0) {
|
|
||||||
this._write(childIndex2, value);
|
|
||||||
this._write(index, childValue2);
|
|
||||||
index = childIndex2;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (childIndex1 <= this.length) {
|
|
||||||
childValue1 = this._read(childIndex1);
|
|
||||||
if (compare(childValue1, value) < 0) {
|
|
||||||
this._write(childIndex1, value);
|
|
||||||
this._write(index, childValue1);
|
|
||||||
index = childIndex1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return void 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
return BHeapStrategy;
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
||||||
|
|
||||||
},{}],5:[function(_dereq_,module,exports){
|
|
||||||
var BinaryHeapStrategy;
|
|
||||||
|
|
||||||
module.exports = BinaryHeapStrategy = (function() {
|
|
||||||
function BinaryHeapStrategy(options) {
|
|
||||||
var ref;
|
|
||||||
this.comparator = (options != null ? options.comparator : void 0) || function(a, b) {
|
|
||||||
return a - b;
|
|
||||||
};
|
|
||||||
this.length = 0;
|
|
||||||
this.data = ((ref = options.initialValues) != null ? ref.slice(0) : void 0) || [];
|
|
||||||
this._heapify();
|
|
||||||
}
|
|
||||||
|
|
||||||
BinaryHeapStrategy.prototype._heapify = function() {
|
|
||||||
var i, j, ref;
|
|
||||||
if (this.data.length > 0) {
|
|
||||||
for (i = j = 1, ref = this.data.length; 1 <= ref ? j < ref : j > ref; i = 1 <= ref ? ++j : --j) {
|
|
||||||
this._bubbleUp(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return void 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
BinaryHeapStrategy.prototype.queue = function(value) {
|
|
||||||
this.data.push(value);
|
|
||||||
this._bubbleUp(this.data.length - 1);
|
|
||||||
return void 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
BinaryHeapStrategy.prototype.dequeue = function() {
|
|
||||||
var last, ret;
|
|
||||||
ret = this.data[0];
|
|
||||||
last = this.data.pop();
|
|
||||||
if (this.data.length > 0) {
|
|
||||||
this.data[0] = last;
|
|
||||||
this._bubbleDown(0);
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
BinaryHeapStrategy.prototype.peek = function() {
|
|
||||||
return this.data[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
BinaryHeapStrategy.prototype.clear = function() {
|
|
||||||
this.length = 0;
|
|
||||||
this.data.length = 0;
|
|
||||||
return void 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
BinaryHeapStrategy.prototype._bubbleUp = function(pos) {
|
|
||||||
var parent, x;
|
|
||||||
while (pos > 0) {
|
|
||||||
parent = (pos - 1) >>> 1;
|
|
||||||
if (this.comparator(this.data[pos], this.data[parent]) < 0) {
|
|
||||||
x = this.data[parent];
|
|
||||||
this.data[parent] = this.data[pos];
|
|
||||||
this.data[pos] = x;
|
|
||||||
pos = parent;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return void 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
BinaryHeapStrategy.prototype._bubbleDown = function(pos) {
|
|
||||||
var last, left, minIndex, right, x;
|
|
||||||
last = this.data.length - 1;
|
|
||||||
while (true) {
|
|
||||||
left = (pos << 1) + 1;
|
|
||||||
right = left + 1;
|
|
||||||
minIndex = pos;
|
|
||||||
if (left <= last && this.comparator(this.data[left], this.data[minIndex]) < 0) {
|
|
||||||
minIndex = left;
|
|
||||||
}
|
|
||||||
if (right <= last && this.comparator(this.data[right], this.data[minIndex]) < 0) {
|
|
||||||
minIndex = right;
|
|
||||||
}
|
|
||||||
if (minIndex !== pos) {
|
|
||||||
x = this.data[minIndex];
|
|
||||||
this.data[minIndex] = this.data[pos];
|
|
||||||
this.data[pos] = x;
|
|
||||||
pos = minIndex;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return void 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
return BinaryHeapStrategy;
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
||||||
|
|
||||||
},{}]},{},[1])(1)
|
|
||||||
});
|
|
||||||
436
libs/quantize.js
|
|
@ -1,436 +0,0 @@
|
||||||
// Forked from color-thief.js Copyright 2011 Lokesh Dhakar under MIT license
|
|
||||||
// var pixelArray = [[190,197,190], [202,204,200], [207,214,210]]; // ... etc;
|
|
||||||
// var cmap = MMCQ.quantize(pixelArray, colorCount);
|
|
||||||
// var palette = cmap ? cmap.palette() : null;
|
|
||||||
|
|
||||||
// Protovis. Copyright 2010 Stanford Visualization Group (http://mbostock.github.com/protovis/)
|
|
||||||
// Licensed under the BSD License: http://www.opensource.org/licenses/bsd-license.php
|
|
||||||
if (!pv) {
|
|
||||||
var pv = {
|
|
||||||
map: function(array, f) {
|
|
||||||
var o = {};
|
|
||||||
return f ? array.map(function(d, i) { o.index = i; return f.call(o, d); }) : array.slice();
|
|
||||||
},
|
|
||||||
naturalOrder: function(a, b) {
|
|
||||||
return (a < b) ? -1 : ((a > b) ? 1 : 0);
|
|
||||||
},
|
|
||||||
sum: function(array, f) {
|
|
||||||
var o = {};
|
|
||||||
return array.reduce(f ? function(p, d, i) { o.index = i; return p + f.call(o, d); } : function(p, d) { return p + d; }, 0);
|
|
||||||
},
|
|
||||||
max: function(array, f) {
|
|
||||||
return Math.max.apply(null, f ? pv.map(array, f) : array);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// MMCQ (Modified median cut quantization). Algorithm from the Leptonica library, modified by Nick Rabinowitz
|
|
||||||
// quantize.js Copyright 2008 Nick Rabinowitz under MIT license
|
|
||||||
var MMCQ = (function() {
|
|
||||||
// private constants
|
|
||||||
var sigbits = 5,
|
|
||||||
rshift = 8 - sigbits,
|
|
||||||
maxIterations = 1000,
|
|
||||||
fractByPopulations = 0.75;
|
|
||||||
|
|
||||||
// get reduced-space color index for a pixel
|
|
||||||
function getColorIndex(r, g, b) {
|
|
||||||
return (r << (2 * sigbits)) + (g << sigbits) + b;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple priority queue
|
|
||||||
function PQueue(comparator) {
|
|
||||||
var contents = [],
|
|
||||||
sorted = false;
|
|
||||||
|
|
||||||
function sort() {
|
|
||||||
contents.sort(comparator);
|
|
||||||
sorted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
push: function(o) {
|
|
||||||
contents.push(o);
|
|
||||||
sorted = false;
|
|
||||||
},
|
|
||||||
peek: function(index) {
|
|
||||||
if (!sorted) sort();
|
|
||||||
if (index===undefined) index = contents.length - 1;
|
|
||||||
return contents[index];
|
|
||||||
},
|
|
||||||
pop: function() {
|
|
||||||
if (!sorted) sort();
|
|
||||||
return contents.pop();
|
|
||||||
},
|
|
||||||
size: function() {
|
|
||||||
return contents.length;
|
|
||||||
},
|
|
||||||
map: function(f) {
|
|
||||||
return contents.map(f);
|
|
||||||
},
|
|
||||||
debug: function() {
|
|
||||||
if (!sorted) sort();
|
|
||||||
return contents;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3d color space box
|
|
||||||
function VBox(r1, r2, g1, g2, b1, b2, histo) {
|
|
||||||
var vbox = this;
|
|
||||||
vbox.r1 = r1;
|
|
||||||
vbox.r2 = r2;
|
|
||||||
vbox.g1 = g1;
|
|
||||||
vbox.g2 = g2;
|
|
||||||
vbox.b1 = b1;
|
|
||||||
vbox.b2 = b2;
|
|
||||||
vbox.histo = histo;
|
|
||||||
}
|
|
||||||
VBox.prototype = {
|
|
||||||
volume: function(force) {
|
|
||||||
var vbox = this;
|
|
||||||
if (!vbox._volume || force) {
|
|
||||||
vbox._volume = ((vbox.r2 - vbox.r1 + 1) * (vbox.g2 - vbox.g1 + 1) * (vbox.b2 - vbox.b1 + 1));
|
|
||||||
}
|
|
||||||
return vbox._volume;
|
|
||||||
},
|
|
||||||
count: function(force) {
|
|
||||||
var vbox = this,
|
|
||||||
histo = vbox.histo;
|
|
||||||
if (!vbox._count_set || force) {
|
|
||||||
var npix = 0,
|
|
||||||
index, i, j, k;
|
|
||||||
for (i = vbox.r1; i <= vbox.r2; i++) {
|
|
||||||
for (j = vbox.g1; j <= vbox.g2; j++) {
|
|
||||||
for (k = vbox.b1; k <= vbox.b2; k++) {
|
|
||||||
index = getColorIndex(i,j,k);
|
|
||||||
npix += (histo[index] || 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
vbox._count = npix;
|
|
||||||
vbox._count_set = true;
|
|
||||||
}
|
|
||||||
return vbox._count;
|
|
||||||
},
|
|
||||||
copy: function() {
|
|
||||||
var vbox = this;
|
|
||||||
return new VBox(vbox.r1, vbox.r2, vbox.g1, vbox.g2, vbox.b1, vbox.b2, vbox.histo);
|
|
||||||
},
|
|
||||||
avg: function(force) {
|
|
||||||
var vbox = this,
|
|
||||||
histo = vbox.histo;
|
|
||||||
if (!vbox._avg || force) {
|
|
||||||
var ntot = 0,
|
|
||||||
mult = 1 << (8 - sigbits),
|
|
||||||
rsum = 0,
|
|
||||||
gsum = 0,
|
|
||||||
bsum = 0,
|
|
||||||
hval,
|
|
||||||
i, j, k, histoindex;
|
|
||||||
for (i = vbox.r1; i <= vbox.r2; i++) {
|
|
||||||
for (j = vbox.g1; j <= vbox.g2; j++) {
|
|
||||||
for (k = vbox.b1; k <= vbox.b2; k++) {
|
|
||||||
histoindex = getColorIndex(i,j,k);
|
|
||||||
hval = histo[histoindex] || 0;
|
|
||||||
ntot += hval;
|
|
||||||
rsum += (hval * (i + 0.5) * mult);
|
|
||||||
gsum += (hval * (j + 0.5) * mult);
|
|
||||||
bsum += (hval * (k + 0.5) * mult);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ntot) {
|
|
||||||
vbox._avg = [~~(rsum/ntot), ~~(gsum/ntot), ~~(bsum/ntot)];
|
|
||||||
} else {
|
|
||||||
// console.log('empty box');
|
|
||||||
vbox._avg = [
|
|
||||||
~~(mult * (vbox.r1 + vbox.r2 + 1) / 2),
|
|
||||||
~~(mult * (vbox.g1 + vbox.g2 + 1) / 2),
|
|
||||||
~~(mult * (vbox.b1 + vbox.b2 + 1) / 2)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return vbox._avg;
|
|
||||||
},
|
|
||||||
contains: function(pixel) {
|
|
||||||
var vbox = this,
|
|
||||||
rval = pixel[0] >> rshift;
|
|
||||||
gval = pixel[1] >> rshift;
|
|
||||||
bval = pixel[2] >> rshift;
|
|
||||||
return (rval >= vbox.r1 && rval <= vbox.r2 &&
|
|
||||||
gval >= vbox.g1 && gval <= vbox.g2 &&
|
|
||||||
bval >= vbox.b1 && bval <= vbox.b2);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Color map
|
|
||||||
function CMap() {
|
|
||||||
this.vboxes = new PQueue(function(a,b) {
|
|
||||||
return pv.naturalOrder(
|
|
||||||
a.vbox.count()*a.vbox.volume(),
|
|
||||||
b.vbox.count()*b.vbox.volume()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
CMap.prototype = {
|
|
||||||
push: function(vbox) {
|
|
||||||
this.vboxes.push({
|
|
||||||
vbox: vbox,
|
|
||||||
color: vbox.avg()
|
|
||||||
});
|
|
||||||
},
|
|
||||||
palette: function() {
|
|
||||||
return this.vboxes.map(function(vb) { return vb.color; });
|
|
||||||
},
|
|
||||||
size: function() {
|
|
||||||
return this.vboxes.size();
|
|
||||||
},
|
|
||||||
map: function(color) {
|
|
||||||
var vboxes = this.vboxes;
|
|
||||||
for (var i=0; i<vboxes.size(); i++) {
|
|
||||||
if (vboxes.peek(i).vbox.contains(color)) {
|
|
||||||
return vboxes.peek(i).color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.nearest(color);
|
|
||||||
},
|
|
||||||
nearest: function(color) {
|
|
||||||
var vboxes = this.vboxes,
|
|
||||||
d1, d2, pColor;
|
|
||||||
for (var i=0; i<vboxes.size(); i++) {
|
|
||||||
d2 = Math.sqrt(
|
|
||||||
Math.pow(color[0] - vboxes.peek(i).color[0], 2) +
|
|
||||||
Math.pow(color[1] - vboxes.peek(i).color[1], 2) +
|
|
||||||
Math.pow(color[2] - vboxes.peek(i).color[2], 2)
|
|
||||||
);
|
|
||||||
if (d2 < d1 || d1 === undefined) {
|
|
||||||
d1 = d2;
|
|
||||||
pColor = vboxes.peek(i).color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pColor;
|
|
||||||
},
|
|
||||||
forcebw: function() {
|
|
||||||
// XXX: won't work yet
|
|
||||||
var vboxes = this.vboxes;
|
|
||||||
vboxes.sort(function(a,b) { return pv.naturalOrder(pv.sum(a.color), pv.sum(b.color));});
|
|
||||||
|
|
||||||
// force darkest color to black if everything < 5
|
|
||||||
var lowest = vboxes[0].color;
|
|
||||||
if (lowest[0] < 5 && lowest[1] < 5 && lowest[2] < 5)
|
|
||||||
vboxes[0].color = [0,0,0];
|
|
||||||
|
|
||||||
// force lightest color to white if everything > 251
|
|
||||||
var idx = vboxes.length-1,
|
|
||||||
highest = vboxes[idx].color;
|
|
||||||
if (highest[0] > 251 && highest[1] > 251 && highest[2] > 251)
|
|
||||||
vboxes[idx].color = [255,255,255];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// histo (1-d array, giving the number of pixels in
|
|
||||||
// each quantized region of color space), or null on error
|
|
||||||
function getHisto(pixels) {
|
|
||||||
var histosize = 1 << (3 * sigbits),
|
|
||||||
histo = new Array(histosize),
|
|
||||||
index, rval, gval, bval;
|
|
||||||
pixels.forEach(function(pixel) {
|
|
||||||
rval = pixel[0] >> rshift;
|
|
||||||
gval = pixel[1] >> rshift;
|
|
||||||
bval = pixel[2] >> rshift;
|
|
||||||
index = getColorIndex(rval, gval, bval);
|
|
||||||
histo[index] = (histo[index] || 0) + 1;
|
|
||||||
});
|
|
||||||
return histo;
|
|
||||||
}
|
|
||||||
|
|
||||||
function vboxFromPixels(pixels, histo) {
|
|
||||||
var rmin=1000000, rmax=0,
|
|
||||||
gmin=1000000, gmax=0,
|
|
||||||
bmin=1000000, bmax=0,
|
|
||||||
rval, gval, bval;
|
|
||||||
// find min/max
|
|
||||||
pixels.forEach(function(pixel) {
|
|
||||||
rval = pixel[0] >> rshift;
|
|
||||||
gval = pixel[1] >> rshift;
|
|
||||||
bval = pixel[2] >> rshift;
|
|
||||||
if (rval < rmin) rmin = rval;
|
|
||||||
else if (rval > rmax) rmax = rval;
|
|
||||||
if (gval < gmin) gmin = gval;
|
|
||||||
else if (gval > gmax) gmax = gval;
|
|
||||||
if (bval < bmin) bmin = bval;
|
|
||||||
else if (bval > bmax) bmax = bval;
|
|
||||||
});
|
|
||||||
return new VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo);
|
|
||||||
}
|
|
||||||
|
|
||||||
function medianCutApply(histo, vbox) {
|
|
||||||
if (!vbox.count()) return;
|
|
||||||
|
|
||||||
var rw = vbox.r2 - vbox.r1 + 1,
|
|
||||||
gw = vbox.g2 - vbox.g1 + 1,
|
|
||||||
bw = vbox.b2 - vbox.b1 + 1,
|
|
||||||
maxw = pv.max([rw, gw, bw]);
|
|
||||||
// only one pixel, no split
|
|
||||||
if (vbox.count() == 1) {
|
|
||||||
return [vbox.copy()];
|
|
||||||
}
|
|
||||||
/* Find the partial sum arrays along the selected axis. */
|
|
||||||
var total = 0,
|
|
||||||
partialsum = [],
|
|
||||||
lookaheadsum = [],
|
|
||||||
i, j, k, sum, index;
|
|
||||||
if (maxw == rw) {
|
|
||||||
for (i = vbox.r1; i <= vbox.r2; i++) {
|
|
||||||
sum = 0;
|
|
||||||
for (j = vbox.g1; j <= vbox.g2; j++) {
|
|
||||||
for (k = vbox.b1; k <= vbox.b2; k++) {
|
|
||||||
index = getColorIndex(i,j,k);
|
|
||||||
sum += (histo[index] || 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
total += sum;
|
|
||||||
partialsum[i] = total;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (maxw == gw) {
|
|
||||||
for (i = vbox.g1; i <= vbox.g2; i++) {
|
|
||||||
sum = 0;
|
|
||||||
for (j = vbox.r1; j <= vbox.r2; j++) {
|
|
||||||
for (k = vbox.b1; k <= vbox.b2; k++) {
|
|
||||||
index = getColorIndex(j,i,k);
|
|
||||||
sum += (histo[index] || 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
total += sum;
|
|
||||||
partialsum[i] = total;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else { /* maxw == bw */
|
|
||||||
for (i = vbox.b1; i <= vbox.b2; i++) {
|
|
||||||
sum = 0;
|
|
||||||
for (j = vbox.r1; j <= vbox.r2; j++) {
|
|
||||||
for (k = vbox.g1; k <= vbox.g2; k++) {
|
|
||||||
index = getColorIndex(j,k,i);
|
|
||||||
sum += (histo[index] || 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
total += sum;
|
|
||||||
partialsum[i] = total;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
partialsum.forEach(function(d,i) {
|
|
||||||
lookaheadsum[i] = total-d;
|
|
||||||
});
|
|
||||||
function doCut(color) {
|
|
||||||
var dim1 = color + '1',
|
|
||||||
dim2 = color + '2',
|
|
||||||
left, right, vbox1, vbox2, d2, count2=0;
|
|
||||||
for (i = vbox[dim1]; i <= vbox[dim2]; i++) {
|
|
||||||
if (partialsum[i] > total / 2) {
|
|
||||||
vbox1 = vbox.copy();
|
|
||||||
vbox2 = vbox.copy();
|
|
||||||
left = i - vbox[dim1];
|
|
||||||
right = vbox[dim2] - i;
|
|
||||||
if (left <= right)
|
|
||||||
d2 = Math.min(vbox[dim2] - 1, ~~(i + right / 2));
|
|
||||||
else d2 = Math.max(vbox[dim1], ~~(i - 1 - left / 2));
|
|
||||||
// avoid 0-count boxes
|
|
||||||
while (!partialsum[d2]) d2++;
|
|
||||||
count2 = lookaheadsum[d2];
|
|
||||||
while (!count2 && partialsum[d2-1]) count2 = lookaheadsum[--d2];
|
|
||||||
// set dimensions
|
|
||||||
vbox1[dim2] = d2;
|
|
||||||
vbox2[dim1] = vbox1[dim2] + 1;
|
|
||||||
// console.log('vbox counts:', vbox.count(), vbox1.count(), vbox2.count());
|
|
||||||
return [vbox1, vbox2];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
// determine the cut planes
|
|
||||||
return maxw == rw ? doCut('r') :
|
|
||||||
maxw == gw ? doCut('g') :
|
|
||||||
doCut('b');
|
|
||||||
}
|
|
||||||
|
|
||||||
function quantize(pixels, maxcolors) {
|
|
||||||
maxcolors++;
|
|
||||||
if (!pixels.length || maxcolors < 2 || maxcolors > 256) {return false;}
|
|
||||||
|
|
||||||
// XXX: check color content and convert to grayscale if insufficient
|
|
||||||
var histo = getHisto(pixels),
|
|
||||||
histosize = 1 << (3 * sigbits);
|
|
||||||
|
|
||||||
// check that we aren't below maxcolors already
|
|
||||||
var nColors = 0;
|
|
||||||
histo.forEach(function() { nColors++; });
|
|
||||||
if (nColors <= maxcolors) {
|
|
||||||
// XXX: generate the new colors from the histo and return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the beginning vbox from the colors
|
|
||||||
var vbox = vboxFromPixels(pixels, histo),
|
|
||||||
pq = new PQueue(function(a,b) { return pv.naturalOrder(a.count(), b.count()); });
|
|
||||||
pq.push(vbox);
|
|
||||||
|
|
||||||
// inner function to do the iteration
|
|
||||||
function iter(lh, target) {
|
|
||||||
var ncolors = 1,
|
|
||||||
niters = 0,
|
|
||||||
vbox;
|
|
||||||
while (niters < maxIterations) {
|
|
||||||
vbox = lh.pop();
|
|
||||||
if (!vbox.count()) { /* just put it back */
|
|
||||||
lh.push(vbox);
|
|
||||||
niters++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// do the cut
|
|
||||||
var vboxes = medianCutApply(histo, vbox),
|
|
||||||
vbox1 = vboxes[0],
|
|
||||||
vbox2 = vboxes[1];
|
|
||||||
|
|
||||||
if (!vbox1) {
|
|
||||||
// console.log("vbox1 not defined; shouldn't happen!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lh.push(vbox1);
|
|
||||||
if (vbox2) { /* vbox2 can be null */
|
|
||||||
lh.push(vbox2);
|
|
||||||
ncolors++;
|
|
||||||
}
|
|
||||||
if (ncolors >= target) return;
|
|
||||||
if (niters++ > maxIterations) {
|
|
||||||
// console.log("infinite loop; perhaps too few pixels!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// first set of colors, sorted by population
|
|
||||||
iter(pq, fractByPopulations * maxcolors);
|
|
||||||
|
|
||||||
// Re-sort by the product of pixel occupancy times the size in color space.
|
|
||||||
var pq2 = new PQueue(function(a,b) {
|
|
||||||
return pv.naturalOrder(a.count()*a.volume(), b.count()*b.volume());
|
|
||||||
});
|
|
||||||
while (pq.size()) {
|
|
||||||
pq2.push(pq.pop());
|
|
||||||
}
|
|
||||||
|
|
||||||
// next set - generate the median cuts using the (npix * vol) sorting.
|
|
||||||
iter(pq2, maxcolors - pq2.size());
|
|
||||||
|
|
||||||
// calculate the actual colors
|
|
||||||
var cmap = new CMap();
|
|
||||||
while (pq2.size()) {cmap.push(pq2.pop());}
|
|
||||||
|
|
||||||
return cmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
quantize: quantize
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
486
modules/burgs-and-states.js
Normal file
|
|
@ -0,0 +1,486 @@
|
||||||
|
(function (global, factory) {
|
||||||
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||||
|
typeof define === 'function' && define.amd ? define(factory) :
|
||||||
|
(global.BurgsAndStates = factory());
|
||||||
|
}(this, (function () { 'use strict';
|
||||||
|
|
||||||
|
const generate = function() {
|
||||||
|
console.time("generateBurgsAndStates");
|
||||||
|
|
||||||
|
const cells = pack.cells, vertices = pack.vertices, features = pack.features, cultures = pack.cultures, n = cells.i.length;
|
||||||
|
|
||||||
|
cells.burg = new Uint16Array(n); // cell burg
|
||||||
|
cells.road = new Uint16Array(n); // cell road power
|
||||||
|
|
||||||
|
const burgs = pack.burgs = placeCapitals();
|
||||||
|
pack.states = createStates();
|
||||||
|
const capitalRoutes = Routes.getRoads();
|
||||||
|
|
||||||
|
placeTowns();
|
||||||
|
const townRoutes = Routes.getTrails();
|
||||||
|
specifyBurgs();
|
||||||
|
const oceanRoutes = Routes.getSearoutes();
|
||||||
|
|
||||||
|
expandStates();
|
||||||
|
normalizeStates();
|
||||||
|
|
||||||
|
Routes.draw(capitalRoutes, townRoutes, oceanRoutes);
|
||||||
|
drawBurgsWithLabels();
|
||||||
|
|
||||||
|
function placeCapitals() {
|
||||||
|
console.time('placeCapitals');
|
||||||
|
let count = +regionsInput.value;
|
||||||
|
let burgs = [0];
|
||||||
|
|
||||||
|
const score = new Int16Array(cells.s.map(s => s * Math.random())); // cell score for capitals placement
|
||||||
|
const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
|
||||||
|
|
||||||
|
if (sorted.length < count * 10) {
|
||||||
|
count = Math.floor(sorted.length / 10);
|
||||||
|
if (!count) {
|
||||||
|
console.error(`There is no populated cells. Cannot generate states`);
|
||||||
|
return burgs;
|
||||||
|
} else {
|
||||||
|
console.error(`Not enought populated cells (${sorted.length}). Will generate only ${count} states`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let burgsTree = d3.quadtree();
|
||||||
|
let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals
|
||||||
|
|
||||||
|
for (let i = 0; burgs.length <= count; i++) {
|
||||||
|
const cell = sorted[i], x = cells.p[cell][0], y = cells.p[cell][1];
|
||||||
|
|
||||||
|
if (burgsTree.find(x, y, spacing) === undefined) {
|
||||||
|
burgs.push({cell, x, y});
|
||||||
|
burgsTree.add([x, y]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i === sorted.length - 1) {
|
||||||
|
console.error("Cannot place capitals with current spacing. Trying again with reduced spacing");
|
||||||
|
burgsTree = d3.quadtree();
|
||||||
|
i = -1, burgs = [0], spacing /= 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
burgs[0] = burgsTree;
|
||||||
|
console.timeEnd('placeCapitals');
|
||||||
|
return burgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each capital create a state
|
||||||
|
function createStates() {
|
||||||
|
console.time('createStates');
|
||||||
|
const states = [{i:0, name: "Neutrals"}];
|
||||||
|
const colors = getColors(burgs.length-1);
|
||||||
|
|
||||||
|
burgs.forEach(function(b, i) {
|
||||||
|
if (!i) return; // skip first element
|
||||||
|
|
||||||
|
// burgs data
|
||||||
|
b.i = b.state = i;
|
||||||
|
b.culture = cells.culture[b.cell];
|
||||||
|
const base = cultures[b.culture].base;
|
||||||
|
const min = nameBases[base].min-1;
|
||||||
|
const max = Math.max(nameBases[base].max-2, min);
|
||||||
|
b.name = Names.getCulture(b.culture, min, max, "", 0);
|
||||||
|
b.feature = cells.f[b.cell];
|
||||||
|
b.capital = true;
|
||||||
|
|
||||||
|
// states data
|
||||||
|
const expansionism = rn(Math.random() * powerInput.value / 2 + 1, 1);
|
||||||
|
const basename = b.name.length < 9 && b.cell%5 === 0 ? b.name : Names.getCulture(b.culture, min, 6, "", 0);
|
||||||
|
const name = Names.getState(basename, b.culture);
|
||||||
|
const type = cultures[b.culture].type;
|
||||||
|
states.push({i, color: colors[i-1], name, expansionism, capital: i, type, center: b.cell, culture: b.culture});
|
||||||
|
cells.burg[b.cell] = i;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.timeEnd('createStates');
|
||||||
|
return states;
|
||||||
|
}
|
||||||
|
|
||||||
|
// place secondary settlements based on geo and economical evaluation
|
||||||
|
function placeTowns() {
|
||||||
|
console.time('placeTowns');
|
||||||
|
const score = new Int16Array(cells.s.map(s => s * Math.random())); // cell score for towns placement
|
||||||
|
const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
|
||||||
|
|
||||||
|
// burgs number depends on ratio between populated and all cells and burgsDensity input (expected mean ~300))
|
||||||
|
const burgsCount = rn(sorted.length / grid.points.length * manorsInput.value * 1000);
|
||||||
|
const spacing = (graphWidth + graphHeight) * 9 / burgsCount; // base min distance between towns
|
||||||
|
const burgsTree = burgs[0];
|
||||||
|
|
||||||
|
for (let i = 0; burgs.length <= burgsCount && i < sorted.length; i++) {
|
||||||
|
const id = sorted[i], x = cells.p[id][0], y = cells.p[id][1];
|
||||||
|
const s = spacing * Math.random() + 0.5; // randomize to make the placement not uniform
|
||||||
|
if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
|
||||||
|
const burg = burgs.length;
|
||||||
|
const culture = cells.culture[id];
|
||||||
|
const name = Names.getCulture(culture);
|
||||||
|
const feature = cells.f[id];
|
||||||
|
burgs.push({cell: id, x, y, state: 0, i: burg, culture, name, capital: false, feature});
|
||||||
|
burgsTree.add([x, y]);
|
||||||
|
cells.burg[id] = burg;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (burgs.length <= burgsCount) console.error(`Cannot place all burgs. Requested ${burgsCount}, placed ${burgs.length-1}`);
|
||||||
|
|
||||||
|
//const min = d3.min(score.filter(s => s)), max = d3.max(score);
|
||||||
|
//terrs.selectAll("polygon").data(sorted).enter().append("polygon").attr("points", d => getPackPolygon(d)).attr("fill", d => color(1 - normalize(score[d], min, max)));
|
||||||
|
//labels.selectAll("text").data(sorted).enter().append("text").attr("x", d => cells.p[d][0]).attr("y", d => cells.p[d][1]).text(d => score[d]).attr("font-size", 2);
|
||||||
|
|
||||||
|
burgs[0] = {name:undefined};
|
||||||
|
console.timeEnd('placeTowns');
|
||||||
|
}
|
||||||
|
|
||||||
|
// define burg coordinates and define details
|
||||||
|
function specifyBurgs() {
|
||||||
|
console.time("specifyBurgs");
|
||||||
|
|
||||||
|
for (const b of burgs) {
|
||||||
|
if (!b.i) continue;
|
||||||
|
const i = b.cell;
|
||||||
|
|
||||||
|
// asign port status: capital with any harbor and towns with good harbors
|
||||||
|
const port = (b.capital && cells.harbor[i]) || cells.harbor[i] === 1;
|
||||||
|
b.port = port ? cells.f[cells.haven[i]] : 0; // port is defined by feature id it lays on
|
||||||
|
|
||||||
|
// define burg population (keep urbanization at about 10% rate)
|
||||||
|
b.population = rn(Math.max((cells.s[i] + cells.road[i]) / 3 + b.i / 1000 + i % 100 / 1000, .1), 3);
|
||||||
|
if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
|
||||||
|
|
||||||
|
if (port) {
|
||||||
|
b.population = rn(b.population * 1.3, 3); // increase port population
|
||||||
|
const e = cells.v[i].filter(v => vertices.c[v].some(c => c === cells.haven[i])); // vertices of common edge
|
||||||
|
b.x = rn((vertices.p[e[0]][0] + vertices.p[e[1]][0]) / 2, 2);
|
||||||
|
b.y = rn((vertices.p[e[0]][1] + vertices.p[e[1]][1]) / 2, 2);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// shift burgs on rivers semi-randomly and just a bit
|
||||||
|
if (cells.r[i]) {
|
||||||
|
const shift = Math.min(cells.fl[i]/150, 1);
|
||||||
|
if (i%2) b.x = rn(b.x + shift, 2); else b.x = rn(b.x - shift, 2);
|
||||||
|
if (cells.r[i]%2) b.y = rn(b.y + shift, 2); else b.y = rn(b.y - shift, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// de-assign port status if it's the only one on feature
|
||||||
|
for (const f of features) {
|
||||||
|
if (!f.i || f.land) continue;
|
||||||
|
const onFeature = burgs.filter(b => b.port === f.i);
|
||||||
|
if (onFeature.length === 1) {
|
||||||
|
onFeature[0].port = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.timeEnd("specifyBurgs");
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawBurgsWithLabels() {
|
||||||
|
console.time("drawBurgs");
|
||||||
|
|
||||||
|
// remove old data
|
||||||
|
burgIcons.selectAll("circle").remove();
|
||||||
|
burgLabels.selectAll("text").remove();
|
||||||
|
icons.selectAll("use").remove();
|
||||||
|
|
||||||
|
// capitals
|
||||||
|
const capitals = burgs.filter(b => b.capital);
|
||||||
|
const capitalIcons = burgIcons.select("#cities");
|
||||||
|
const capitalLabels = burgLabels.select("#cities");
|
||||||
|
const capitalSize = capitalIcons.attr("size") || 1;
|
||||||
|
const capitalAnchors = anchors.selectAll("#cities");
|
||||||
|
const caSize = capitalAnchors.attr("size") || 2;
|
||||||
|
|
||||||
|
capitalIcons.selectAll("circle").data(capitals).enter()
|
||||||
|
.append("circle").attr("id", d => "burg"+d.i).attr("data-id", d => d.i)
|
||||||
|
.attr("cx", d => d.x).attr("cy", d => d.y).attr("r", capitalSize);
|
||||||
|
|
||||||
|
capitalLabels.selectAll("text").data(capitals).enter()
|
||||||
|
.append("text").attr("id", d => "burgLabel"+d.i).attr("data-id", d => d.i)
|
||||||
|
.attr("x", d => d.x).attr("y", d => d.y).attr("dy", `${capitalSize * -1.5}px`).text(d => d.name);
|
||||||
|
|
||||||
|
capitalAnchors.selectAll("use").data(capitals.filter(c => c.port)).enter()
|
||||||
|
.append("use").attr("xlink:href", "#icon-anchor").attr("data-id", d => d.i)
|
||||||
|
.attr("x", d => rn(d.x - caSize * .47, 2)).attr("y", d => rn(d.y - caSize * .47, 2))
|
||||||
|
.attr("width", caSize).attr("height", caSize);
|
||||||
|
|
||||||
|
// towns
|
||||||
|
const towns = burgs.filter(b => b.capital === false);
|
||||||
|
const townIcons = burgIcons.select("#towns");
|
||||||
|
const townLabels = burgLabels.select("#towns");
|
||||||
|
const townSize = townIcons.attr("size") || 0.5;
|
||||||
|
const townsAnchors = anchors.selectAll("#towns");
|
||||||
|
const taSize = townsAnchors.attr("size") || 1;
|
||||||
|
|
||||||
|
townIcons.selectAll("circle").data(towns).enter()
|
||||||
|
.append("circle").attr("id", d => "burg"+d.i).attr("data-id", d => d.i)
|
||||||
|
.attr("cx", d => d.x).attr("cy", d => d.y).attr("r", townSize);
|
||||||
|
|
||||||
|
townLabels.selectAll("text").data(towns).enter()
|
||||||
|
.append("text").attr("id", d => "burgLabel"+d.i).attr("data-id", d => d.i)
|
||||||
|
.attr("x", d => d.x).attr("y", d => d.y).attr("dy", `${townSize * -1.5}px`).text(d => d.name);
|
||||||
|
|
||||||
|
townsAnchors.selectAll("use").data(towns.filter(c => c.port)).enter()
|
||||||
|
.append("use").attr("xlink:href", "#icon-anchor").attr("data-id", d => d.i)
|
||||||
|
.attr("x", d => rn(d.x - taSize * .47, 2)).attr("y", d => rn(d.y - taSize * .47, 2))
|
||||||
|
.attr("width", taSize).attr("height", taSize);
|
||||||
|
|
||||||
|
console.timeEnd("drawBurgs");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.timeEnd("generateBurgsAndStates");
|
||||||
|
}
|
||||||
|
|
||||||
|
// growth algorithm to assign cells to states like we did for cultures
|
||||||
|
const expandStates = function() {
|
||||||
|
console.time("expandStates");
|
||||||
|
const cells = pack.cells, states = pack.states, cultures = pack.cultures, burgs = pack.burgs;
|
||||||
|
|
||||||
|
cells.state = new Uint8Array(cells.i.length); // cell state
|
||||||
|
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||||
|
const cost = [];
|
||||||
|
states.filter(s => s.i && !s.removed).forEach(function(s) {
|
||||||
|
cells.state[burgs[s.capital].cell] = s.i;
|
||||||
|
const b = cells.biome[cultures[s.culture].center]; // native biome
|
||||||
|
queue.queue({e:s.center, p:0, s:s.i, b});
|
||||||
|
cost[s.center] = 1;
|
||||||
|
});
|
||||||
|
const neutral = cells.i.length / 5000 * 2000 * neutralInput.value * statesNeutral.value; // limit cost for state growth
|
||||||
|
|
||||||
|
while (queue.length) {
|
||||||
|
const next = queue.dequeue(), n = next.e, p = next.p, s = next.s, b = next.b;
|
||||||
|
const type = states[s].type;
|
||||||
|
|
||||||
|
cells.c[n].forEach(function(e) {
|
||||||
|
const biome = cells.biome[e];
|
||||||
|
const cultureCost = states[s].culture === cells.culture[e] ? 10 : 100;
|
||||||
|
const biomeCost = getBiomeCost(cells.road[e], b, biome, type);
|
||||||
|
const heightCost = getHeightCost(cells.h[e], type);
|
||||||
|
const riverCost = getRiverCost(cells.r[e], e, type);
|
||||||
|
const typeCost = getTypeCost(cells.t[e], type);
|
||||||
|
const totalCost = p + (cultureCost + biomeCost + heightCost + riverCost + typeCost) / states[s].expansionism;
|
||||||
|
|
||||||
|
if (totalCost > neutral) return;
|
||||||
|
|
||||||
|
if (!cost[e] || totalCost < cost[e]) {
|
||||||
|
if (cells.h[e] >= 20) {
|
||||||
|
cells.state[e] = s; // assign state to cell
|
||||||
|
if (cells.burg[e]) burgs[cells.burg[e]].state = s;
|
||||||
|
}
|
||||||
|
cost[e] = totalCost;
|
||||||
|
queue.queue({e, p:totalCost, s, b});
|
||||||
|
|
||||||
|
//const points = [cells.p[n][0], cells.p[n][1], (cells.p[n][0]+cells.p[e][0])/2, (cells.p[n][1]+cells.p[e][1])/2, cells.p[e][0], cells.p[e][1]];
|
||||||
|
//debug.append("text").attr("x", (cells.p[n][0]+cells.p[e][0])/2 - 1).attr("y", (cells.p[n][1]+cells.p[e][1])/2 - 1).text(rn(totalCost-p)).attr("font-size", .8);
|
||||||
|
//debug.append("polyline").attr("points", points).attr("marker-mid", "url(#arrow)").attr("opacity", .6);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//debug.selectAll(".text").data(cost).enter().append("text").attr("x", (d, e) => cells.p[e][0]-1).attr("y", (d, e) => cells.p[e][1]-1).text(d => d ? rn(d) : "").attr("font-size", 2);
|
||||||
|
|
||||||
|
function getBiomeCost(r, b, biome, type) {
|
||||||
|
if (r > 5) return 0; // no penalty if there is a road;
|
||||||
|
if (b === biome) return 10; // tiny penalty for native biome
|
||||||
|
if (type === "Hunting") return biomesData.cost[biome] * 2; // non-native biome penalty for hunters
|
||||||
|
if (type === "Nomadic" && biome > 4 && biome < 10) return biomesData.cost[biome] * 3; // forest biome penalty for nomads
|
||||||
|
return biomesData.cost[biome]; // general non-native biome penalty
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHeightCost(h, type) {
|
||||||
|
if ((type === "Naval" || type === "Lake") && h < 20) return 200; // low sea crossing penalty for Navals
|
||||||
|
if (type === "Nomadic" && h < 20) return 10000; // giant sea crossing penalty for Navals
|
||||||
|
if (h < 20) return 1000; // general sea crossing penalty
|
||||||
|
if (type === "Highland" && h < 50) return 30; // penalty for highlanders on lowlands
|
||||||
|
if (type === "Highland") return 0; // no penalty for highlanders on highlands
|
||||||
|
if (h >= 70) return 100; // general mountains crossing penalty
|
||||||
|
if (h >= 50) return 30; // general hills crossing penalty
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRiverCost(r, i, type) {
|
||||||
|
if (type === "River") return r ? 0 : 50; // penalty for river cultures
|
||||||
|
if (!r) return 0; // no penalty for others if there is no river
|
||||||
|
return Math.min(Math.max(cells.fl[i] / 10, 20), 100) // river penalty from 20 to 100 based on flux
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTypeCost(ctype, type) {
|
||||||
|
if (ctype === 1) return type === "Naval" || type === "Lake" ? 0 : type === "Nomadic" ? 60 : 20; // penalty for coastline
|
||||||
|
if (ctype === 2) return type === "Naval" || type === "Nomadic" ? 30 : 0; // low penalty for land level 2 for Navals and nomads
|
||||||
|
if (ctype !== -1) return type === "Naval" || type === "Lake" ? 100 : 0; // penalty for mainland for navals
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.timeEnd("expandStates");
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeStates = function() {
|
||||||
|
console.time("normalizeStates");
|
||||||
|
const cells = pack.cells;
|
||||||
|
const burgs = pack.burgs;
|
||||||
|
|
||||||
|
for (const i of cells.i) {
|
||||||
|
if (cells.h[i] < 20) continue;
|
||||||
|
const adversaries = cells.c[i].filter(c => cells.h[c] >= 20 && cells.state[c] !== cells.state[i]);
|
||||||
|
const buddies = cells.c[i].filter(c => cells.h[c] >= 20 && cells.state[c] === cells.state[i]);
|
||||||
|
if (adversaries.length <= buddies.length) continue;
|
||||||
|
if (cells.c[i].some(c => burgs[cells.burg[c]].capital)) continue; // do not overwrite near capital
|
||||||
|
if (burgs[cells.burg[i]].capital) continue; // do not overwrite capital
|
||||||
|
const newState = cells.state[adversaries[0]];
|
||||||
|
cells.state[i] = newState;
|
||||||
|
if (cells.burg[i]) burgs[cells.burg[i]].state = newState;
|
||||||
|
}
|
||||||
|
console.timeEnd("normalizeStates");
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate and draw curved state labels
|
||||||
|
const drawStateLabels = function() {
|
||||||
|
console.time("drawStateLabels");
|
||||||
|
const cells = pack.cells, features = pack.features, states = pack.states;
|
||||||
|
const paths = []; // text paths
|
||||||
|
lineGen.curve(d3.curveBundle.beta(1));
|
||||||
|
|
||||||
|
for (const s of states) {
|
||||||
|
if (!s.i || s.removed) continue;
|
||||||
|
const used = [];
|
||||||
|
const hull = getHull(s.center, s.i);
|
||||||
|
const points = [...hull].map(v => pack.vertices.p[v]);
|
||||||
|
|
||||||
|
//const poly = polylabel([points], 1.0); // pole of inaccessibility
|
||||||
|
//debug.append("circle").attr("r", 3).attr("cx", poly[0]).attr("cy", poly[1]);
|
||||||
|
|
||||||
|
const delaunay = Delaunator.from(points);
|
||||||
|
const voronoi = Voronoi(delaunay, points, points.length);
|
||||||
|
const c = voronoi.vertices;
|
||||||
|
const chain = connectCenters(c, s.i);
|
||||||
|
const relaxed = chain.map(i => c.p[i]).filter((p, i) => i%8 === 0 || i+1 === chain.length);
|
||||||
|
paths.push([s.i, relaxed]);
|
||||||
|
|
||||||
|
// if (s.i == 13) debug.selectAll(".circle").data(points).enter().append("circle").attr("cx", d => d[0]).attr("cy", d => d[1]).attr("r", .5).attr("fill", "red");
|
||||||
|
// if (s.i == 13) d3.select("#cells").selectAll(".polygon").data(d3.range(voronoi.cells.v.length)).enter().append("polygon").attr("points", d => voronoi.cells.v[d] ? voronoi.cells.v[d].map(v => c.p[v]) : "");
|
||||||
|
// if (s.i == 13) debug.append("path").attr("d", round(lineGen(relaxed))).attr("fill", "none").attr("stroke", "blue").attr("stroke-width", .5);
|
||||||
|
// if (s.i == 13) debug.selectAll(".circle").data(chain).enter().append("circle").attr("cx", d => c.p[d][0]).attr("cy", d => c.p[d][1]).attr("r", 1);
|
||||||
|
|
||||||
|
function getHull(start, state) {
|
||||||
|
const queue = [start], hull = new Set();
|
||||||
|
|
||||||
|
while (queue.length) {
|
||||||
|
const q = queue.pop();
|
||||||
|
const nQ = cells.c[q].filter(c => cells.state[c] === state);
|
||||||
|
cells.c[q].forEach(function(c, d) {
|
||||||
|
if (features[cells.f[c]].type === "lake" && features[cells.f[c]].cells < 10) return; // ignore small lakes
|
||||||
|
if (cells.b[c]) {hull.add(cells.v[q][d]); return;}
|
||||||
|
if (cells.state[c] !== state) {hull.add(cells.v[q][d]); return;}
|
||||||
|
const nC = cells.c[c].filter(n => cells.state[n] === state);
|
||||||
|
const intersected = intersect(nQ, nC).length
|
||||||
|
if (hull.size > 20 && !intersected) {hull.add(cells.v[q][d]); return;}
|
||||||
|
if (used[c]) return;
|
||||||
|
used[c] = 1;
|
||||||
|
queue.push(c);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return hull;
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectCenters(c, state) {
|
||||||
|
// check if vertex is inside the area
|
||||||
|
const inside = c.p.map(function(p) {
|
||||||
|
if (p[0] <= 0 || p[1] <= 0 || p[0] >= graphWidth || p[1] >= graphHeight) return false; // out of the screen
|
||||||
|
return used[findCell(p[0], p[1])];
|
||||||
|
});
|
||||||
|
//if (state == 13) debug.selectAll(".circle").data(c.p).enter().append("circle").attr("cx", d => d[0]).attr("cy", d => d[1]).attr("r", .5).attr("fill", (d, i) => inside[i] ? "green" : "blue");
|
||||||
|
|
||||||
|
const sorted = d3.range(c.p.length).filter(i => inside[i]).sort((a, b) => c.p[a][0] - c.p[b][0]);
|
||||||
|
const left = sorted[0] || 0, right = sorted.pop() || 0;
|
||||||
|
|
||||||
|
// connect leftmost and rightmost points with shortest path
|
||||||
|
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||||
|
const cost = [], from = [];
|
||||||
|
queue.queue({e: right, p: 0});
|
||||||
|
|
||||||
|
while (queue.length) {
|
||||||
|
const next = queue.dequeue(), n = next.e, p = next.p;
|
||||||
|
if (n === left) break;
|
||||||
|
|
||||||
|
for (const v of c.v[n]) {
|
||||||
|
if (v === -1) continue;
|
||||||
|
const totalCost = p + (inside[v] ? 1 : 100);
|
||||||
|
if (from[v] || totalCost >= cost[v]) continue;
|
||||||
|
cost[v] = totalCost;
|
||||||
|
from[v] = n;
|
||||||
|
queue.queue({e: v, p: totalCost});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// restore path
|
||||||
|
const chain = [left];
|
||||||
|
let cur = left;
|
||||||
|
while (cur !== right) {
|
||||||
|
cur = from[cur];
|
||||||
|
if (inside[cur]) chain.push(cur);
|
||||||
|
}
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void function drawLabels() {
|
||||||
|
const g = labels.select("#states"), p = defs.select("#textPaths");
|
||||||
|
g.selectAll("text").remove();
|
||||||
|
p.selectAll("path").remove();
|
||||||
|
|
||||||
|
const data = paths.map(p => [round(lineGen(p[1])), "stateLabel"+p[0], states[p[0]].name, p[1]]);
|
||||||
|
p.selectAll("path").data(data).enter().append("path").attr("d", d => d[0]).attr("id", d => "textPath_"+d[1]);
|
||||||
|
|
||||||
|
g.selectAll("text").data(data).enter()
|
||||||
|
.append("text").attr("id", d => d[1])
|
||||||
|
.append("textPath").attr("xlink:href", d => "#textPath_"+d[1])
|
||||||
|
.attr("startOffset", "50%").text(d => d[2]);
|
||||||
|
|
||||||
|
// resize label based on its length
|
||||||
|
g.selectAll("text").each(function(e) {
|
||||||
|
const textPath = document.getElementById("textPath_"+e[1])
|
||||||
|
const pathLength = textPath.getTotalLength();
|
||||||
|
|
||||||
|
// if area is too small to get a path and length is 0
|
||||||
|
if (pathLength === 0) {
|
||||||
|
const x = e[3][0][0], y = e[3][0][1];
|
||||||
|
textPath.setAttribute("d", `M${x-50},${y}h${100}`);
|
||||||
|
this.firstChild.setAttribute("font-size", "60%");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const copy = g.append("text").text(this.textContent);
|
||||||
|
const textLength = copy.node().getComputedTextLength();
|
||||||
|
copy.remove();
|
||||||
|
|
||||||
|
const size = Math.max(Math.min(rn(pathLength / textLength * 60), 175), 60);
|
||||||
|
this.firstChild.setAttribute("font-size", size+"%");
|
||||||
|
|
||||||
|
// prolongate textPath to not trim labels
|
||||||
|
if (pathLength < 100) {
|
||||||
|
const mod = 25 / pathLength;
|
||||||
|
const points = e[3];
|
||||||
|
const f = points[0], l = points[points.length-1];
|
||||||
|
const dx = l[0] - f[0], dy = l[1] - f[1];
|
||||||
|
points[0] = [rn(f[0] - dx * mod), rn(f[1] - dy * mod)];
|
||||||
|
points[points.length-1] = [rn(l[0] + dx * mod), rn(l[1] + dy * mod)];
|
||||||
|
textPath.setAttribute("d", round(lineGen(points)));
|
||||||
|
//debug.append("path").attr("d", round(lineGen(points))).attr("fill", "none").attr("stroke", "red");
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}()
|
||||||
|
|
||||||
|
console.timeEnd("drawStateLabels");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {generate, expandStates, normalizeStates, drawStateLabels};
|
||||||
|
|
||||||
|
})));
|
||||||
210
modules/cultures-generator.js
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
(function (global, factory) {
|
||||||
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||||
|
typeof define === 'function' && define.amd ? define(factory) :
|
||||||
|
(global.Cultures = factory());
|
||||||
|
}(this, (function () {'use strict';
|
||||||
|
|
||||||
|
let cells;
|
||||||
|
|
||||||
|
const generate = function() {
|
||||||
|
console.time('generateCultures');
|
||||||
|
cells = pack.cells;
|
||||||
|
cells.culture = new Int8Array(cells.i.length); // cell cultures
|
||||||
|
let count = +culturesInput.value;
|
||||||
|
|
||||||
|
const populated = cells.i.filter(i => cells.s[i]).sort((a, b) => cells.s[b] - cells.s[a]); // cells sorted by population
|
||||||
|
if (populated.length < count * 25) {
|
||||||
|
count = Math.floor(populated.length / 50);
|
||||||
|
if (!count) {
|
||||||
|
console.error(`There is no populated cells. Cannot generate cultures`);
|
||||||
|
pack.cultures = [{name:"Wildlands", i:0, base:1}];
|
||||||
|
alertMessage.innerHTML = `
|
||||||
|
The climate is harsh and people cannot live in this world.<br>
|
||||||
|
No cultures, states and burgs will be created.<br>
|
||||||
|
Please consider changing the World Configurator settings`;
|
||||||
|
$("#alert").dialog({resizable: false, title: "Extreme climate warning",
|
||||||
|
buttons: {Ok: function() {$(this).dialog("close");}}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
console.error(`Not enought populated cells (${populated.length}). Will generate only ${count} cultures`);
|
||||||
|
alertMessage.innerHTML = `
|
||||||
|
There is only ${populated.length} populated cells and it's insufficient livable area.<br>
|
||||||
|
Only ${count} out of ${culturesInput.value} requiested cultures will be generated.<br>
|
||||||
|
Please consider changing the World Configurator settings`;
|
||||||
|
$("#alert").dialog({resizable: false, title: "Extreme climate warning",
|
||||||
|
buttons: {Ok: function() {$(this).dialog("close");}}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pack.cultures = d3.shuffle(getDefault()).slice(0, count);
|
||||||
|
const centers = d3.quadtree();
|
||||||
|
const colors = getColors(count);
|
||||||
|
|
||||||
|
pack.cultures.forEach(function(culture, i) {
|
||||||
|
const c = culture.center = placeCultureCenter();
|
||||||
|
centers.add(cells.p[c]);
|
||||||
|
culture.i = i+1;
|
||||||
|
culture.color = colors[i];
|
||||||
|
culture.type = defineCultureType(c);
|
||||||
|
culture.expansionism = defineCultureExpansionism(culture.type);
|
||||||
|
cells.culture[c] = i+1;
|
||||||
|
//debug.append("text").attr("stroke", "#000").attr("font-size", "10").attr("font-family", "Almendra SC").attr("x", cells.p[c][0]).attr("y", cells.p[c][1]).text(culture.type);
|
||||||
|
});
|
||||||
|
|
||||||
|
// the first culture with id 0 is for wildlands
|
||||||
|
pack.cultures.unshift({name:"Wildlands", i:0, base:1});
|
||||||
|
|
||||||
|
// check whether all bases are valid. If not, load default namesbase
|
||||||
|
const invalidBase = pack.cultures.some(c => !nameBase[c.base]);
|
||||||
|
if (invalidBase) applyDefaultNamesData();
|
||||||
|
|
||||||
|
// culture center tends to be placed in a density populated cell
|
||||||
|
function placeCultureCenter() {
|
||||||
|
let center, spacing = (graphWidth + graphHeight) / count;
|
||||||
|
do {
|
||||||
|
center = populated[biased(0, populated.length-1, 3)];
|
||||||
|
spacing = spacing * .8;
|
||||||
|
}
|
||||||
|
while (centers.find(cells.p[center][0], cells.p[center][1], spacing) !== undefined);
|
||||||
|
return center;
|
||||||
|
}
|
||||||
|
|
||||||
|
function defineCultureType(i) {
|
||||||
|
if (cells.h[i] > 50) return "Highland"; // no penalty for hills and moutains, high for other elevations
|
||||||
|
const f = cells.f[cells.haven[i]];
|
||||||
|
if (pack.features[f].type === "lake" && pack.features[f].cells > 5) return "Lake" // low water cross penalty and high for non-along-coastline growth
|
||||||
|
if (cells.harbor[i] === 1) return "Naval"; // low water cross penalty and high for non-along-coastline growth
|
||||||
|
if (cells.r[i] && cells.fl[i] > 100) return "River"; // no River cross penalty, penalty for non-River growth
|
||||||
|
const b = cells.biome[i];
|
||||||
|
if (b === 4 || b === 1 || b === 2) return "Nomadic"; // high penalty in forest biomes and near coastline
|
||||||
|
if (b === 3 || b === 9 || b === 10) return "Hunting"; // high penalty in non-native biomes
|
||||||
|
return "Generic";
|
||||||
|
}
|
||||||
|
|
||||||
|
function defineCultureExpansionism(type) {
|
||||||
|
let base = 1; // Generic
|
||||||
|
if (type === "Lake") base = .8; else
|
||||||
|
if (type === "Naval") base = 1.5; else
|
||||||
|
if (type === "River") base = .9; else
|
||||||
|
if (type === "Nomadic") base = 1.8; else
|
||||||
|
if (type === "Hunting") base = .7; else
|
||||||
|
if (type === "Highland") base = .5;
|
||||||
|
return rn((Math.random() * powerInput.value / 2 + 1) * base, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.timeEnd('generateCultures');
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDefault = function() {
|
||||||
|
return [
|
||||||
|
{name:"Shwazen", base:0},
|
||||||
|
{name:"Angshire", base:1},
|
||||||
|
{name:"Luari", base:2},
|
||||||
|
{name:"Tallian", base:3},
|
||||||
|
{name:"Astellian", base:4},
|
||||||
|
{name:"Slovan", base:5},
|
||||||
|
{name:"Norse", base:6},
|
||||||
|
{name:"Elladan", base:7},
|
||||||
|
{name:"Romian", base:8},
|
||||||
|
{name:"Soumi", base:9},
|
||||||
|
{name:"Koryo", base:10},
|
||||||
|
{name:"Hantzu", base:11},
|
||||||
|
{name:"Yamoto", base:12},
|
||||||
|
{name:"Portuzian", base:13},
|
||||||
|
{name:"Nawatli", base:14},
|
||||||
|
{name:"Vengrian", base: 15},
|
||||||
|
{name:"Turchian", base: 16},
|
||||||
|
{name:"Berberan", base: 17},
|
||||||
|
{name:"Eurabic", base: 18},
|
||||||
|
{name:"Inuk", base: 19},
|
||||||
|
{name:"Euskati", base: 20},
|
||||||
|
{name:"Negarian", base: 21},
|
||||||
|
{name:"Keltan", base: 22},
|
||||||
|
{name:"Efratic", base: 23},
|
||||||
|
{name:"Tehrani", base: 24},
|
||||||
|
{name:"Maui", base: 25},
|
||||||
|
{name:"Carnatic", base: 26},
|
||||||
|
{name:"Inqan", base: 27},
|
||||||
|
{name:"Kiswaili", base: 28},
|
||||||
|
{name:"Vietic", base: 29}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// expand cultures across the map (Dijkstra-like algorithm)
|
||||||
|
const expand = function() {
|
||||||
|
console.time('expandCultures');
|
||||||
|
cells = pack.cells;
|
||||||
|
|
||||||
|
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||||
|
pack.cultures.forEach(function(c) {
|
||||||
|
if (!c.i || c.removed) return;
|
||||||
|
queue.queue({e:c.center, p:0, c:c.i});
|
||||||
|
});
|
||||||
|
|
||||||
|
const neutral = cells.i.length / 5000 * 3000 * neutralInput.value; // limit cost for culture growth
|
||||||
|
const cost = [];
|
||||||
|
while (queue.length) {
|
||||||
|
const next = queue.dequeue(), n = next.e, p = next.p, c = next.c;
|
||||||
|
const type = pack.cultures[c].type;
|
||||||
|
cells.c[n].forEach(function(e) {
|
||||||
|
const biome = cells.biome[e];
|
||||||
|
const biomeCost = getBiomeCost(c, biome, type);
|
||||||
|
const biomeChangeCost = biome === cells.biome[n] ? 0 : 5 * Math.abs(biome - cells.biome[n]); // penalty on biome change
|
||||||
|
const heightCost = getHeightCost(e, cells.h[e], type);
|
||||||
|
const riverCost = getRiverCost(cells.r[e], e, type);
|
||||||
|
const typeCost = getTypeCost(cells.t[e], type);
|
||||||
|
const totalCost = p + (biomeCost + biomeChangeCost + heightCost + riverCost + typeCost) / pack.cultures[c].expansionism;
|
||||||
|
|
||||||
|
if (totalCost > neutral) return;
|
||||||
|
|
||||||
|
if (!cost[e] || totalCost < cost[e]) {
|
||||||
|
if (cells.s[e] > 0) cells.culture[e] = c; // assign culture to populated cell
|
||||||
|
cost[e] = totalCost;
|
||||||
|
queue.queue({e, p:totalCost, c});
|
||||||
|
|
||||||
|
//debug.append("text").attr("x", (cells.p[n][0]+cells.p[e][0])/2 - 1).attr("y", (cells.p[n][1]+cells.p[e][1])/2 - 1).text(rn(totalCost-p)).attr("font-size", .8);
|
||||||
|
//const points = [cells.p[n][0], cells.p[n][1], (cells.p[n][0]+cells.p[e][0])/2, (cells.p[n][1]+cells.p[e][1])/2, cells.p[e][0], cells.p[e][1]];
|
||||||
|
//debug.append("polyline").attr("points", points.toString()).attr("marker-mid", "url(#arrow)").attr("opacity", .6);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//debug.selectAll(".text").data(cost).enter().append("text").attr("x", (d, e) => cells.p[e][0]-1).attr("y", (d, e) => cells.p[e][1]-1).text(d => d ? rn(d) : "").attr("font-size", 2);
|
||||||
|
console.timeEnd('expandCultures');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBiomeCost(c, biome, type) {
|
||||||
|
if (cells.biome[pack.cultures[c].center] === biome) return biomesData.cost[biome] / 2; // tiny penalty for native biome
|
||||||
|
if (type === "Hunting") return biomesData.cost[biome] * 5; // non-native biome penalty for hunters
|
||||||
|
if (type === "Nomadic" && biome > 4 && biome < 10) return biomesData.cost[biome] * 10; // forest biome penalty for nomads
|
||||||
|
return biomesData.cost[biome] * 2; // general non-native biome penalty
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHeightCost(i, h, type) {
|
||||||
|
if ((type === "Naval" || type === "Lake") && h < 20) return cells.area[i]; // low sea crossing penalty for Navals
|
||||||
|
if (type === "Nomadic" && h < 20) return cells.area[i] * 50; // giant sea crossing penalty for Navals
|
||||||
|
if (h < 20) return cells.area[i] * 5; // general sea crossing penalty
|
||||||
|
if (type === "Highland" && h < 50) return 30; // penalty for highlanders on lowlands
|
||||||
|
if (type === "Highland") return 0; // no penalty for highlanders on highlands
|
||||||
|
if (h >= 70) return 100; // general mountains crossing penalty
|
||||||
|
if (h >= 50) return 30; // general hills crossing penalty
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRiverCost(r, i, type) {
|
||||||
|
if (type === "River") return r ? 0 : 50; // penalty for river cultures
|
||||||
|
if (!r) return 0; // no penalty for others if there is no river
|
||||||
|
return Math.min(Math.max(cells.fl[i] / 10, 20), 100) // river penalty from 20 to 100 based on flux
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTypeCost(ctype, type) {
|
||||||
|
if (ctype === 1) return type === "Naval" || type === "Lake" ? 0 : type === "Nomadic" ? 60 : 20; // penalty for coastline
|
||||||
|
if (ctype === 2) return type === "Naval" || type === "Nomadic" ? 30 : 0; // low penalty for land level 2 for Navals and nomads
|
||||||
|
if (ctype !== -1) return type === "Naval" || type === "Lake" ? 100 : 0; // penalty for mainland for navals
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {generate, expand, getDefault};
|
||||||
|
|
||||||
|
})));
|
||||||
475
modules/heightmap-generator.js
Normal file
|
|
@ -0,0 +1,475 @@
|
||||||
|
(function (global, factory) {
|
||||||
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||||
|
typeof define === 'function' && define.amd ? define(factory) :
|
||||||
|
(global.HeightmapGenerator = factory());
|
||||||
|
}(this, (function () { 'use strict';
|
||||||
|
|
||||||
|
let cells, p;
|
||||||
|
|
||||||
|
const generate = function() {
|
||||||
|
console.time('generateHeightmap');
|
||||||
|
cells = grid.cells;
|
||||||
|
p = grid.points;
|
||||||
|
cells.h = new Uint8Array(grid.points.length);
|
||||||
|
|
||||||
|
const input = document.getElementById("templateInput");
|
||||||
|
if (!locked("template")) {
|
||||||
|
const rnd = Math.random();
|
||||||
|
if (rnd < .05) input.value = "Volcano"; else // 5%
|
||||||
|
if (rnd < .25) input.value = "High Island"; else // 20%
|
||||||
|
if (rnd < .35) input.value = "Low Island"; else // 10%
|
||||||
|
if (rnd < .55) input.value = "Continents"; else // 20%
|
||||||
|
if (rnd < .85) input.value = "Archipelago"; else // 30%
|
||||||
|
if (rnd < .90) input.value = "Mediterranean"; else // 5%
|
||||||
|
if (rnd < .95) input.value = "Peninsula"; else // 5%
|
||||||
|
if (rnd < .99) input.value = "Pangea"; else // 4%
|
||||||
|
input.value = "Atoll"; // 1%
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (input.value) {
|
||||||
|
case "Volcano": templateVolcano(); break;
|
||||||
|
case "High Island": templateHighIsland(); break;
|
||||||
|
case "Low Island": templateLowIsland(); break;
|
||||||
|
case "Continents": templateContinents(); break;
|
||||||
|
case "Archipelago": templateArchipelago(); break;
|
||||||
|
case "Atoll": templateAtoll(); break;
|
||||||
|
case "Mediterranean": templateMediterranean(); break;
|
||||||
|
case "Peninsula": templatePeninsula(); break;
|
||||||
|
case "Pangea": templatePangea(); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.timeEnd('generateHeightmap');
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse template step
|
||||||
|
function addStep(a1, a2, a3, a4, a5) {
|
||||||
|
if (a1 === "Hill") addHill(a2, a3, a4, a5); else
|
||||||
|
if (a1 === "Pit") addPit(a2, a3, a4, a5); else
|
||||||
|
if (a1 === "Range") addRange(a2, a3, a4, a5); else
|
||||||
|
if (a1 === "Trough") addTrough(a2, a3, a4, a5); else
|
||||||
|
if (a1 === "Strait") addStrait(a2, a3); else
|
||||||
|
if (a1 === "Add") modify(a3, a2, 1); else
|
||||||
|
if (a1 === "Multiply") modify(a3, 0, a2); else
|
||||||
|
if (a1 === "Smooth") smooth(a2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heighmap Template: Volcano
|
||||||
|
function templateVolcano() {
|
||||||
|
addStep("Hill", "1", "90-100", "44-56", "40-60");
|
||||||
|
addStep("Multiply", .8, "50-100");
|
||||||
|
addStep("Range", "1.5", "30-55", "45-55", "40-60");
|
||||||
|
addStep("Smooth", 2);
|
||||||
|
addStep("Hill", "1.5", "25-35", "25-30", "20-75");
|
||||||
|
addStep("Hill", "1", "25-35", "75-80", "25-75");
|
||||||
|
addStep("Hill", "0.5", "20-25", "10-15", "20-25");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heighmap Template: High Island
|
||||||
|
function templateHighIsland() {
|
||||||
|
addStep("Hill", "1", "90-100", "65-75", "47-53");
|
||||||
|
addStep("Add", 5, "all");
|
||||||
|
addStep("Hill", "6", "20-23", "25-55", "45-55");
|
||||||
|
addStep("Range", "1", "40-50", "45-55", "45-55");
|
||||||
|
addStep("Smooth", 2);
|
||||||
|
addStep("Trough", "2-3", "20-30", "20-30", "20-30");
|
||||||
|
addStep("Trough", "2-3", "20-30", "60-80", "70-80");
|
||||||
|
addStep("Hill", "1", "10-15", "60-60", "50-50");
|
||||||
|
addStep("Hill", "1.5", "13-16", "15-20", "20-75");
|
||||||
|
addStep("Multiply", .8, "20-100");
|
||||||
|
addStep("Range", "1.5", "30-40", "15-85", "30-40");
|
||||||
|
addStep("Range", "1.5", "30-40", "15-85", "60-70");
|
||||||
|
addStep("Pit", "2-3", "10-15", "15-85", "20-80");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heighmap Template: Low Island
|
||||||
|
function templateLowIsland() {
|
||||||
|
addStep("Hill", "1", "90-99", "60-80", "45-55");
|
||||||
|
addStep("Hill", "4-5", "25-35", "20-65", "40-60");
|
||||||
|
addStep("Range", "1", "40-50", "45-55", "45-55");
|
||||||
|
addStep("Smooth", 3);
|
||||||
|
addStep("Trough", "1.5", "20-30", "15-85", "20-30");
|
||||||
|
addStep("Trough", "1.5", "20-30", "15-85", "70-80");
|
||||||
|
addStep("Hill", "1.5", "10-15", "5-15", "20-80");
|
||||||
|
addStep("Hill", "1", "10-15", "85-95", "70-80");
|
||||||
|
addStep("Pit", "3-5", "10-15", "15-85", "20-80");
|
||||||
|
addStep("Multiply", .4, "20-100");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heighmap Template: Continents
|
||||||
|
function templateContinents() {
|
||||||
|
addStep("Hill", "1", "80-85", "75-80", "40-60");
|
||||||
|
addStep("Hill", "1", "80-85", "20-25", "40-60");
|
||||||
|
addStep("Multiply", .22, "20-100");
|
||||||
|
addStep("Hill", "5-6", "15-20", "25-75", "20-82");
|
||||||
|
addStep("Range", ".8", "30-60", "5-15", "20-45");
|
||||||
|
addStep("Range", ".8", "30-60", "5-15", "55-80");
|
||||||
|
addStep("Range", "0-3", "30-60", "80-90", "20-80");
|
||||||
|
addStep("Trough", "3-4", "15-20", "15-85", "20-80");
|
||||||
|
addStep("Strait", "2", "vertical");
|
||||||
|
addStep("Smooth", 2);
|
||||||
|
addStep("Trough", "1-2", "5-10", "45-55", "45-55");
|
||||||
|
addStep("Pit", "3-4", "10-15", "15-85", "20-80");
|
||||||
|
addStep("Hill", "1", "5-10", "40-60", "40-60");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heighmap Template: Archipelago
|
||||||
|
function templateArchipelago() {
|
||||||
|
addStep("Add", 11, "all");
|
||||||
|
addStep("Range", "2-3", "40-60", "20-80", "20-80");
|
||||||
|
addStep("Hill", "5", "15-20", "10-90", "30-70");
|
||||||
|
addStep("Hill", "2", "10-15", "10-30", "20-80");
|
||||||
|
addStep("Hill", "2", "10-15", "60-90", "20-80");
|
||||||
|
addStep("Smooth", 3);
|
||||||
|
addStep("Trough", "10", "20-30", "5-95", "5-95");
|
||||||
|
addStep("Strait", "2", "vertical");
|
||||||
|
addStep("Strait", "2", "horizontal");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heighmap Template: Atoll
|
||||||
|
function templateAtoll() {
|
||||||
|
addStep("Hill", "1", "75-80", "50-60", "45-55");
|
||||||
|
addStep("Hill", "1.5", "30-50", "25-75", "30-70");
|
||||||
|
addStep("Hill", ".5", "30-50", "25-35", "30-70");
|
||||||
|
addStep("Smooth", 1);
|
||||||
|
addStep("Multiply", .2, "25-100");
|
||||||
|
addStep("Hill", ".5", "10-20", "50-55", "48-52");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heighmap Template: Mediterranean
|
||||||
|
function templateMediterranean() {
|
||||||
|
addStep("Range", "3-4", "30-50", "0-100", "0-10");
|
||||||
|
addStep("Range", "3-4", "30-50", "0-100", "90-100");
|
||||||
|
addStep("Hill", "5-6", "30-70", "0-100", "0-5");
|
||||||
|
addStep("Hill", "5-6", "30-70", "0-100", "95-100");
|
||||||
|
addStep("Smooth", 1);
|
||||||
|
addStep("Hill", "2-3", "30-70", "0-5", "20-80");
|
||||||
|
addStep("Hill", "2-3", "30-70", "95-100", "20-80");
|
||||||
|
addStep("Multiply", .8, "land");
|
||||||
|
addStep("Trough", "3-5", "40-50", "0-100", "0-10");
|
||||||
|
addStep("Trough", "3-5", "40-50", "0-100", "90-100");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heighmap Template: Peninsula
|
||||||
|
function templatePeninsula() {
|
||||||
|
addStep("Range", "2-3", "20-35", "40-50", "0-15");
|
||||||
|
addStep("Add", 5, "all");
|
||||||
|
addStep("Hill", "1", "90-100", "10-90", "0-5");
|
||||||
|
addStep("Add", 13, "all");
|
||||||
|
addStep("Hill", "3-4", "3-5", "5-95", "80-100");
|
||||||
|
addStep("Hill", "1-2", "3-5", "5-95", "40-60");
|
||||||
|
addStep("Trough", "5-6", "10-25", "5-95", "5-95");
|
||||||
|
addStep("Smooth", 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heighmap Template: Pangea
|
||||||
|
function templatePangea() {
|
||||||
|
addStep("Hill", "1-2", "25-40", "15-50", "0-10");
|
||||||
|
addStep("Hill", "1-2", "5-40", "50-85", "0-10");
|
||||||
|
addStep("Hill", "1-2", "25-40", "50-85", "90-100");
|
||||||
|
addStep("Hill", "1-2", "5-40", "15-50", "90-100");
|
||||||
|
addStep("Hill", "8-12", "20-40", "20-80", "48-52");
|
||||||
|
addStep("Smooth", 2);
|
||||||
|
addStep("Multiply", .7, "land");
|
||||||
|
addStep("Trough", "3-4", "25-35", "5-95", "10-20");
|
||||||
|
addStep("Trough", "3-4", "25-35", "5-95", "80-90");
|
||||||
|
addStep("Range", "5-6", "30-40", "10-90", "35-65");
|
||||||
|
}
|
||||||
|
|
||||||
|
const addHill = function(count, height, rangeX, rangeY) {
|
||||||
|
count = getNumberInRange(count);
|
||||||
|
while (count >= 1 || Math.random() < count) {addOneHill(); count--;}
|
||||||
|
|
||||||
|
function addOneHill() {
|
||||||
|
const change = new Uint8Array( cells.h.length);
|
||||||
|
let limit = 0, start;
|
||||||
|
let h = lim(getNumberInRange(height));
|
||||||
|
|
||||||
|
do {
|
||||||
|
const x = getPointInRange(rangeX, graphWidth);
|
||||||
|
const y = getPointInRange(rangeY, graphHeight);
|
||||||
|
start = findGridCell(x, y);
|
||||||
|
limit++;
|
||||||
|
} while (cells.h[start] + h > 90 && limit < 50)
|
||||||
|
|
||||||
|
change[start] = h;
|
||||||
|
const queue = [start];
|
||||||
|
while (queue.length) {
|
||||||
|
const q = queue.shift();
|
||||||
|
|
||||||
|
for (const c of cells.c[q]) {
|
||||||
|
if (change[c]) continue;
|
||||||
|
change[c] = change[q] ** .98 * (Math.random() * .2 + .9);
|
||||||
|
if (change[c] > 1) queue.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cells.h = cells.h.map((h, i) => lim(h + change[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const addPit = function(count, height, rangeX, rangeY) {
|
||||||
|
count = getNumberInRange(count);
|
||||||
|
while (count >= 1 || Math.random() < count) {addOnePit(); count--;}
|
||||||
|
|
||||||
|
function addOnePit() {
|
||||||
|
const used = new Uint8Array(cells.h.length);
|
||||||
|
let limit = 0, start;
|
||||||
|
let h = lim(getNumberInRange(height));
|
||||||
|
|
||||||
|
do {
|
||||||
|
const x = getPointInRange(rangeX, graphWidth);
|
||||||
|
const y = getPointInRange(rangeY, graphHeight);
|
||||||
|
start = findGridCell(x, y);
|
||||||
|
limit++;
|
||||||
|
} while (cells.h[start] < 20 && limit < 50)
|
||||||
|
|
||||||
|
const queue = [start];
|
||||||
|
while (queue.length) {
|
||||||
|
const q = queue.shift();
|
||||||
|
h = h ** .98 * (Math.random() * .2 + .9);
|
||||||
|
if (h < 1) return;
|
||||||
|
|
||||||
|
cells.c[q].forEach(function(c, i) {
|
||||||
|
if (used[c]) return;
|
||||||
|
cells.h[c] = lim(cells.h[c] - h * (Math.random() * .2 + .9));
|
||||||
|
used[c] = 1;
|
||||||
|
queue.push(c);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addRange = function(count, height, rangeX, rangeY) {
|
||||||
|
count = getNumberInRange(count);
|
||||||
|
while (count >= 1 || Math.random() < count) {addOneRange(); count--;}
|
||||||
|
|
||||||
|
function addOneRange() {
|
||||||
|
const used = new Uint8Array(cells.h.length);
|
||||||
|
let h = lim(getNumberInRange(height));
|
||||||
|
|
||||||
|
// find start and end points
|
||||||
|
const startX = getPointInRange(rangeX, graphWidth);
|
||||||
|
const startY = getPointInRange(rangeY, graphHeight);
|
||||||
|
|
||||||
|
let dist = 0, limit = 0, endX, endY;
|
||||||
|
do {
|
||||||
|
endX = Math.random() * graphWidth * .8 + graphWidth * .1;
|
||||||
|
endY = Math.random() * graphHeight * .7 + graphHeight * .15;
|
||||||
|
dist = Math.abs(endY - startY) + Math.abs(endX - startX);
|
||||||
|
limit++;
|
||||||
|
} while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50)
|
||||||
|
|
||||||
|
let range = getRange(findGridCell(startX, startY), findGridCell(endX, endY));
|
||||||
|
|
||||||
|
// get main ridge
|
||||||
|
function getRange(cur, end) {
|
||||||
|
const range = [cur];
|
||||||
|
used[cur] = 1;
|
||||||
|
|
||||||
|
while (cur !== end) {
|
||||||
|
let min = Infinity;
|
||||||
|
cells.c[cur].forEach(function(e) {
|
||||||
|
if (used[e]) return;
|
||||||
|
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
||||||
|
if (Math.random() > .85) diff = diff / 2;
|
||||||
|
if (diff < min) {min = diff; cur = e;}
|
||||||
|
});
|
||||||
|
if (min === Infinity) return range;
|
||||||
|
range.push(cur);
|
||||||
|
used[cur] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add height to ridge and cells around
|
||||||
|
let queue = range.slice(), i = 0;
|
||||||
|
while (queue.length) {
|
||||||
|
const frontier = queue.slice();
|
||||||
|
queue = [], i++;
|
||||||
|
frontier.forEach(i => {
|
||||||
|
cells.h[i] = lim(cells.h[i] + h * (Math.random() * .3 + .85));
|
||||||
|
});
|
||||||
|
h = h ** .82 - 1;
|
||||||
|
if (h < 2) break;
|
||||||
|
frontier.forEach(f => {
|
||||||
|
cells.c[f].forEach(i => {
|
||||||
|
if (!used[i]) {queue.push(i); used[i] = 1;}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate prominences
|
||||||
|
range.forEach((cur, d) => {
|
||||||
|
if (d%6 !== 0) return;
|
||||||
|
for (const l of d3.range(i)) {
|
||||||
|
const min = cells.c[cur][d3.scan(cells.c[cur], (a, b) => cells.h[a] - cells.h[b])]; // downhill cell
|
||||||
|
//debug.append("circle").attr("cx", p[min][0]).attr("cy", p[min][1]).attr("r", 1);
|
||||||
|
cells.h[min] = (cells.h[cur] * 2 + cells.h[min]) / 3;
|
||||||
|
cur = min;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addTrough = function(count, height, rangeX, rangeY) {
|
||||||
|
count = getNumberInRange(count);
|
||||||
|
while (count >= 1 || Math.random() < count) {addOneTrough(); count--;}
|
||||||
|
|
||||||
|
function addOneTrough() {
|
||||||
|
const used = new Uint8Array(cells.h.length);
|
||||||
|
let h = lim(getNumberInRange(height));
|
||||||
|
|
||||||
|
// find start and end points
|
||||||
|
let limit = 0, startX, startY, start, dist = 0, endX, endY;
|
||||||
|
do {
|
||||||
|
startX = getPointInRange(rangeX, graphWidth);
|
||||||
|
startY = getPointInRange(rangeY, graphHeight);
|
||||||
|
start = findGridCell(startX, startY);
|
||||||
|
limit++;
|
||||||
|
} while (cells.h[start] < 20 && limit < 50)
|
||||||
|
|
||||||
|
limit = 0;
|
||||||
|
do {
|
||||||
|
endX = Math.random() * graphWidth * .8 + graphWidth * .1;
|
||||||
|
endY = Math.random() * graphHeight * .7 + graphHeight * .15;
|
||||||
|
dist = Math.abs(endY - startY) + Math.abs(endX - startX);
|
||||||
|
limit++;
|
||||||
|
} while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50)
|
||||||
|
|
||||||
|
let range = getRange(start, findGridCell(endX, endY));
|
||||||
|
|
||||||
|
// get main ridge
|
||||||
|
function getRange(cur, end) {
|
||||||
|
const range = [cur];
|
||||||
|
used[cur] = 1;
|
||||||
|
|
||||||
|
while (cur !== end) {
|
||||||
|
let min = Infinity;
|
||||||
|
cells.c[cur].forEach(function(e) {
|
||||||
|
if (used[e]) return;
|
||||||
|
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
||||||
|
if (Math.random() > .8) diff = diff / 2;
|
||||||
|
if (diff < min) {min = diff; cur = e;}
|
||||||
|
});
|
||||||
|
if (min === Infinity) return range;
|
||||||
|
range.push(cur);
|
||||||
|
used[cur] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add height to ridge and cells around
|
||||||
|
let queue = range.slice(), i = 0;
|
||||||
|
while (queue.length) {
|
||||||
|
const frontier = queue.slice();
|
||||||
|
queue = [], i++;
|
||||||
|
frontier.forEach(i => {
|
||||||
|
cells.h[i] = lim(cells.h[i] - h * (Math.random() * .3 + .85));
|
||||||
|
});
|
||||||
|
h = h ** .8 - 1;
|
||||||
|
if (h < 2) break;
|
||||||
|
frontier.forEach(f => {
|
||||||
|
cells.c[f].forEach(i => {
|
||||||
|
if (!used[i]) {queue.push(i); used[i] = 1;}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate prominences
|
||||||
|
range.forEach((cur, d) => {
|
||||||
|
if (d%6 !== 0) return;
|
||||||
|
for (const l of d3.range(i)) {
|
||||||
|
const min = cells.c[cur][d3.scan(cells.c[cur], (a, b) => cells.h[a] - cells.h[b])]; // downhill cell
|
||||||
|
//debug.append("circle").attr("cx", p[min][0]).attr("cy", p[min][1]).attr("r", 1);
|
||||||
|
cells.h[min] = (cells.h[cur] * 2 + cells.h[min]) / 3;
|
||||||
|
cur = min;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addStrait = function(width, direction = "vertical") {
|
||||||
|
width = Math.min(getNumberInRange(width), grid.cellsX/3);
|
||||||
|
if (width < 1 && Math.random() < width) return;
|
||||||
|
const used = new Uint8Array(cells.h.length);
|
||||||
|
const vert = direction === "vertical";
|
||||||
|
const startX = vert ? Math.floor(Math.random() * graphWidth * .4 + graphWidth * .3) : 5;
|
||||||
|
const startY = vert ? 5 : Math.floor(Math.random() * graphHeight * .4 + graphHeight * .3);
|
||||||
|
const endX = vert ? Math.floor((graphWidth - startX) - (graphWidth * .1) + (Math.random() * graphWidth * .2)) : graphWidth - 5;
|
||||||
|
const endY = vert ? graphHeight - 5 : Math.floor((graphHeight - startY) - (graphHeight * .1) + (Math.random() * graphHeight * .2));
|
||||||
|
|
||||||
|
const start = findGridCell(startX, startY), end = findGridCell(endX, endY);
|
||||||
|
let range = getRange(start, end);
|
||||||
|
const query = [];
|
||||||
|
|
||||||
|
function getRange(cur, end) {
|
||||||
|
const range = [];
|
||||||
|
|
||||||
|
while (cur !== end) {
|
||||||
|
let min = Infinity;
|
||||||
|
cells.c[cur].forEach(function(e) {
|
||||||
|
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
||||||
|
if (Math.random() > 0.8) diff = diff / 2;
|
||||||
|
if (diff < min) {min = diff; cur = e;}
|
||||||
|
});
|
||||||
|
range.push(cur);
|
||||||
|
}
|
||||||
|
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
const step = .1 / width;
|
||||||
|
|
||||||
|
while (width > 0) {
|
||||||
|
const exp = .9 - step * width;
|
||||||
|
range.forEach(function(r) {
|
||||||
|
cells.c[r].forEach(function(e) {
|
||||||
|
if (used[e]) return;
|
||||||
|
used[e] = 1;
|
||||||
|
query.push(e);
|
||||||
|
cells.h[e] **= exp;
|
||||||
|
if (cells.h[e] > 100) cells.h[e] = 5;
|
||||||
|
});
|
||||||
|
|
||||||
|
range = query.slice();
|
||||||
|
});
|
||||||
|
width--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const modify = function(range, add, mult, power) {
|
||||||
|
const min = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0];
|
||||||
|
const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
|
||||||
|
grid.cells.h = grid.cells.h.map(h => h >= min && h <= max ? mod(h) : h);
|
||||||
|
|
||||||
|
function mod(v) {
|
||||||
|
if (add) v = min === 20 ? Math.max(v + add, 20) : v + add;
|
||||||
|
if (mult !== 1) v = min === 20 ? (v-20) * mult + 20 : v * mult;
|
||||||
|
if (power) v = min === 20 ? (v-20) ** power + 20 : v ** power;
|
||||||
|
return lim(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const smooth = function(fr = 2) {
|
||||||
|
cells.h = cells.h.map((h, i) => {
|
||||||
|
const a = [h];
|
||||||
|
cells.c[i].forEach(c => a.push(cells.h[c]));
|
||||||
|
return lim((h * (fr-1) + d3.mean(a)) / fr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPointInRange(range, length) {
|
||||||
|
if (typeof range !== "string") {console.error("Range should be a string"); return;}
|
||||||
|
const min = range.split("-")[0]/100 || 0;
|
||||||
|
const max = range.split("-")[1]/100 || 100;
|
||||||
|
return rand(min * length, max * length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {generate, addHill, addRange, addTrough, addStrait, addPit, smooth, modify};
|
||||||
|
|
||||||
|
})));
|
||||||
152
modules/names-generator.js
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
(function (global, factory) {
|
||||||
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||||
|
typeof define === 'function' && define.amd ? define(factory) :
|
||||||
|
(global.Names = factory());
|
||||||
|
}(this, (function () { 'use strict';
|
||||||
|
|
||||||
|
const chains = [];
|
||||||
|
|
||||||
|
// calculate Markov chain for a namesbase
|
||||||
|
const calculateChain = function(b) {
|
||||||
|
const chain = [];
|
||||||
|
const d = nameBase[b].join(" ").toLowerCase();
|
||||||
|
|
||||||
|
for (let i = -1, prev = " ", str = ""; i < d.length - 2; prev = str, i += str.length, str = "") {
|
||||||
|
let v = 0, f = " ";
|
||||||
|
|
||||||
|
for (let c=i+1; str.length < 5; c++) {
|
||||||
|
if (d[c] === undefined) break;
|
||||||
|
str += d[c];
|
||||||
|
if (str === " ") break;
|
||||||
|
if (d[c] !== "o" && d[c] !== "e" && vowel(d[c]) && d[c+1] === d[c]) break;
|
||||||
|
if (d[c+2] === " ") {str += d[c+1]; break;}
|
||||||
|
if (vowel(d[c])) v++;
|
||||||
|
if (v && vowel(d[c+2])) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i >= 0) f = d[i];
|
||||||
|
if (chain[f] === undefined) chain[f] = [];
|
||||||
|
chain[f].push(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update chain for specific base
|
||||||
|
const updateChain = (b) => chains[b] = nameBase[b] ? calculateChain(b) : null;
|
||||||
|
|
||||||
|
// update chains for all used bases
|
||||||
|
const updateChains = () => chains.forEach((c, i) => chains[i] = nameBase[i] ? calculateChain(i) : null);
|
||||||
|
|
||||||
|
// generate name using Markov's chain
|
||||||
|
const getBase = function(base, min, max, dupl, multi) {
|
||||||
|
if (base === undefined) {console.error("Please define a base"); return;}
|
||||||
|
if (!chains[base]) chains[base] = nameBase[base] ? calculateChain(base) : null;
|
||||||
|
|
||||||
|
const data = chains[base];
|
||||||
|
if (!data || data[" "] === undefined) {
|
||||||
|
tip("Namesbase " + base + " is incorrect. Please checl in namesbase editor", false, "error");
|
||||||
|
console.error("nameBase " + base + " is incorrect!");
|
||||||
|
return "ERROR";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!min) min = nameBases[base].min;
|
||||||
|
if (!max) max = nameBases[base].max;
|
||||||
|
if (!dupl) dupl = nameBases[base].d;
|
||||||
|
if (!multi) multi = nameBases[base].m;
|
||||||
|
|
||||||
|
let v = data[" "], cur = v[rand(v.length-1)], w = "";
|
||||||
|
for (let i=0; i < 21; i++) {
|
||||||
|
if (cur === " " && Math.random() > multi) {
|
||||||
|
if (w.length < min) {cur = ""; w = ""; v = data[" "];} else break;
|
||||||
|
} else {
|
||||||
|
if ((w+cur).length > max) {
|
||||||
|
if (w.length < min) w += cur;
|
||||||
|
break;
|
||||||
|
} else if (cur === " " && w.length+1 < min) {
|
||||||
|
cur = "";
|
||||||
|
v = data[" "];
|
||||||
|
} else {
|
||||||
|
v = data[cur.slice(-1)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w += cur;
|
||||||
|
cur = v[rand(v.length - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse word to get a final name
|
||||||
|
let name = [...w].reduce(function(r, c, i, d) {
|
||||||
|
if (c === d[i+1] && !dupl.includes(c)) return r; // duplication is not allowed
|
||||||
|
if (!r.length) return c.toUpperCase();
|
||||||
|
if (r.slice(-1) === " ") return r + c.toUpperCase();
|
||||||
|
if (c === "a" && d[i+1] === "e") return r; // "ae" => "e"
|
||||||
|
if (c === " " && i+1 === d.length) return r;
|
||||||
|
// remove consonant before 2 consonants
|
||||||
|
if (i+2 < d.length && !vowel(c) && !vowel(d[i+1]) && !vowel(d[i+2])) return r;
|
||||||
|
if (i+2 < d.length && c === d[i+1] && c === d[i+2]) return r; // remove tree same letters in a row
|
||||||
|
return r + c;
|
||||||
|
}, "");
|
||||||
|
|
||||||
|
if (name.length < 2) name = nameBase[base][rand(nameBase[base].length-1)]; // rare case when no name generated
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate name for culture
|
||||||
|
const getCulture = function(culture, min, max, dupl, multi) {
|
||||||
|
if (culture === undefined) {console.error("Please define a culture"); return;}
|
||||||
|
const base = pack.cultures[culture].base;
|
||||||
|
return getBase(base, min, max, dupl, multi);
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate state name based on capital or random name and culture-specific suffix
|
||||||
|
const getState = function(name, culture) {
|
||||||
|
if (name === undefined) {console.error("Please define a base name"); return;}
|
||||||
|
if (culture === undefined) {console.error("Please define a culture"); return;}
|
||||||
|
const base = pack.cultures[culture].base;
|
||||||
|
|
||||||
|
// exclude endings inappropriate for states name
|
||||||
|
if (name.includes(" ")) name = capitalize(name.replace(/ /g, "").toLowerCase()); // don't allow multiword state names
|
||||||
|
if (name.length > 6 && name.slice(-4) === "berg") name = name.slice(0,-4); // remove -berg for any
|
||||||
|
if (base === 5 && ["sk", "ev", "ov"].includes(name.slice(-2))) name = name.slice(0,-2); // remove -sk/-ev/-ov for Ruthenian
|
||||||
|
else if (base === 1 && name.length > 5 && name.slice(-3) === "ton") name = name.slice(0,-3); // remove -ton for English
|
||||||
|
else if (base === 12) return vowel(name.slice(-1)) ? name : name + "u"; // Japanese ends on any vowel or -u
|
||||||
|
else if (base === 18 && Math.random() < .4) name = vowel(name.slice(0,1).toLowerCase()) ? "Al" + name.toLowerCase() : "Al " + name; // Arabic starts with -Al
|
||||||
|
|
||||||
|
// define if suffix should be used
|
||||||
|
if (name.length > 3 && vowel(name.slice(-1))) {
|
||||||
|
if (vowel(name.slice(-2,-1)) && Math.random() < .85) name = name.slice(0,-2); // 85% for vv
|
||||||
|
else if (Math.random() < .7) name = name.slice(0,-1); // ~60% for cv
|
||||||
|
else return name;
|
||||||
|
} else if (Math.random() < .4) return name; // 60% for cc and vc
|
||||||
|
|
||||||
|
// define suffix
|
||||||
|
let suffix = "";
|
||||||
|
const rnd = Math.random(), l = name.length;
|
||||||
|
if (base === 3) suffix = rnd < .03 && l < 7 ? "terra" : "ia"; // Italian
|
||||||
|
else if (base === 4) suffix = rnd < .03 && l < 7 ? "terra" : "ia"; // Spanish
|
||||||
|
else if (base === 13) suffix = rnd < .03 && l < 7 ? "terra" : "ia"; // Portuguese
|
||||||
|
else if (base === 2) suffix = rnd < .03 && l < 7 ? "terre" : "ia"; // French
|
||||||
|
else if (base === 0) suffix = rnd < .5 && l < 7 ? "land" : "ia"; // German
|
||||||
|
else if (base === 1) suffix = rnd < .4 && l < 7 ? "land" : "ia"; // English
|
||||||
|
else if (base === 6) suffix = rnd < .3 && l < 7 ? "land" : "ia"; // Nordic
|
||||||
|
else if (base === 7) suffix = rnd < .1 ? "eia" : "ia"; // Greek
|
||||||
|
else if (base === 9) suffix = rnd < .35 ? "maa" : "ia"; // Finnic
|
||||||
|
else if (base === 15) suffix = rnd < .6 && l < 6 ? "orszag" : "ia"; // Hungarian
|
||||||
|
else if (base === 16) suffix = rnd < .5 ? "stan" : "ya"; // Turkish
|
||||||
|
else if (base === 10) suffix = "guk"; // Korean
|
||||||
|
else if (base === 11) suffix = " Guo"; // Chinese
|
||||||
|
else if (base === 14) suffix = rnd < .6 && l < 7 ? "tlan" : "co"; // Nahuatl
|
||||||
|
else if (base === 17) suffix = rnd < .8 ? "a" : "ia"; // Berber
|
||||||
|
else if (base === 18) suffix = rnd < .8 ? "a" : "ia"; // Arabic
|
||||||
|
else suffix = "ia" // other
|
||||||
|
|
||||||
|
if (name.slice(-1 * suffix.length) === suffix) return name; // no suffix if name already ends with it
|
||||||
|
const s1 = suffix.charAt(0);
|
||||||
|
if (name.slice(-1) === s1) name = name.slice(0, -1); // remove name last letter if it's a suffix first letter
|
||||||
|
if (vowel(s1) === vowel(name.slice(-1)) && vowel(s1) === vowel(name.slice(-2,-1))) name = name.slice(0, -1); // remove name last char if 2 last chars are the same type as suffix's 1st
|
||||||
|
return name + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {getBase, getCulture, getState, updateChain, updateChains};
|
||||||
|
})));
|
||||||
95
modules/ocean-layers.js
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
(function (global, factory) {
|
||||||
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||||
|
typeof define === 'function' && define.amd ? define(factory) :
|
||||||
|
(global.OceanLayers = factory());
|
||||||
|
}(this, (function () { 'use strict';
|
||||||
|
|
||||||
|
let cells, vertices, pointsN, used;
|
||||||
|
|
||||||
|
var OceanLayers = function OceanLayers() {
|
||||||
|
const outline = outlineLayersInput.value;
|
||||||
|
if (outline === "none") return;
|
||||||
|
console.time("drawOceanLayers");
|
||||||
|
|
||||||
|
cells = grid.cells, pointsN = grid.cells.i.length, vertices = grid.vertices;
|
||||||
|
const limits = outline === "random" ? randomizeOutline() : outline.split(",").map(s => +s);
|
||||||
|
markupOcean(limits);
|
||||||
|
|
||||||
|
const chains = [];
|
||||||
|
const opacity = rn(0.4 / limits.length, 2);
|
||||||
|
used = new Uint8Array(pointsN); // to detect already passed cells
|
||||||
|
|
||||||
|
for (const i of cells.i) {
|
||||||
|
const t = cells.t[i];
|
||||||
|
if (used[i] || !limits.includes(t)) continue;
|
||||||
|
const start = findStart(i, t);
|
||||||
|
if (!start) continue;
|
||||||
|
used[i] = 1;
|
||||||
|
//debug.append("circle").attr("r", 3).attr("cx", vertices.p[start.c][0]).attr("cy", vertices.p[start.c][1]).attr("fill", "red").attr("stroke", "black").attr("stroke-width", .3);
|
||||||
|
|
||||||
|
const chain = connectVertices(start, t); // vertices chain to form a path
|
||||||
|
const relaxation = 1 + t * -2; // select only n-th point
|
||||||
|
const relaxed = chain.filter((v, i) => i % relaxation === 0 || vertices.c[v].some(c => c >= pointsN));
|
||||||
|
if (relaxed.length >= 3) chains.push([t, relaxed.map(v => vertices.p[v])]);
|
||||||
|
}
|
||||||
|
//debug.selectAll("text").data(cells.i).enter().append("text").attr("font-size", 2).attr("x", d => grid.points[d][0]).attr("y", d => grid.points[d][1]).text(d => cells.t[d]+","+used[d]);
|
||||||
|
|
||||||
|
for (const t of limits) {
|
||||||
|
const path = chains.filter(c => c[0] === t).map(c => round(lineGen(c[1]))).join();
|
||||||
|
if (path) oceanLayers.append("path").attr("d", path).attr("fill", "#ecf2f9").style("opacity", opacity);
|
||||||
|
// For each layer there should outer ring. If no, layer will be upside down. Need to fix it in the future
|
||||||
|
}
|
||||||
|
|
||||||
|
// find eligible cell vertex to start path detection
|
||||||
|
function findStart(i, t) {
|
||||||
|
if (cells.b[i]) return cells.v[i].find(v => vertices.c[v].some(c => c >= pointsN)); // map border cell
|
||||||
|
return cells.v[i][cells.c[i].findIndex(c => cells.t[c] < t || !cells.t[c])];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.timeEnd("drawOceanLayers");
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomizeOutline() {
|
||||||
|
const limits = [];
|
||||||
|
let odd = 0.2
|
||||||
|
for (let l = -9; l < 0; l++) {
|
||||||
|
if (Math.random() < odd) {odd = 0.2; limits.push(l);}
|
||||||
|
else {odd *= 2;}
|
||||||
|
}
|
||||||
|
return limits;
|
||||||
|
}
|
||||||
|
|
||||||
|
function markupOcean(limits) {
|
||||||
|
// Define ocean cells type based on distance form land
|
||||||
|
for (let t = -2; t >= limits[0]-1; t--) {
|
||||||
|
for (let i = 0; i < pointsN; i++) {
|
||||||
|
if (cells.t[i] !== t+1) continue;
|
||||||
|
cells.c[i].forEach(function(e) {if (!cells.t[e]) cells.t[e] = t;});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect vertices to chain
|
||||||
|
function connectVertices(start, t) {
|
||||||
|
const chain = []; // vertices chain to form a path
|
||||||
|
for (let i=0, current = start; i === 0 || current !== start && i < 10000; i++) {
|
||||||
|
const prev = chain[chain.length - 1]; // previous vertex in chain
|
||||||
|
chain.push(current); // add current vertex to sequence
|
||||||
|
const c = vertices.c[current]; // cells adjacent to vertex
|
||||||
|
c.filter(c => cells.t[c] === t).forEach(c => used[c] = 1);
|
||||||
|
const v = vertices.v[current]; // neighboring vertices
|
||||||
|
const c0 = !cells.t[c[0]] || cells.t[c[0]] === t-1;
|
||||||
|
const c1 = !cells.t[c[1]] || cells.t[c[1]] === t-1;
|
||||||
|
const c2 = !cells.t[c[2]] || cells.t[c[2]] === t-1;
|
||||||
|
if (v[0] !== undefined && v[0] !== prev && c0 !== c1) current = v[0];
|
||||||
|
else if (v[1] !== undefined && v[1] !== prev && c1 !== c2) current = v[1];
|
||||||
|
else if (v[2] !== undefined && v[2] !== prev && c0 !== c2) current = v[2];
|
||||||
|
if (current === chain[chain.length - 1]) {console.error("Next vertex is not found"); break;}
|
||||||
|
}
|
||||||
|
chain.push(chain[0]); // push first vertex as the last one
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
|
||||||
|
return OceanLayers;
|
||||||
|
|
||||||
|
})));
|
||||||
80
modules/relief-icons.js
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
(function (global, factory) {
|
||||||
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||||
|
typeof define === 'function' && define.amd ? define(factory) :
|
||||||
|
(global.ReliefIcons = factory());
|
||||||
|
}(this, (function () {'use strict';
|
||||||
|
|
||||||
|
var ReliefIcons = function ReliefIcons() {
|
||||||
|
console.time('drawRelief');
|
||||||
|
terrain.selectAll("*").remove();
|
||||||
|
const density = +styleReliefDensityInput.value;
|
||||||
|
if (!density) return;
|
||||||
|
|
||||||
|
const size = 1.6, mod = .2 * size; // size modifier;s
|
||||||
|
const relief = []; // t: type, c: cell, x: centerX, y: centerY, s: size;
|
||||||
|
const cells = pack.cells;
|
||||||
|
|
||||||
|
for (const i of cells.i) {
|
||||||
|
const height = cells.h[i];
|
||||||
|
if (height < 20) continue; // no icons on water
|
||||||
|
if (cells.r[i]) continue; // no icons on rivers
|
||||||
|
const b = cells.biome[i];
|
||||||
|
if (height < 50 && biomesData.iconsDensity[b] === 0) continue; // no icons for this biome
|
||||||
|
const polygon = getPackPolygon(i);
|
||||||
|
const x = d3.extent(polygon, p => p[0]), y = d3.extent(polygon, p => p[1]);
|
||||||
|
const e = [Math.ceil(x[0]), Math.ceil(y[0]), Math.floor(x[1]), Math.floor(y[1])]; // polygon box
|
||||||
|
|
||||||
|
if (height < 50) placeBiomeIcons(i, b); else placeReliefIcons(i);
|
||||||
|
|
||||||
|
function placeBiomeIcons() {
|
||||||
|
const iconsDensity = biomesData.iconsDensity[b] / 100;
|
||||||
|
const radius = 2 / iconsDensity / density;
|
||||||
|
if (Math.random() > iconsDensity * 10) return;
|
||||||
|
|
||||||
|
for (const [cx, cy] of poissonDiscSampler(e[0], e[1], e[2], e[3], radius)) {
|
||||||
|
if (!d3.polygonContains(polygon, [cx, cy])) continue;
|
||||||
|
let h = rn((4 + Math.random()) * size, 2);
|
||||||
|
const icon = getBiomeIcon(biomesData.icons[b]);
|
||||||
|
if (icon === "#relief-grass-1") h *= 1.3;
|
||||||
|
relief.push({t: icon, c: i, x: rn(cx-h, 2), y: rn(cy-h, 2), s: h*2});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeReliefIcons() {
|
||||||
|
const radius = 2 / density;
|
||||||
|
const [icon, h] = getReliefIcon(height);
|
||||||
|
|
||||||
|
for (const [cx, cy] of poissonDiscSampler(e[0], e[1], e[2], e[3], radius)) {
|
||||||
|
if (!d3.polygonContains(polygon, [cx, cy])) continue;
|
||||||
|
relief.push({t: icon, c: i, x: rn(cx-h, 2), y: rn(cy-h, 2), s: h*2});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReliefIcon(h) {
|
||||||
|
const type = h > 70 ? "mount" : "hill";
|
||||||
|
const size = h > 70 ? (h - 45) * mod : Math.min(Math.max((h - 40) * mod, 3), 6);
|
||||||
|
return ["#relief-" + type + "-1", size];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort relief icons by y+size
|
||||||
|
relief.sort((a, b) => (a.y + a.s) - (b.y + b.s));
|
||||||
|
|
||||||
|
// append relief icons at once using pure js
|
||||||
|
void function renderRelief() {
|
||||||
|
let reliefHTML = "";
|
||||||
|
for (const r of relief) {reliefHTML += `<use xlink:href="${r.t}" data-type="${r.t}" x=${r.x} y=${r.y} data-size=${r.s} width=${r.s} height=${r.s}></use>`;}
|
||||||
|
terrain.html(reliefHTML);
|
||||||
|
}()
|
||||||
|
|
||||||
|
console.timeEnd('drawRelief');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBiomeIcon(i) {
|
||||||
|
return "#relief-" + i[Math.floor(Math.random() * i.length)] + "-1";
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReliefIcons;
|
||||||
|
|
||||||
|
})));
|
||||||
204
modules/river-generator.js
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
(function (global, factory) {
|
||||||
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||||
|
typeof define === 'function' && define.amd ? define(factory) :
|
||||||
|
(global.Rivers = factory());
|
||||||
|
}(this, (function () {'use strict';
|
||||||
|
|
||||||
|
const generate = function Rivers() {
|
||||||
|
console.time('generateRivers');
|
||||||
|
Math.seedrandom(seed);
|
||||||
|
const cells = pack.cells, p = cells.p, features = pack.features;
|
||||||
|
features.forEach(f => {delete f.river; delete f.flux;});
|
||||||
|
|
||||||
|
const riversData = []; // rivers data
|
||||||
|
cells.fl = new Uint16Array(cells.i.length); // water flux array
|
||||||
|
cells.r = new Uint16Array(cells.i.length); // rivers array
|
||||||
|
cells.conf = new Uint8Array(cells.i.length); // confluences array
|
||||||
|
let riverNext = 1; // first river id is 1, not 0
|
||||||
|
|
||||||
|
void function drainWater() {
|
||||||
|
const land = cells.i.filter(isLand).sort(highest);
|
||||||
|
|
||||||
|
land.forEach(function(i) {
|
||||||
|
cells.fl[i] += grid.cells.prec[cells.g[i]]; // flux from precipitation
|
||||||
|
const x = p[i][0], y = p[i][1];
|
||||||
|
|
||||||
|
// near-border cell: pour out of the screen
|
||||||
|
if (cells.b[i]) {
|
||||||
|
if (cells.r[i]) {
|
||||||
|
const to = [];
|
||||||
|
const min = Math.min(y, graphHeight - y, x, graphWidth - x);
|
||||||
|
if (min === y) {to[0] = x; to[1] = 0;} else
|
||||||
|
if (min === graphHeight - y) {to[0] = x; to[1] = graphHeight;} else
|
||||||
|
if (min === x) {to[0] = 0; to[1] = y;} else
|
||||||
|
if (min === graphWidth - x) {to[0] = graphWidth; to[1] = y;}
|
||||||
|
riversData.push({river: cells.r[i], cell: i, x: to[0], y: to[1]});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const min = cells.c[i][d3.scan(cells.c[i], (a, b) => cells.h[a] - cells.h[b])]; // downhill cell
|
||||||
|
|
||||||
|
// allow only one river can flow thought a lake
|
||||||
|
const cf = features[cells.f[i]]; // current cell feature
|
||||||
|
if (cf.river && cf.river !== cells.r[i]) {
|
||||||
|
cells.fl[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cells.fl[i] < 30) {
|
||||||
|
if (cells.h[min] >= 20) cells.fl[min] += cells.fl[i];
|
||||||
|
return; // flux is too small to operate as river
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proclaim a new river
|
||||||
|
if (!cells.r[i]) {
|
||||||
|
cells.r[i] = riverNext;
|
||||||
|
riversData.push({river: riverNext, cell: i, x, y});
|
||||||
|
riverNext++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cells.r[min]) { // downhill cell already has river assigned
|
||||||
|
if (cells.fl[min] < cells.fl[i]) {
|
||||||
|
cells.conf[min] = cells.fl[min]; // confluence
|
||||||
|
cells.r[min] = cells.r[i]; // re-assign river if downhill part has less flux
|
||||||
|
} else cells.conf[min] += cells.fl[i]; // confluence
|
||||||
|
} else cells.r[min] = cells.r[i]; // assign the river to the downhill cell
|
||||||
|
|
||||||
|
const nx = p[min][0], ny = p[min][1];
|
||||||
|
if (cells.h[min] < 20) {
|
||||||
|
// pour water to the sea haven
|
||||||
|
riversData.push({river: cells.r[i], cell: cells.haven[i], x: nx, y: ny});
|
||||||
|
} else {
|
||||||
|
const mf = features[cells.f[min]]; // feature of min cell
|
||||||
|
if (mf.type === "lake") {
|
||||||
|
if (!mf.river || cells.fl[i] > mf.flux) {
|
||||||
|
mf.river = cells.r[i]; // pour water to temporaly elevated lake
|
||||||
|
mf.flux = cells.fl[i]; // entering flux
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cells.fl[min] += cells.fl[i]; // propagate flux
|
||||||
|
riversData.push({river: cells.r[i], cell: min, x: nx, y: ny}); // add next River segment
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}()
|
||||||
|
|
||||||
|
void function drawRivers() {
|
||||||
|
const riverPaths = []; // to store data for all rivers
|
||||||
|
|
||||||
|
for (let r = 1; r <= riverNext; r++) {
|
||||||
|
const riverSegments = riversData.filter(d => d.river === r);
|
||||||
|
|
||||||
|
if (riverSegments.length > 2) {
|
||||||
|
const riverEnhanced = addMeandring(riverSegments);
|
||||||
|
const width = rn(0.8 + Math.random() * 0.4, 1); // river width modifier
|
||||||
|
const increment = rn(0.8 + Math.random() * 0.6, 1); // river bed widening modifier
|
||||||
|
const path = getPath(riverEnhanced, width, increment);
|
||||||
|
riverPaths.push([r, path, width, increment]);
|
||||||
|
} else {
|
||||||
|
// remove too short rivers
|
||||||
|
riverSegments.filter(s => cells.r[s.cell] === r).forEach(s => cells.r[s.cell] = 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rivers.selectAll("path").remove();
|
||||||
|
rivers.selectAll("path").data(riverPaths).enter()
|
||||||
|
.append("path").attr("d", d => d[1]).attr("id", d => "river"+d[0])
|
||||||
|
.attr("data-width", d => d[2]).attr("data-increment", d => d[3]);
|
||||||
|
}()
|
||||||
|
|
||||||
|
console.timeEnd('generateRivers');
|
||||||
|
}
|
||||||
|
|
||||||
|
// add more river points on 1/3 and 2/3 of length
|
||||||
|
const addMeandring = function(segments, rndFactor = 0.3) {
|
||||||
|
const riverEnhanced = []; // to store enhanced segments
|
||||||
|
let side = 1; // to control meandring direction
|
||||||
|
|
||||||
|
for (let s = 0; s < segments.length; s++) {
|
||||||
|
const sX = segments[s].x, sY = segments[s].y; // segment start coordinates
|
||||||
|
const c = pack.cells.conf[segments[s].cell] || 0; // if segment is river confluence
|
||||||
|
riverEnhanced.push([sX, sY, c]);
|
||||||
|
|
||||||
|
if (s+1 === segments.length) break; // do not enhance last segment
|
||||||
|
|
||||||
|
const eX = segments[s+1].x, eY = segments[s+1].y; // segment end coordinates
|
||||||
|
const angle = Math.atan2(eY - sY, eX - sX);
|
||||||
|
const sin = Math.sin(angle), cos = Math.cos(angle);
|
||||||
|
const serpentine = 1 / (s + 1) + 0.3;
|
||||||
|
const meandr = serpentine + Math.random() * rndFactor;
|
||||||
|
if (Math.random() < 0.5) side *= -1; // change meandring direction in 50%
|
||||||
|
const dist2 = (eX - sX) ** 2 + (eY - sY) ** 2;
|
||||||
|
// if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment
|
||||||
|
if (dist2 > 64 || (dist2 > 16 && segments.length < 6)) {
|
||||||
|
const p1x = (sX * 2 + eX) / 3 + side * -sin * meandr;
|
||||||
|
const p1y = (sY * 2 + eY) / 3 + side * cos * meandr;
|
||||||
|
if (Math.random() < 0.2) side *= -1; // change 2nd extra point meandring direction in 20%
|
||||||
|
const p2x = (sX + eX * 2) / 3 + side * sin * meandr;
|
||||||
|
const p2y = (sY + eY * 2) / 3 + side * cos * meandr;
|
||||||
|
riverEnhanced.push([p1x, p1y], [p2x, p2y]);
|
||||||
|
// if dist is medium or river is small add 1 extra middlepoint
|
||||||
|
} else if (dist2 > 16 || segments.length < 6) {
|
||||||
|
const p1x = (sX + eX) / 2 + side * -sin * meandr;
|
||||||
|
const p1y = (sY + eY) / 2 + side * cos * meandr;
|
||||||
|
riverEnhanced.push([p1x, p1y]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return riverEnhanced;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPath = function(points, width = 1, increment = 1) {
|
||||||
|
let offset, extraOffset = .1; // starting river width (to make river source visible)
|
||||||
|
const riverLength = points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i-1][0], v[1] - p[i-1][1]) : 0), 0); // summ of segments length
|
||||||
|
const widening = rn((1000 + (riverLength * 30)) * increment);
|
||||||
|
const riverPointsLeft = [], riverPointsRight = []; // store points on both sides to build a valid polygon
|
||||||
|
const last = points.length - 1;
|
||||||
|
const factor = riverLength / points.length;
|
||||||
|
|
||||||
|
// first point
|
||||||
|
let x = points[0][0], y = points[0][1], c;
|
||||||
|
let angle = Math.atan2(y - points[1][1], x - points[1][0]);
|
||||||
|
let sin = Math.sin(angle), cos = Math.cos(angle);
|
||||||
|
let xLeft = x + -sin * extraOffset, yLeft = y + cos * extraOffset;
|
||||||
|
riverPointsLeft.push([xLeft, yLeft]);
|
||||||
|
let xRight = x + sin * extraOffset, yRight = y + -cos * extraOffset;
|
||||||
|
riverPointsRight.unshift([xRight, yRight]);
|
||||||
|
|
||||||
|
// middle points
|
||||||
|
for (let p = 1; p < last; p++) {
|
||||||
|
x = points[p][0], y = points[p][1], c = points[p][2] || 0;
|
||||||
|
const xPrev = points[p-1][0], yPrev = points[p - 1][1];
|
||||||
|
const xNext = points[p+1][0], yNext = points[p + 1][1];
|
||||||
|
angle = Math.atan2(yPrev - yNext, xPrev - xNext);
|
||||||
|
sin = Math.sin(angle), cos = Math.cos(angle);
|
||||||
|
offset = (Math.atan(Math.pow(p * factor, 2) / widening) / 2 * width) + extraOffset;
|
||||||
|
const confOffset = Math.atan(c * 5 / widening);
|
||||||
|
extraOffset += confOffset;
|
||||||
|
xLeft = x + -sin * offset, yLeft = y + cos * (offset + confOffset);
|
||||||
|
riverPointsLeft.push([xLeft, yLeft]);
|
||||||
|
xRight = x + sin * offset, yRight = y + -cos * offset;
|
||||||
|
riverPointsRight.unshift([xRight, yRight]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// end point
|
||||||
|
x = points[last][0], y = points[last][1], c = points[last][2];
|
||||||
|
if (c) extraOffset += Math.atan(c * 10 / widening); // add extra width on river confluence
|
||||||
|
angle = Math.atan2(points[last-1][1] - y, points[last-1][0] - x);
|
||||||
|
sin = Math.sin(angle), cos = Math.cos(angle);
|
||||||
|
xLeft = x + -sin * offset, yLeft = y + cos * offset;
|
||||||
|
riverPointsLeft.push([xLeft, yLeft]);
|
||||||
|
xRight = x + sin * offset, yRight = y + -cos * offset;
|
||||||
|
riverPointsRight.unshift([xRight, yRight]);
|
||||||
|
|
||||||
|
// generate polygon path and return
|
||||||
|
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||||
|
const right = lineGen(riverPointsRight);
|
||||||
|
let left = lineGen(riverPointsLeft);
|
||||||
|
left = left.substring(left.indexOf("C"));
|
||||||
|
return round(right + left, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {generate, addMeandring, getPath};
|
||||||
|
|
||||||
|
})));
|
||||||
252
modules/routes-generator.js
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
(function (global, factory) {
|
||||||
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||||
|
typeof define === 'function' && define.amd ? define(factory) :
|
||||||
|
(global.Routes = factory());
|
||||||
|
}(this, (function () {'use strict';
|
||||||
|
|
||||||
|
const getRoads = function() {
|
||||||
|
console.time("generateMainRoads");
|
||||||
|
const cells = pack.cells, burgs = pack.burgs.filter(b => b.i && !b.removed);
|
||||||
|
const capitals = burgs.filter(b => b.capital);
|
||||||
|
if (capitals.length < 2) return []; // not enought capitals to build main roads
|
||||||
|
const paths = []; // array to store path segments
|
||||||
|
|
||||||
|
for (const b of capitals) {
|
||||||
|
const connect = capitals.filter(c => c.i > b.i && c.feature === b.feature);
|
||||||
|
if (!connect.length) continue;
|
||||||
|
const farthest = d3.scan(connect, (a, c) => ((c.y - b.y) ** 2 + (c.x - b.x) ** 2) - ((a.y - b.y) ** 2 + (a.x - b.x) ** 2));
|
||||||
|
const [from, exit] = findLandPath(b.cell, connect[farthest].cell, null);
|
||||||
|
const segments = restorePath(b.cell, exit, "main", from);
|
||||||
|
segments.forEach(s => paths.push(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
cells.i.forEach(i => cells.s[i] += cells.road[i] / 2); // add roads to suitability score
|
||||||
|
console.timeEnd("generateMainRoads");
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTrails = function() {
|
||||||
|
console.time("generateTrails");
|
||||||
|
const cells = pack.cells, burgs = pack.burgs.filter(b => b.i && !b.removed);
|
||||||
|
if (burgs.length < 2) return []; // not enought capitals to build main roads
|
||||||
|
|
||||||
|
let paths = []; // array to store path segments
|
||||||
|
for (const f of pack.features.filter(f => f.land)) {
|
||||||
|
const isle = burgs.filter(b => b.feature === f.i); // burgs on island
|
||||||
|
if (isle.length < 2) continue;
|
||||||
|
|
||||||
|
isle.forEach(function(b, i) {
|
||||||
|
let path = [];
|
||||||
|
if (!i) {
|
||||||
|
const farthest = d3.scan(isle, (a, c) => ((c.y - b.y) ** 2 + (c.x - b.x) ** 2) - ((a.y - b.y) ** 2 + (a.x - b.x) ** 2));
|
||||||
|
const to = isle[farthest].cell;
|
||||||
|
if (cells.road[to]) return;
|
||||||
|
const [from, exit] = findLandPath(b.cell, to, null);
|
||||||
|
path = restorePath(b.cell, exit, "small", from);
|
||||||
|
} else {
|
||||||
|
if (cells.road[b.cell]) return;
|
||||||
|
const [from, exit] = findLandPath(b.cell, null, true);
|
||||||
|
if (exit === null) return;
|
||||||
|
path = restorePath(b.cell, exit, "small", from);
|
||||||
|
}
|
||||||
|
if (path) paths = paths.concat(path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.timeEnd("generateTrails");
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSearoutes = function() {
|
||||||
|
console.time("generateSearoutes");
|
||||||
|
const cells = pack.cells, allPorts = pack.burgs.filter(b => b.port != 0 && !b.removed);
|
||||||
|
if (allPorts.length < 2) return [];
|
||||||
|
const bodies = new Set(allPorts.map(b => b.port)); // features with ports
|
||||||
|
let from = [], exit = null, path = [], paths = []; // array to store path segments
|
||||||
|
|
||||||
|
bodies.forEach(function(f) {
|
||||||
|
const ports = allPorts.filter(b => b.port === f);
|
||||||
|
if (ports.length < 2) return;
|
||||||
|
const first = ports[0].cell;
|
||||||
|
|
||||||
|
// directly connect first port with the farthest one on the same island to remove gap
|
||||||
|
if (pack.features[f].type !== "lake") {
|
||||||
|
const portsOnIsland = ports.filter(b => cells.f[b.cell] === cells.f[first]);
|
||||||
|
if (portsOnIsland.length > 3) {
|
||||||
|
const opposite = ports[d3.scan(portsOnIsland, (a, b) => ((b.y - ports[0].y) ** 2 + (b.x - ports[0].x) ** 2) - ((a.y - ports[0].y) ** 2 + (a.x - ports[0].x) ** 2))].cell;
|
||||||
|
//debug.append("circle").attr("r", 1).attr("fill", "blue").attr("cx", pack.cells.p[first][0]).attr("cy", pack.cells.p[first][1])
|
||||||
|
//debug.append("circle").attr("r", 1).attr("fill", "green").attr("cx", pack.cells.p[opposite][0]).attr("cy", pack.cells.p[opposite][1])
|
||||||
|
[from, exit] = findOceanPath(opposite, first);
|
||||||
|
from[first] = cells.haven[first];
|
||||||
|
path = restorePath(opposite, first, "ocean", from);
|
||||||
|
paths = paths.concat(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// directly connect first port with the farthest one
|
||||||
|
const farthest = ports[d3.scan(ports, (a, b) => ((b.y - ports[0].y) ** 2 + (b.x - ports[0].x) ** 2) - ((a.y - ports[0].y) ** 2 + (a.x - ports[0].x) ** 2))].cell;
|
||||||
|
[from, exit] = findOceanPath(farthest, first);
|
||||||
|
from[first] = cells.haven[first];
|
||||||
|
path = restorePath(farthest, first, "ocean", from);
|
||||||
|
paths = paths.concat(path);
|
||||||
|
|
||||||
|
// indirectly connect first port with all other ports
|
||||||
|
if (ports.length < 3) return;
|
||||||
|
for (const p of ports) {
|
||||||
|
if (p.cell === first || p.cell === farthest) continue;
|
||||||
|
[from, exit] = findOceanPath(p.cell, first, true);
|
||||||
|
//from[exit] = cells.haven[exit];
|
||||||
|
const path = restorePath(p.cell, exit, "ocean", from);
|
||||||
|
paths = paths.concat(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
console.timeEnd("generateSearoutes");
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
const draw = function(main, small, ocean) {
|
||||||
|
console.time("drawRoutes");
|
||||||
|
const cells = pack.cells, burgs = pack.burgs;
|
||||||
|
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||||
|
|
||||||
|
// main routes
|
||||||
|
roads.selectAll("path").data(main).enter().append("path")
|
||||||
|
.attr("id", (d, i) => "road" + i)
|
||||||
|
.attr("d", d => round(lineGen(d.map(c => {
|
||||||
|
const b = cells.burg[c];
|
||||||
|
const x = b ? burgs[b].x : cells.p[c][0];
|
||||||
|
const y = b ? burgs[b].y : cells.p[c][1];
|
||||||
|
return [x, y];
|
||||||
|
})), 1));
|
||||||
|
|
||||||
|
// small routes
|
||||||
|
trails.selectAll("path").data(small).enter().append("path")
|
||||||
|
.attr("id", (d, i) => "trail" + i)
|
||||||
|
.attr("d", d => round(lineGen(d.map(c => {
|
||||||
|
const b = cells.burg[c];
|
||||||
|
const x = b ? burgs[b].x : cells.p[c][0];
|
||||||
|
const y = b ? burgs[b].y : cells.p[c][1];
|
||||||
|
return [x, y];
|
||||||
|
})), 1));
|
||||||
|
|
||||||
|
// ocean routes
|
||||||
|
lineGen.curve(d3.curveBundle.beta(1));
|
||||||
|
searoutes.selectAll("path").data(ocean).enter().append("path")
|
||||||
|
.attr("id", (d, i) => "searoute" + i)
|
||||||
|
.attr("d", d => round(lineGen(d.map(c => {
|
||||||
|
const b = cells.burg[c];
|
||||||
|
const x = b ? burgs[b].x : cells.p[c][0];
|
||||||
|
const y = b ? burgs[b].y : cells.p[c][1];
|
||||||
|
return [x, y];
|
||||||
|
})), 1));
|
||||||
|
|
||||||
|
console.timeEnd("drawRoutes");
|
||||||
|
}
|
||||||
|
|
||||||
|
const regenerate = function() {
|
||||||
|
routes.selectAll("path").remove();
|
||||||
|
pack.cells.road = new Uint16Array(pack.cells.i.length);
|
||||||
|
const main = getRoads();
|
||||||
|
const small = getTrails();
|
||||||
|
const ocean = getSearoutes();
|
||||||
|
draw(main, small, ocean);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {getRoads, getTrails, getSearoutes, draw, regenerate};
|
||||||
|
|
||||||
|
// Dijkstra's algorithm to find a land path
|
||||||
|
function findLandPath(start, exit = null, toRoad = null) {
|
||||||
|
const cells = pack.cells;
|
||||||
|
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||||
|
const cost = [], from = [];
|
||||||
|
const basicCost = 10;
|
||||||
|
queue.queue({e: start, p: 0});
|
||||||
|
|
||||||
|
while (queue.length) {
|
||||||
|
const next = queue.dequeue(), n = next.e, p = next.p;
|
||||||
|
if (toRoad && cells.road[n]) return [from, n];
|
||||||
|
|
||||||
|
for (const c of cells.c[n]) {
|
||||||
|
if (cells.h[c] < 20) continue; // ignore water cells
|
||||||
|
const habitedCost = 100 - biomesData.habitability[cells.biome[c]];
|
||||||
|
const heightCost = Math.abs(cells.h[c] - cells.h[n]) * 10;
|
||||||
|
const cellCoast = basicCost + habitedCost + heightCost;
|
||||||
|
const totalCost = p + (cells.road[c] || cells.burg[c] ? cellCoast / 3 : cellCoast);
|
||||||
|
|
||||||
|
if (from[c] || totalCost >= cost[c]) continue;
|
||||||
|
from[c] = n;
|
||||||
|
if (c === exit) return [from, exit];
|
||||||
|
cost[c] = totalCost;
|
||||||
|
queue.queue({e: c, p: totalCost});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return [from, exit];
|
||||||
|
}
|
||||||
|
|
||||||
|
function restorePath(start, end, type, from) {
|
||||||
|
const cells = pack.cells;
|
||||||
|
const path = []; // to store all segments;
|
||||||
|
let segment = [], current = end, prev = end;
|
||||||
|
const score = type === "main" ? 5 : 1; // to incrade road score at cell
|
||||||
|
|
||||||
|
if (type === "ocean" || !cells.road[prev]) segment.push(end);
|
||||||
|
if (!cells.road[prev]) cells.road[prev] = score;
|
||||||
|
|
||||||
|
for (let i = 0, limit = 1000; i < limit; i++) {
|
||||||
|
if (!from[current]) break;
|
||||||
|
current = from[current];
|
||||||
|
|
||||||
|
if (cells.road[current]) {
|
||||||
|
if (segment.length) {
|
||||||
|
segment.push(current);
|
||||||
|
path.push(segment);
|
||||||
|
if (segment[0] !== end) cells.road[segment[0]] += score; // crossroad
|
||||||
|
if (current !== start) cells.road[current] += score; // crossroad
|
||||||
|
}
|
||||||
|
segment = [];
|
||||||
|
prev = current;
|
||||||
|
} else {
|
||||||
|
if (prev) segment.push(prev);
|
||||||
|
prev = null;
|
||||||
|
segment.push(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
cells.road[current] += score;
|
||||||
|
if (current === start) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment.length > 1) path.push(segment);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find water paths
|
||||||
|
function findOceanPath(start, exit = null, toRoute = null) {
|
||||||
|
const cells = pack.cells;
|
||||||
|
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||||
|
const cost = [], from = [];
|
||||||
|
queue.queue({e: start, p: 0});
|
||||||
|
|
||||||
|
while (queue.length) {
|
||||||
|
const next = queue.dequeue(), n = next.e, p = next.p;
|
||||||
|
if (toRoute && n !== start && cells.road[n]) return [from, n];
|
||||||
|
|
||||||
|
for (const c of cells.c[n]) {
|
||||||
|
if (cells.h[c] >= 20) continue; // ignore land cells
|
||||||
|
const dist2 = (cells.p[c][1] - cells.p[n][1]) ** 2 + (cells.p[c][0] - cells.p[n][0]) ** 2;
|
||||||
|
const totalCost = p + (cells.road[c] ? 1 + dist2 / 2 : dist2 + (cells.t[c] ? 1 : 100));
|
||||||
|
|
||||||
|
if (from[c] || totalCost >= cost[c]) continue;
|
||||||
|
from[c] = n;
|
||||||
|
if (c === exit) return [from, exit];
|
||||||
|
cost[c] = totalCost;
|
||||||
|
queue.queue({e: c, p: totalCost});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return [from, exit];
|
||||||
|
}
|
||||||
|
|
||||||
|
})));
|
||||||
377
modules/save-and-load.js
Normal file
|
|
@ -0,0 +1,377 @@
|
||||||
|
// Functions to save and load the map
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// download map as SVG or PNG file
|
||||||
|
function saveAsImage(type) {
|
||||||
|
console.time("saveAsImage");
|
||||||
|
|
||||||
|
// clone svg
|
||||||
|
const cloneEl = document.getElementById("map").cloneNode(true);
|
||||||
|
cloneEl.id = "fantasyMap";
|
||||||
|
document.getElementsByTagName("body")[0].appendChild(cloneEl);
|
||||||
|
const clone = d3.select("#fantasyMap");
|
||||||
|
|
||||||
|
if (type === "svg") clone.select("#viewbox").attr("transform", null); // reset transform to show whole map
|
||||||
|
if (layerIsOn("texture") && type === "png") clone.select("#texture").remove(); // no texture for png
|
||||||
|
|
||||||
|
// for each g element get inline style
|
||||||
|
const emptyG = clone.append("g").node();
|
||||||
|
const defaultStyles = window.getComputedStyle(emptyG);
|
||||||
|
clone.selectAll("g, #ruler > g > *, #scaleBar > text").each(function(d) {
|
||||||
|
const compStyle = window.getComputedStyle(this);
|
||||||
|
let style = "";
|
||||||
|
for (let i=0; i < compStyle.length; i++) {
|
||||||
|
const key = compStyle[i];
|
||||||
|
const value = compStyle.getPropertyValue(key);
|
||||||
|
// Firefox mask hack
|
||||||
|
if (key === "mask-image" && value !== defaultStyles.getPropertyValue(key)) {
|
||||||
|
style += "mask-image: url('#land');";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key === "cursor") continue; // cursor should be default
|
||||||
|
if (this.hasAttribute(key)) continue; // don't add style if there is the same attribute
|
||||||
|
if (value === defaultStyles.getPropertyValue(key)) continue;
|
||||||
|
style += key + ':' + value + ';';
|
||||||
|
}
|
||||||
|
if (style != "") this.setAttribute('style', style);
|
||||||
|
});
|
||||||
|
emptyG.remove();
|
||||||
|
|
||||||
|
// load fonts as dataURI so they will be available in downloaded svg/png
|
||||||
|
GFontToDataURI(getFontsToLoad()).then(cssRules => {
|
||||||
|
clone.select("defs").append("style").text(cssRules.join('\n'));
|
||||||
|
const svg_xml = (new XMLSerializer()).serializeToString(clone.node());
|
||||||
|
clone.remove();
|
||||||
|
const blob = new Blob([svg_xml], {type: 'image/svg+xml;charset=utf-8'});
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.target = "_blank";
|
||||||
|
|
||||||
|
if (type === "png") {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
canvas.width = svgWidth * pngResolutionInput.value;
|
||||||
|
canvas.height = svgHeight * pngResolutionInput.value;
|
||||||
|
const img = new Image();
|
||||||
|
img.src = url;
|
||||||
|
img.onload = function() {
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
|
link.download = "fantasy_map_" + Date.now() + ".png";
|
||||||
|
canvas.toBlob(function(blob) {
|
||||||
|
link.href = window.URL.createObjectURL(blob);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
window.setTimeout(function() {
|
||||||
|
canvas.remove();
|
||||||
|
window.URL.revokeObjectURL(link.href);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
link.download = "fantasy_map_" + Date.now() + ".svg";
|
||||||
|
link.href = url;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
|
||||||
|
console.timeEnd("saveAsImage");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// get non-standard fonts used for labels to fetch them from web
|
||||||
|
function getFontsToLoad() {
|
||||||
|
const webSafe = ["Georgia", "Times+New+Roman", "Comic+Sans+MS", "Lucida+Sans+Unicode", "Courier+New", "Verdana", "Arial", "Impact"];
|
||||||
|
|
||||||
|
const fontsInUse = []; // to store fonts currently in use
|
||||||
|
labels.selectAll("g").each(function() {
|
||||||
|
const font = this.dataset.font;
|
||||||
|
if (!font) return;
|
||||||
|
if (webSafe.includes(font)) return; // do not fetch web-safe fonts
|
||||||
|
if (!fontsInUse.includes(font)) fontsInUse.push(font);
|
||||||
|
});
|
||||||
|
return "https://fonts.googleapis.com/css?family=" + fontsInUse.join("|");
|
||||||
|
}
|
||||||
|
|
||||||
|
// code from Kaiido's answer https://stackoverflow.com/questions/42402584/how-to-use-google-fonts-in-canvas-when-drawing-dom-objects-in-svg
|
||||||
|
function GFontToDataURI(url) {
|
||||||
|
return fetch(url) // first fecth the embed stylesheet page
|
||||||
|
.then(resp => resp.text()) // we only need the text of it
|
||||||
|
.then(text => {
|
||||||
|
let s = document.createElement('style');
|
||||||
|
s.innerHTML = text;
|
||||||
|
document.head.appendChild(s);
|
||||||
|
let styleSheet = Array.prototype.filter.call(
|
||||||
|
document.styleSheets,
|
||||||
|
sS => sS.ownerNode === s)[0];
|
||||||
|
let FontRule = rule => {
|
||||||
|
let src = rule.style.getPropertyValue('src');
|
||||||
|
let url = src.split('url(')[1].split(')')[0];
|
||||||
|
return {rule: rule, src: src, url: url.substring(url.length - 1, 1)};
|
||||||
|
};
|
||||||
|
let fontRules = [], fontProms = [];
|
||||||
|
|
||||||
|
for (let r of styleSheet.cssRules) {
|
||||||
|
let fR = FontRule(r);
|
||||||
|
fontRules.push(fR);
|
||||||
|
fontProms.push(
|
||||||
|
fetch(fR.url) // fetch the actual font-file (.woff)
|
||||||
|
.then(resp => resp.blob())
|
||||||
|
.then(blob => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
let f = new FileReader();
|
||||||
|
f.onload = e => resolve(f.result);
|
||||||
|
f.readAsDataURL(blob);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(dataURL => {
|
||||||
|
return fR.rule.cssText.replace(fR.url, dataURL);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
document.head.removeChild(s); // clean up
|
||||||
|
return Promise.all(fontProms); // wait for all this has been done
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save in .map format
|
||||||
|
function saveMap() {
|
||||||
|
if (customization) {tip("Map cannot be saved when is in edit mode, please exit the mode and re-try", false, "error"); return;}
|
||||||
|
console.time("saveMap");
|
||||||
|
const date = new Date();
|
||||||
|
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
|
||||||
|
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
|
||||||
|
const params = [version, license, dateString, seed, graphWidth, graphHeight].join("|");
|
||||||
|
const options = [distanceUnit.value, distanceScale.value, areaUnit.value, heightUnit.value, heightExponent.value, temperatureScale.value,
|
||||||
|
barSize.value, barLabel.value, barBackOpacity.value, barBackColor.value, barPosX.value, barPosY.value, populationRate.value, urbanization.value,
|
||||||
|
equatorOutput.value, equidistanceOutput.value, temperatureEquatorOutput.value, temperaturePoleOutput.value, precOutput.value, JSON.stringify(winds)].join("|");
|
||||||
|
const coords = JSON.stringify(mapCoordinates);
|
||||||
|
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join("|");
|
||||||
|
const notesData = JSON.stringify(notes);
|
||||||
|
|
||||||
|
// set transform values to default
|
||||||
|
svg.attr("width", graphWidth).attr("height", graphHeight);
|
||||||
|
const transform = d3.zoomTransform(svg.node());
|
||||||
|
viewbox.attr("transform", null);
|
||||||
|
const svg_xml = (new XMLSerializer()).serializeToString(svg.node());
|
||||||
|
|
||||||
|
const gridGeneral = JSON.stringify({spacing:grid.spacing, cellsX:grid.cellsX, cellsY:grid.cellsY, boundary:grid.boundary, points:grid.points, features:grid.features});
|
||||||
|
const features = JSON.stringify(pack.features);
|
||||||
|
const cultures = JSON.stringify(pack.cultures);
|
||||||
|
const states = JSON.stringify(pack.states);
|
||||||
|
const burgs = JSON.stringify(pack.burgs);
|
||||||
|
|
||||||
|
const data = [params, options, coords, biomes, notesData, svg_xml,
|
||||||
|
gridGeneral, grid.cells.h, grid.cells.prec, grid.cells.f, grid.cells.t, grid.cells.temp,
|
||||||
|
features, cultures, states, burgs,
|
||||||
|
pack.cells.biome, pack.cells.burg, pack.cells.conf, pack.cells.culture, pack.cells.fl,
|
||||||
|
pack.cells.pop, pack.cells.r, pack.cells.road, pack.cells.s, pack.cells.state].join("\r\n");
|
||||||
|
const dataBlob = new Blob([data], {type: "text/plain"});
|
||||||
|
const dataURL = window.URL.createObjectURL(dataBlob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.download = "fantasy_map_" + Date.now() + ".map";
|
||||||
|
link.href = dataURL;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
// restore initial values
|
||||||
|
svg.attr("width", svgWidth).attr("height", svgHeight);
|
||||||
|
zoom.transform(svg, transform);
|
||||||
|
|
||||||
|
window.setTimeout(function() {window.URL.revokeObjectURL(dataURL);}, 2000);
|
||||||
|
console.timeEnd("saveMap");
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadFile(file, callback) {
|
||||||
|
console.time("loadMap");
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
fileReader.onload = function(fileLoadedEvent) {
|
||||||
|
const dataLoaded = fileLoadedEvent.target.result;
|
||||||
|
const data = dataLoaded.split("\r\n");
|
||||||
|
|
||||||
|
const mapVersion = data[0].split("|")[0] || data[0];
|
||||||
|
if (mapVersion === version) {parseLoadedData(data); return;}
|
||||||
|
|
||||||
|
const archive = "<a href='https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog' target='_blank'>archived version</a>";
|
||||||
|
const parsed = parseFloat(mapVersion);
|
||||||
|
let message = "", load = false;
|
||||||
|
if (isNaN(parsed) || data.length < 26 || !data[5]) {
|
||||||
|
message = `The file you are trying to load is not a valid .map file`;
|
||||||
|
} else if (parsed < 0.7) {
|
||||||
|
message = `The map version you are trying to load (${mapVersion}) is too old and cannot be updated to the current version.
|
||||||
|
<br>Please keep using an ${archive}`;
|
||||||
|
} else {
|
||||||
|
load = true;
|
||||||
|
message = `The map version (${mapVersion}) does not match the Generator version (${version}). The map will be auto-updated.
|
||||||
|
<br>In case of issues please keep using an ${archive} of the Generator`;
|
||||||
|
}
|
||||||
|
alertMessage.innerHTML = message;
|
||||||
|
$("#alert").dialog({title: "Version conflict", buttons: {
|
||||||
|
OK: function() {$(this).dialog("close"); if (load) parseLoadedData(data);}
|
||||||
|
}});
|
||||||
|
};
|
||||||
|
|
||||||
|
fileReader.readAsText(file, "UTF-8");
|
||||||
|
if (callback) callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLoadedData(data) {
|
||||||
|
closeDialogs();
|
||||||
|
|
||||||
|
void function parseParameters() {
|
||||||
|
const params = data[0].split("|");
|
||||||
|
if (params[3]) {seed = params[3]; optionsSeed.value = seed;}
|
||||||
|
if (params[4]) graphWidth = +params[4];
|
||||||
|
if (params[5]) graphHeight = +params[5];
|
||||||
|
}()
|
||||||
|
|
||||||
|
void function parseOptions() {
|
||||||
|
const options = data[1].split("|");
|
||||||
|
if (options[0]) distanceUnit.value = distanceUnitOutput.innerHTML = options[0];
|
||||||
|
if (options[1]) distanceScale.value = distanceScaleSlider.value = options[1];
|
||||||
|
if (options[2]) areaUnit.value = options[2];
|
||||||
|
if (options[3]) heightUnit.value= options[3];
|
||||||
|
if (options[4]) heightExponent.value = heightExponentSlider.value = options[4];
|
||||||
|
if (options[5]) temperatureScale.value = options[5];
|
||||||
|
if (options[6]) barSize.value = barSizeSlider.value = options[6];
|
||||||
|
if (options[7] !== undefined) barLabel.value = options[7];
|
||||||
|
if (options[8] !== undefined) barBackOpacity.value = options[8];
|
||||||
|
if (options[9]) barBackColor.value = options[9];
|
||||||
|
if (options[10]) barPosX.value = options[10];
|
||||||
|
if (options[11]) barPosY.value = options[11];
|
||||||
|
if (options[12]) populationRate.value = populationRateSlider.value = options[12];
|
||||||
|
if (options[13]) urbanization.value = urbanizationSlider.value = options[13];
|
||||||
|
if (options[14]) equatorInput.value = equatorOutput.value = options[14];
|
||||||
|
if (options[15]) equidistanceInput.value = equidistanceOutput.value = options[15];
|
||||||
|
if (options[16]) temperatureEquatorInput.value = temperatureEquatorOutput.value = options[16];
|
||||||
|
if (options[17]) temperaturePoleInput.value = temperaturePoleOutput.value = options[17];
|
||||||
|
if (options[18]) precInput.value = precOutput.value = options[18];
|
||||||
|
if (options[19]) winds = JSON.parse(options[19]);
|
||||||
|
}()
|
||||||
|
|
||||||
|
void function parseConfiguration() {
|
||||||
|
if (data[2]) mapCoordinates = JSON.parse(data[2]);
|
||||||
|
if (data[4]) notes = JSON.parse(data[4]);
|
||||||
|
|
||||||
|
const biomes = data[3].split("|");
|
||||||
|
const name = biomes[2].split(",");
|
||||||
|
if (name.length !== biomesData.name.length) {
|
||||||
|
console.error("Biomes data is not correct and will not be loaded");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
biomesData.color = biomes[0].split(",");
|
||||||
|
biomesData.habitability = biomes[1].split(",");
|
||||||
|
biomesData.name = name;
|
||||||
|
}()
|
||||||
|
|
||||||
|
void function replaceSVG() {
|
||||||
|
svg.remove();
|
||||||
|
document.body.insertAdjacentHTML("afterbegin", data[5]);
|
||||||
|
}()
|
||||||
|
|
||||||
|
void function redefineElements() {
|
||||||
|
svg = d3.select("#map");
|
||||||
|
defs = svg.select("#deftemp");
|
||||||
|
viewbox = svg.select("#viewbox");
|
||||||
|
scaleBar = svg.select("#scaleBar");
|
||||||
|
ocean = viewbox.select("#ocean");
|
||||||
|
oceanLayers = ocean.select("#oceanLayers");
|
||||||
|
oceanPattern = ocean.select("#oceanPattern");
|
||||||
|
lakes = viewbox.select("#lakes");
|
||||||
|
landmass = viewbox.select("#landmass");
|
||||||
|
texture = viewbox.select("#texture");
|
||||||
|
terrs = viewbox.select("#terrs");
|
||||||
|
biomes = viewbox.select("#biomes");
|
||||||
|
cells = viewbox.select("#cells");
|
||||||
|
gridOverlay = viewbox.select("#gridOverlay");
|
||||||
|
coordinates = viewbox.select("#coordinates");
|
||||||
|
compass = viewbox.select("#compass");
|
||||||
|
rivers = viewbox.select("#rivers");
|
||||||
|
terrain = viewbox.select("#terrain");
|
||||||
|
cults = viewbox.select("#cults");
|
||||||
|
regions = viewbox.select("#regions");
|
||||||
|
statesBody = regions.select("#statesBody");
|
||||||
|
statesHalo = regions.select("#statesHalo");
|
||||||
|
borders = viewbox.select("#borders");
|
||||||
|
routes = viewbox.select("#routes");
|
||||||
|
roads = routes.select("#roads");
|
||||||
|
trails = routes.select("#trails");
|
||||||
|
searoutes = routes.select("#searoutes");
|
||||||
|
temperature = viewbox.select("#temperature");
|
||||||
|
coastline = viewbox.select("#coastline");
|
||||||
|
prec = viewbox.select("#prec");
|
||||||
|
population = viewbox.select("#population");
|
||||||
|
labels = viewbox.select("#labels");
|
||||||
|
icons = viewbox.select("#icons");
|
||||||
|
burgIcons = icons.select("#burgIcons");
|
||||||
|
anchors = icons.select("#anchors");
|
||||||
|
markers = viewbox.select("#markers");
|
||||||
|
ruler = viewbox.select("#ruler");
|
||||||
|
debug = viewbox.select("#debug");
|
||||||
|
freshwater = lakes.select("#freshwater");
|
||||||
|
salt = lakes.select("#salt");
|
||||||
|
burgLabels = labels.select("#burgLabels");
|
||||||
|
}()
|
||||||
|
|
||||||
|
void function parseGridData() {
|
||||||
|
grid = JSON.parse(data[6]);
|
||||||
|
calculateVoronoi(grid, grid.points);
|
||||||
|
grid.cells.h = Uint8Array.from(data[7].split(","));
|
||||||
|
grid.cells.prec = Uint8Array.from(data[8].split(","));
|
||||||
|
grid.cells.f = Uint16Array.from(data[9].split(","));
|
||||||
|
grid.cells.t = Int8Array.from(data[10].split(","));
|
||||||
|
grid.cells.temp = Int8Array.from(data[11].split(","));
|
||||||
|
}()
|
||||||
|
|
||||||
|
void function parsePackData() {
|
||||||
|
pack = {};
|
||||||
|
reGraph();
|
||||||
|
reMarkFeatures();
|
||||||
|
pack.features = JSON.parse(data[12]);
|
||||||
|
pack.cultures = JSON.parse(data[13]);
|
||||||
|
pack.states = JSON.parse(data[14]);
|
||||||
|
pack.burgs = JSON.parse(data[15]);
|
||||||
|
|
||||||
|
pack.cells.biome = Uint8Array.from(data[16].split(","));
|
||||||
|
pack.cells.burg = Uint16Array.from(data[17].split(","));
|
||||||
|
pack.cells.conf = Uint8Array.from(data[18].split(","));
|
||||||
|
pack.cells.culture = Uint8Array.from(data[19].split(","));
|
||||||
|
pack.cells.fl = Uint16Array.from(data[20].split(","));
|
||||||
|
pack.cells.pop = Uint16Array.from(data[21].split(","));
|
||||||
|
pack.cells.r = Uint16Array.from(data[22].split(","));
|
||||||
|
pack.cells.road = Uint16Array.from(data[23].split(","));
|
||||||
|
pack.cells.s = Uint16Array.from(data[24].split(","));
|
||||||
|
pack.cells.state = Uint8Array.from(data[25].split(","));
|
||||||
|
}()
|
||||||
|
|
||||||
|
void function restoreLayersState() {
|
||||||
|
if (texture.style("display") !== "none" && texture.select("image").size()) turnButtonOn("toggleTexture"); else turnButtonOff("toggleTexture");
|
||||||
|
if (terrs.selectAll("*").size()) turnButtonOn("toggleHeight"); else turnButtonOff("toggleHeight");
|
||||||
|
if (biomes.selectAll("*").size()) turnButtonOn("toggleBiomes"); else turnButtonOff("toggleBiomes");
|
||||||
|
if (cells.selectAll("*").size()) turnButtonOn("toggleCells"); else turnButtonOff("toggleCells");
|
||||||
|
if (gridOverlay.selectAll("*").size()) turnButtonOn("toggleGrid"); else turnButtonOff("toggleGrid");
|
||||||
|
if (coordinates.selectAll("*").size()) turnButtonOn("toggleCoordinates"); else turnButtonOff("toggleCoordinates");
|
||||||
|
if (compass.style("display") !== "none" && compass.select("use").size()) turnButtonOn("toggleCompass"); else turnButtonOff("toggleCompass");
|
||||||
|
if (rivers.style("display") !== "none") turnButtonOn("toggleRivers"); else turnButtonOff("toggleRivers");
|
||||||
|
if (terrain.style("display") !== "none" && terrain.selectAll("*").size()) turnButtonOn("toggleRelief"); else turnButtonOff("toggleRelief");
|
||||||
|
if (cults.selectAll("*").size()) turnButtonOn("toggleCultures"); else turnButtonOff("toggleCultures");
|
||||||
|
if (statesBody.selectAll("*").size()) turnButtonOn("toggleStates"); else turnButtonOff("toggleStates");
|
||||||
|
if (borders.style("display") !== "none" && borders.selectAll("*").size()) turnButtonOn("toggleBorders"); else turnButtonOff("toggleBorders");
|
||||||
|
if (routes.style("display") !== "none" && routes.selectAll("path").size()) turnButtonOn("toggleRoutes"); else turnButtonOff("toggleRoutes");
|
||||||
|
if (temperature.selectAll("*").size()) turnButtonOn("toggleTemp"); else turnButtonOff("toggleTemp");
|
||||||
|
if (population.select("#rural").selectAll("*").size()) turnButtonOn("togglePopulation"); else turnButtonOff("togglePopulation");
|
||||||
|
if (prec.selectAll("circle").size()) turnButtonOn("togglePrec"); else turnButtonOff("togglePrec");
|
||||||
|
if (labels.style("display") !== "none") turnButtonOn("toggleLabels"); else turnButtonOff("toggleLabels");
|
||||||
|
if (icons.style("display") !== "none") turnButtonOn("toggleIcons"); else turnButtonOff("toggleIcons");
|
||||||
|
if (markers.style("display") !== "none") turnButtonOn("toggleMarkers"); else turnButtonOff("toggleMarkers");
|
||||||
|
if (ruler.style("display") !== "none") turnButtonOn("toggleRulers"); else turnButtonOff("toggleRulers");
|
||||||
|
if (scaleBar.style("display") !== "none") turnButtonOn("toggleScaleBar"); else turnButtonOff("toggleScaleBar");
|
||||||
|
}()
|
||||||
|
|
||||||
|
changeMapSize();
|
||||||
|
restoreDefaultEvents();
|
||||||
|
invokeActiveZooming();
|
||||||
|
tip("Map is loaded");
|
||||||
|
console.timeEnd("loadMap");
|
||||||
|
}
|
||||||
303
modules/ui/biomes-editor.js
Normal file
|
|
@ -0,0 +1,303 @@
|
||||||
|
"use strict";
|
||||||
|
function editBiomes() {
|
||||||
|
if (customization) return;
|
||||||
|
closeDialogs("#biomesEditor, .stable");
|
||||||
|
if (!layerIsOn("toggleBiomes")) toggleBiomes();
|
||||||
|
if (layerIsOn("toggleStates")) toggleStates();
|
||||||
|
if (layerIsOn("toggleCultures")) toggleCultures();
|
||||||
|
|
||||||
|
const body = document.getElementById("biomesBody");
|
||||||
|
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
|
||||||
|
refreshBiomesEditor();
|
||||||
|
|
||||||
|
if (modules.editBiomes) return;
|
||||||
|
modules.editBiomes = true;
|
||||||
|
|
||||||
|
$("#biomesEditor").dialog({
|
||||||
|
title: "Biomes Editor", width: fitContent(), close: closeBiomesEditor,
|
||||||
|
position: {my: "right top", at: "right-10 top+10", of: "svg"}
|
||||||
|
});
|
||||||
|
|
||||||
|
// add listeners
|
||||||
|
document.getElementById("biomesEditorRefresh").addEventListener("click", refreshBiomesEditor);
|
||||||
|
document.getElementById("biomesPercentage").addEventListener("click", togglePercentageMode);
|
||||||
|
document.getElementById("biomesManually").addEventListener("click", enterBiomesCustomizationMode);
|
||||||
|
document.getElementById("biomesManuallyApply").addEventListener("click", applyBiomesChange);
|
||||||
|
document.getElementById("biomesManuallyCancel").addEventListener("click", exitBiomesCustomizationMode);
|
||||||
|
document.getElementById("biomesRestore").addEventListener("click", restoreInitialBiomes);
|
||||||
|
document.getElementById("biomesRegenerateReliefIcons").addEventListener("click", regenerateIcons);
|
||||||
|
document.getElementById("biomesExport").addEventListener("click", downloadBiomesData);
|
||||||
|
|
||||||
|
function refreshBiomesEditor() {
|
||||||
|
biomesCollectStatistics();
|
||||||
|
biomesEditorAddLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
function biomesCollectStatistics() {
|
||||||
|
const cells = pack.cells;
|
||||||
|
biomesData.cells = new Uint32Array(biomesData.i.length);
|
||||||
|
biomesData.area = new Uint32Array(biomesData.i.length);
|
||||||
|
biomesData.rural = new Uint32Array(biomesData.i.length);
|
||||||
|
biomesData.urban = new Uint32Array(biomesData.i.length);
|
||||||
|
|
||||||
|
for (const i of cells.i) {
|
||||||
|
if (cells.h[i] < 20) continue;
|
||||||
|
const b = cells.biome[i];
|
||||||
|
biomesData.cells[b] += 1;
|
||||||
|
biomesData.area[b] += cells.area[i];
|
||||||
|
biomesData.rural[b] += cells.pop[i];
|
||||||
|
if (cells.burg[i]) biomesData.urban[b] += pack.burgs[cells.burg[i]].population;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function biomesEditorAddLines() {
|
||||||
|
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
|
||||||
|
const b = biomesData;
|
||||||
|
let lines = "", totalArea = 0, totalPopulation = 0;;
|
||||||
|
|
||||||
|
for (const i of b.i) {
|
||||||
|
if (!i) continue; // ignore marine (water) biome
|
||||||
|
const area = b.area[i] * distanceScale.value ** 2;
|
||||||
|
const rural = b.rural[i] * populationRate.value;
|
||||||
|
const urban = b.urban[i] * populationRate.value * urbanization.value;
|
||||||
|
const population = rural + urban;
|
||||||
|
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
|
||||||
|
totalArea += area;
|
||||||
|
totalPopulation += population;
|
||||||
|
|
||||||
|
lines += `<div class="states biomes" data-id="${i}" data-name="${b.name[i]}" data-habitability="${b.habitability[i]}"
|
||||||
|
data-cells=${b.cells[i]} data-area=${area} data-population=${population} data-color=${b.color[i]}>
|
||||||
|
<input data-tip="Biome color. Click to change" class="stateColor" type="color" value="${b.color[i]}">
|
||||||
|
<input data-tip="Biome name. Click and type to change" class="biomeName" value="${b.name[i]}" autocorrect="off" spellcheck="false">
|
||||||
|
<span data-tip="Biome habitability percent">%</span>
|
||||||
|
<input data-tip="Biome habitability percent. Click and set new value to change" type="number" min=0 max=999 step=1 class="biomeHabitability" value=${b.habitability[i]}>
|
||||||
|
<span data-tip="Cells count" class="icon-check-empty"></span>
|
||||||
|
<div data-tip="Cells count" class="biomeCells">${b.cells[i]}</div>
|
||||||
|
<span data-tip="Biome area" style="padding-right: 4px" class="icon-map-o"></span>
|
||||||
|
<div data-tip="Biome area" class="biomeArea">${si(area) + unit}</div>
|
||||||
|
<span data-tip="${populationTip}" class="icon-male"></span>
|
||||||
|
<div data-tip="${populationTip}" class="biomePopulation">${si(population)}</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
body.innerHTML = lines;
|
||||||
|
|
||||||
|
// update footer
|
||||||
|
biomesFooterBiomes.innerHTML = b.i.length - 1;
|
||||||
|
biomesFooterCells.innerHTML = pack.cells.h.filter(h => h >= 20).length;
|
||||||
|
biomesFooterArea.innerHTML = si(totalArea) + unit;
|
||||||
|
biomesFooterPopulation.innerHTML = si(totalPopulation);
|
||||||
|
biomesFooterArea.dataset.area = totalArea;
|
||||||
|
biomesFooterPopulation.dataset.population = totalPopulation;
|
||||||
|
|
||||||
|
// add listeners
|
||||||
|
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseenter", ev => biomeHighlightOn(ev)));
|
||||||
|
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseleave", ev => biomeHighlightOff(ev)));
|
||||||
|
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("click", selectBiomeOnLineClick));
|
||||||
|
body.querySelectorAll("div > input[type='color']").forEach(el => el.addEventListener("input", biomeChangeColor));
|
||||||
|
body.querySelectorAll("div > input.biomeName").forEach(el => el.addEventListener("input", biomeChangeName));
|
||||||
|
body.querySelectorAll("div > input.biomeHabitability").forEach(el => el.addEventListener("change", biomeChangeHabitability));
|
||||||
|
|
||||||
|
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
|
||||||
|
applySorting(biomesHeader);
|
||||||
|
$("#biomesEditor").dialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
function biomeHighlightOn(event) {
|
||||||
|
if (customization === 6) return;
|
||||||
|
const biome = +event.target.dataset.id;
|
||||||
|
biomes.select("#biome"+biome).raise().transition(animate).attr("stroke-width", 2).attr("stroke", "#cd4c11");
|
||||||
|
}
|
||||||
|
|
||||||
|
function biomeHighlightOff(event) {
|
||||||
|
if (customization === 6) return;
|
||||||
|
const biome = +event.target.dataset.id;
|
||||||
|
const color = biomesData.color[biome];
|
||||||
|
biomes.select("#biome"+biome).transition().attr("stroke-width", .7).attr("stroke", color);
|
||||||
|
}
|
||||||
|
|
||||||
|
function biomeChangeColor() {
|
||||||
|
const biome = +this.parentNode.dataset.id;
|
||||||
|
biomesData.color[biome] = this.value;
|
||||||
|
biomes.select("#biome"+biome).attr("fill", this.value).attr("stroke", this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function biomeChangeName() {
|
||||||
|
const biome = +this.parentNode.dataset.id;
|
||||||
|
this.parentNode.dataset.name = this.value;
|
||||||
|
biomesData.name[biome] = this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function biomeChangeHabitability() {
|
||||||
|
const biome = +this.parentNode.dataset.id;
|
||||||
|
const failed = isNaN(+this.value) || +this.value < 0 || +this.value > 999;
|
||||||
|
if (failed) {
|
||||||
|
this.value = biomesData.habitability[biome];
|
||||||
|
tip("Please provide a valid number in range 0-999", false, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
biomesData.habitability[biome] = +this.value;
|
||||||
|
this.parentNode.dataset.habitability = this.value;
|
||||||
|
recalculatePopulation();
|
||||||
|
refreshBiomesEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePercentageMode() {
|
||||||
|
if (body.dataset.type === "absolute") {
|
||||||
|
body.dataset.type = "percentage";
|
||||||
|
const totalCells = +biomesFooterCells.innerHTML;
|
||||||
|
const totalArea = +biomesFooterArea.dataset.area;
|
||||||
|
const totalPopulation = +biomesFooterPopulation.dataset.population;
|
||||||
|
|
||||||
|
body.querySelectorAll(":scope> div").forEach(function(el) {
|
||||||
|
el.querySelector(".biomeCells").innerHTML = rn(+el.dataset.cells / totalCells * 100) + "%";
|
||||||
|
el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100) + "%";
|
||||||
|
el.querySelector(".biomePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100) + "%";
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
body.dataset.type = "absolute";
|
||||||
|
biomesEditorAddLines();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function regenerateIcons() {
|
||||||
|
ReliefIcons();
|
||||||
|
if (!layerIsOn("toggleRelief")) toggleRelief();
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadBiomesData() {
|
||||||
|
const unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value;
|
||||||
|
let data = "Id,Biome,Color,Habitability,Cells,Area "+unit+",Population\n"; // headers
|
||||||
|
|
||||||
|
body.querySelectorAll(":scope > div").forEach(function(el) {
|
||||||
|
data += el.dataset.id + ",";
|
||||||
|
data += el.dataset.name + ",";
|
||||||
|
data += el.dataset.color + ",";
|
||||||
|
data += el.dataset.habitability + "%,";
|
||||||
|
data += el.dataset.cells + ",";
|
||||||
|
data += el.dataset.area + ",";
|
||||||
|
data += el.dataset.population + "\n";
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataBlob = new Blob([data], {type: "text/plain"});
|
||||||
|
const url = window.URL.createObjectURL(dataBlob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.download = "states_data" + Date.now() + ".csv";
|
||||||
|
link.href = url;
|
||||||
|
link.click();
|
||||||
|
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterBiomesCustomizationMode() {
|
||||||
|
if (!layerIsOn("toggleBiomes")) toggleBiomes();
|
||||||
|
customization = 6;
|
||||||
|
biomes.append("g").attr("id", "temp");
|
||||||
|
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
|
||||||
|
|
||||||
|
document.querySelectorAll("#biomesBottom > button").forEach(el => el.style.display = "none");
|
||||||
|
document.querySelectorAll("#biomesBottom > div").forEach(el => el.style.display = "block");
|
||||||
|
body.querySelector("div.biomes").classList.add("selected");
|
||||||
|
|
||||||
|
tip("Click on biome to select, drag the circle to change biome", true);
|
||||||
|
viewbox.style("cursor", "crosshair").call(d3.drag()
|
||||||
|
.on("drag", dragBiomeBrush))
|
||||||
|
.on("click", selectBiomeOnMapClick)
|
||||||
|
.on("touchmove mousemove", moveBiomeBrush);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectBiomeOnLineClick() {
|
||||||
|
if (customization !== 6) return;
|
||||||
|
const selected = body.querySelector("div.selected");
|
||||||
|
if (selected) selected.classList.remove("selected");
|
||||||
|
this.classList.add("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectBiomeOnMapClick() {
|
||||||
|
const point = d3.mouse(this);
|
||||||
|
const i = findCell(point[0], point[1]);
|
||||||
|
if (pack.cells.h[i] < 20) {tip("You cannot reassign water via biomes. Please edit the Heightmap to change water", false, "error"); return;}
|
||||||
|
|
||||||
|
const assigned = biomes.select("#temp").select("polygon[data-cell='"+i+"']");
|
||||||
|
const biome = assigned.size() ? +assigned.attr("data-biome") : pack.cells.biome[i];
|
||||||
|
|
||||||
|
body.querySelector("div.selected").classList.remove("selected");
|
||||||
|
body.querySelector("div[data-id='"+biome+"']").classList.add("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragBiomeBrush() {
|
||||||
|
const p = d3.mouse(this);
|
||||||
|
const r = +biomesManuallyBrush.value;
|
||||||
|
moveCircle(p[0], p[1], r);
|
||||||
|
|
||||||
|
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
|
||||||
|
const selection = found.filter(isLand);
|
||||||
|
if (selection) changeBiomeForSelection(selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// change region within selection
|
||||||
|
function changeBiomeForSelection(selection) {
|
||||||
|
const temp = biomes.select("#temp");
|
||||||
|
const selected = body.querySelector("div.selected");
|
||||||
|
|
||||||
|
const biomeNew = selected.dataset.id;
|
||||||
|
const color = biomesData.color[biomeNew];
|
||||||
|
|
||||||
|
selection.forEach(function(i) {
|
||||||
|
const exists = temp.select("polygon[data-cell='"+i+"']");
|
||||||
|
const biomeOld = exists.size() ? +exists.attr("data-biome") : pack.cells.biome[i];
|
||||||
|
if (biomeNew === biomeOld) return;
|
||||||
|
|
||||||
|
// change of append new element
|
||||||
|
if (exists.size()) exists.attr("data-biome", biomeNew).attr("fill", color).attr("stroke", color);
|
||||||
|
else temp.append("polygon").attr("data-cell", i).attr("data-biome", biomeNew).attr("points", getPackPolygon(i)).attr("fill", color).attr("stroke", color);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveBiomeBrush() {
|
||||||
|
showMainTip();
|
||||||
|
const point = d3.mouse(this);
|
||||||
|
const radius = +biomesManuallyBrush.value;
|
||||||
|
moveCircle(point[0], point[1], radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyBiomesChange() {
|
||||||
|
const changed = biomes.select("#temp").selectAll("polygon");
|
||||||
|
changed.each(function() {
|
||||||
|
const i = +this.dataset.cell;
|
||||||
|
const b = +this.dataset.biome;
|
||||||
|
pack.cells.biome[i] = b;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (changed.size()) {
|
||||||
|
drawBiomes();
|
||||||
|
refreshBiomesEditor();
|
||||||
|
}
|
||||||
|
exitBiomesCustomizationMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitBiomesCustomizationMode() {
|
||||||
|
customization = 0;
|
||||||
|
biomes.select("#temp").remove();
|
||||||
|
removeCircle();
|
||||||
|
document.querySelectorAll("#biomesBottom > button").forEach(el => el.style.display = "inline-block");
|
||||||
|
document.querySelectorAll("#biomesBottom > div").forEach(el => el.style.display = "none");
|
||||||
|
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
|
||||||
|
restoreDefaultEvents();
|
||||||
|
clearMainTip();
|
||||||
|
const selected = document.querySelector("#biomesBody > div.selected");
|
||||||
|
if (selected) selected.classList.remove("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreInitialBiomes() {
|
||||||
|
biomesData = applyDefaultBiomesSystem();
|
||||||
|
defineBiomes();
|
||||||
|
drawBiomes();
|
||||||
|
recalculatePopulation();
|
||||||
|
refreshBiomesEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeBiomesEditor() {
|
||||||
|
//biomes.on("mousemove", null).on("mouseleave", null);
|
||||||
|
exitBiomesCustomizationMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
336
modules/ui/burg-editor.js
Normal file
|
|
@ -0,0 +1,336 @@
|
||||||
|
"use strict";
|
||||||
|
function editBurg() {
|
||||||
|
if (customization) return;
|
||||||
|
closeDialogs(".stable");
|
||||||
|
if (!layerIsOn("toggleIcons")) toggleIcons();
|
||||||
|
if (!layerIsOn("toggleLabels")) toggleLabels();
|
||||||
|
|
||||||
|
const id = +d3.event.target.dataset.id;
|
||||||
|
elSelected = burgLabels.select("[data-id='" + id + "']");
|
||||||
|
burgLabels.selectAll("text").call(d3.drag().on("start", dragBurgLabel)).classed("draggable", true);
|
||||||
|
|
||||||
|
selectBurgGroup(event.target);
|
||||||
|
document.getElementById("burgNameInput").value = elSelected.text();
|
||||||
|
const my = elSelected.attr("id") == d3.event.target.id ? "center bottom" : "center top+10";
|
||||||
|
const at = elSelected.attr("id") == d3.event.target.id ? "top" : "bottom";
|
||||||
|
|
||||||
|
$("#burgEditor").dialog({
|
||||||
|
title: "Edit Burg: " + elSelected.text(), resizable: false,
|
||||||
|
position: {my, at, of: d3.event.target, collision: "fit"},
|
||||||
|
close: closeBurgEditor
|
||||||
|
});
|
||||||
|
|
||||||
|
if (modules.editBurg) return;
|
||||||
|
modules.editBurg = true;
|
||||||
|
|
||||||
|
// add listeners
|
||||||
|
document.getElementById("burgGroupShow").addEventListener("click", showGroupSection);
|
||||||
|
document.getElementById("burgGroupHide").addEventListener("click", hideGroupSection);
|
||||||
|
document.getElementById("burgSelectGroup").addEventListener("change", changeGroup);
|
||||||
|
document.getElementById("burgInputGroup").addEventListener("change", createNewGroup);
|
||||||
|
document.getElementById("burgAddGroup").addEventListener("click", toggleNewGroupInput);
|
||||||
|
document.getElementById("burgRemoveGroup").addEventListener("click", removeBurgsGroup);
|
||||||
|
|
||||||
|
document.getElementById("burgNameShow").addEventListener("click", showNameSection);
|
||||||
|
document.getElementById("burgNameHide").addEventListener("click", hideNameSection);
|
||||||
|
document.getElementById("burgNameInput").addEventListener("input", changeName);
|
||||||
|
document.getElementById("burgNameReCulture").addEventListener("click", generateNameCulture);
|
||||||
|
document.getElementById("burgNameReRandom").addEventListener("click", generateNameRandom);
|
||||||
|
|
||||||
|
document.getElementById("burgSeeInMFCG").addEventListener("click", openInMFCG);
|
||||||
|
document.getElementById("burgRelocate").addEventListener("click", toggleRelocateBurg);
|
||||||
|
document.getElementById("burglLegend").addEventListener("click", editBurgLegend);
|
||||||
|
document.getElementById("burgRemove").addEventListener("click", removeSelectedBurg);
|
||||||
|
|
||||||
|
function dragBurgLabel() {
|
||||||
|
const tr = parseTransform(this.getAttribute("transform"));
|
||||||
|
const dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y;
|
||||||
|
|
||||||
|
d3.event.on("drag", function() {
|
||||||
|
const x = d3.event.x, y = d3.event.y;
|
||||||
|
this.setAttribute("transform", `translate(${(dx+x)},${(dy+y)})`);
|
||||||
|
tip('Use dragging for fine-tuning only, to actually move burg use "Relocate" button', false, "warning");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectBurgGroup(node) {
|
||||||
|
const group = node.parentNode.id;
|
||||||
|
const select = document.getElementById("burgSelectGroup");
|
||||||
|
select.options.length = 0; // remove all options
|
||||||
|
|
||||||
|
burgLabels.selectAll("g").each(function() {
|
||||||
|
select.options.add(new Option(this.id, this.id, false, this.id === group));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showGroupSection() {
|
||||||
|
document.querySelectorAll("#burgEditor > button").forEach(el => el.style.display = "none");
|
||||||
|
document.getElementById("burgGroupSection").style.display = "inline-block";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideGroupSection() {
|
||||||
|
document.querySelectorAll("#burgEditor > button").forEach(el => el.style.display = "inline-block");
|
||||||
|
document.getElementById("burgGroupSection").style.display = "none";
|
||||||
|
document.getElementById("burgInputGroup").style.display = "none";
|
||||||
|
document.getElementById("burgInputGroup").value = "";
|
||||||
|
document.getElementById("burgSelectGroup").style.display = "inline-block";
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeGroup() {
|
||||||
|
const id = +elSelected.attr("data-id");
|
||||||
|
moveBurgToGroup(id, this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleNewGroupInput() {
|
||||||
|
if (burgInputGroup.style.display === "none") {
|
||||||
|
burgInputGroup.style.display = "inline-block";
|
||||||
|
burgInputGroup.focus();
|
||||||
|
burgSelectGroup.style.display = "none";
|
||||||
|
} else {
|
||||||
|
burgInputGroup.style.display = "none";
|
||||||
|
burgSelectGroup.style.display = "inline-block";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNewGroup() {
|
||||||
|
if (!this.value) {tip("Please provide a valid group name"); return;}
|
||||||
|
let group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
|
||||||
|
if (Number.isFinite(+group.charAt(0))) group = "g" + group;
|
||||||
|
|
||||||
|
if (document.getElementById(group)) {
|
||||||
|
tip("Element with this id already exists. Please provide a unique name", false, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = +elSelected.attr("data-id");
|
||||||
|
const oldGroup = elSelected.node().parentNode.id;
|
||||||
|
|
||||||
|
const label = document.querySelector("#burgLabels [data-id='" + id + "']");
|
||||||
|
const icon = document.querySelector("#burgIcons [data-id='" + id + "']");
|
||||||
|
const anchor = document.querySelector("#anchors [data-id='" + id + "']");
|
||||||
|
if (!label || !icon) {console.error("Cannot find label or icon elements"); return;}
|
||||||
|
|
||||||
|
const labelG = document.querySelector("#burgLabels > #"+oldGroup);
|
||||||
|
const iconG = document.querySelector("#burgIcons > #"+oldGroup);
|
||||||
|
const anchorG = document.querySelector("#anchors > #"+oldGroup);
|
||||||
|
|
||||||
|
// just rename if only 1 element left
|
||||||
|
const count = elSelected.node().parentNode.childElementCount;
|
||||||
|
if (oldGroup !== "cities" && oldGroup !== "towns" && count === 1) {
|
||||||
|
document.getElementById("burgSelectGroup").selectedOptions[0].remove();
|
||||||
|
document.getElementById("burgSelectGroup").options.add(new Option(group, group, false, true));
|
||||||
|
toggleNewGroupInput();
|
||||||
|
document.getElementById("burgInputGroup").value = "";
|
||||||
|
labelG.id = group;
|
||||||
|
iconG.id = group;
|
||||||
|
if (anchor) anchorG.id = group;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create new groups
|
||||||
|
document.getElementById("burgSelectGroup").options.add(new Option(group, group, false, true));
|
||||||
|
toggleNewGroupInput();
|
||||||
|
document.getElementById("burgInputGroup").value = "";
|
||||||
|
|
||||||
|
const newLabelG = document.querySelector("#burgLabels").appendChild(labelG.cloneNode(false));
|
||||||
|
newLabelG.id = group;
|
||||||
|
const newIconG = document.querySelector("#burgIcons").appendChild(iconG.cloneNode(false));
|
||||||
|
newIconG.id = group;
|
||||||
|
if (anchor) {
|
||||||
|
const newAnchorG = document.querySelector("#anchors").appendChild(anchorG.cloneNode(false));
|
||||||
|
newAnchorG.id = group;
|
||||||
|
}
|
||||||
|
moveBurgToGroup(id, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeBurgsGroup() {
|
||||||
|
const group = elSelected.node().parentNode;
|
||||||
|
const basic = group.id === "cities" || group.id === "towns";
|
||||||
|
|
||||||
|
const burgsInGroup = [];
|
||||||
|
for (let i=0; i < group.children.length; i++) {
|
||||||
|
burgsInGroup.push(+group.children[i].dataset.id);
|
||||||
|
}
|
||||||
|
const burgsToRemove = burgsInGroup.filter(b => !pack.burgs[b].capital);
|
||||||
|
const capital = burgsToRemove.length < burgsInGroup.length;
|
||||||
|
|
||||||
|
alertMessage.innerHTML = `Are you sure you want to remove
|
||||||
|
${basic || capital ? "all elements in the group" : "the entire burg group"}?
|
||||||
|
<br>Please note that capital burgs will not be deleted.
|
||||||
|
<br><br>Burgs to be removed: ${burgsToRemove.length}`;
|
||||||
|
$("#alert").dialog({resizable: false, title: "Remove route group",
|
||||||
|
buttons: {
|
||||||
|
Remove: function() {
|
||||||
|
$(this).dialog("close");
|
||||||
|
$("#burgEditor").dialog("close");
|
||||||
|
hideGroupSection();
|
||||||
|
burgsToRemove.forEach(b => removeBurg(b));
|
||||||
|
|
||||||
|
if (!basic && !capital) {
|
||||||
|
// entirely remove group
|
||||||
|
const labelG = document.querySelector("#burgLabels > #"+group.id);
|
||||||
|
const iconG = document.querySelector("#burgIcons > #"+group.id);
|
||||||
|
const anchorG = document.querySelector("#anchors > #"+group.id);
|
||||||
|
if (labelG) labelG.remove();
|
||||||
|
if (iconG) iconG.remove();
|
||||||
|
if (anchorG) anchorG.remove();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Cancel: function() {$(this).dialog("close");}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNameSection() {
|
||||||
|
document.querySelectorAll("#burgEditor > button").forEach(el => el.style.display = "none");
|
||||||
|
document.getElementById("burgNameSection").style.display = "inline-block";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideNameSection() {
|
||||||
|
document.querySelectorAll("#burgEditor > button").forEach(el => el.style.display = "inline-block");
|
||||||
|
document.getElementById("burgNameSection").style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeName() {
|
||||||
|
const id = +elSelected.attr("data-id");
|
||||||
|
pack.burgs[id].name = burgNameInput.value;
|
||||||
|
elSelected.text(burgNameInput.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateNameCulture() {
|
||||||
|
const id = +elSelected.attr("data-id");
|
||||||
|
const culture = pack.burgs[id].culture;
|
||||||
|
burgNameInput.value = Names.getCulture(culture);
|
||||||
|
changeName();
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateNameRandom() {
|
||||||
|
const base = rand(nameBase.length-1);
|
||||||
|
burgNameInput.value = Names.getBase(base);
|
||||||
|
changeName();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openInMFCG() {
|
||||||
|
const id = elSelected.attr("data-id");
|
||||||
|
const name = elSelected.text();
|
||||||
|
const cell = pack.burgs[id].cell;
|
||||||
|
const pop = rn(pack.burgs[id].population);
|
||||||
|
const size = Math.max(Math.min(pop, 65), 6);
|
||||||
|
|
||||||
|
// MFCG seed is FMG map seed + burg id padded to 4 chars with zeros
|
||||||
|
const s = seed + id.padStart(4, 0);
|
||||||
|
const hub = +pack.cells.road[cell] > 50;
|
||||||
|
const river = pack.cells.r[cell] ? 1 : 0;
|
||||||
|
const coast = +pack.burgs[id].port;
|
||||||
|
|
||||||
|
const half = rn(pop) % 2;
|
||||||
|
const most = (+id + rn(pop)) % 3 ? 1 : 0;
|
||||||
|
const walls = pop > 10 && half || pop > 20 && most || pop > 30 ? 1 : 0;;
|
||||||
|
const shanty = pop > 40 && half || pop > 60 && most || pop > 80 ? 1 : 0;
|
||||||
|
const temple = pop > 50 && half || pop > 80 && most || pop > 100 ? 1 : 0;
|
||||||
|
|
||||||
|
const url = `http://fantasycities.watabou.ru/?name=${name}&size=${size}&seed=${s}&hub=${hub}&random=0&continuous=0&river=${river}&coast=${coast}&citadel=${half}&plaza=${half}&temple=${temple}&walls=${walls}&shantytown=${shanty}`;
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRelocateBurg() {
|
||||||
|
const toggler = document.getElementById("toggleCells");
|
||||||
|
document.getElementById("burgRelocate").classList.toggle("pressed");
|
||||||
|
if (document.getElementById("burgRelocate").classList.contains("pressed")) {
|
||||||
|
viewbox.style("cursor", "crosshair").on("click", relocateBurgOnClick);
|
||||||
|
tip("Click on map to relocate burg. Hold Shift for continuous move", true);
|
||||||
|
if (!layerIsOn("toggleCells")) {toggleCells(); toggler.dataset.forced = true;}
|
||||||
|
} else {
|
||||||
|
clearMainTip();
|
||||||
|
viewbox.on("click", clicked).style("cursor", "default");
|
||||||
|
if (layerIsOn("toggleCells") && toggler.dataset.forced) {toggleCells(); toggler.dataset.forced = false;}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function relocateBurgOnClick() {
|
||||||
|
const cells = pack.cells;
|
||||||
|
const point = d3.mouse(this);
|
||||||
|
const cell = findCell(point[0], point[1]);
|
||||||
|
const id = +elSelected.attr("data-id");
|
||||||
|
const burg = pack.burgs[id];
|
||||||
|
|
||||||
|
if (cells.h[cell] < 20) {
|
||||||
|
tip("Cannot place burg into the water! Select a land cell", false, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cells.burg[cell] && cells.burg[cell] !== id) {
|
||||||
|
tip("There is already a burg in this cell. Please select a free cell", false, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newState = cells.state[cell];
|
||||||
|
const oldState = burg.state;
|
||||||
|
|
||||||
|
if (newState !== oldState && burg.capital) {
|
||||||
|
tip("Capital cannot be relocated into another state!", false, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// change UI
|
||||||
|
const x = rn(point[0], 2), y = rn(point[1], 2);
|
||||||
|
burgIcons.select("[data-id='" + id + "']").attr("transform", null).attr("cx", x).attr("cy", y);
|
||||||
|
burgLabels.select("text[data-id='" + id + "']").attr("transform", null).attr("x", x).attr("y", y);
|
||||||
|
const anchor = anchors.select("use[data-id='" + id+ "']");
|
||||||
|
if (anchor.size()) {
|
||||||
|
const size = anchor.attr("width");
|
||||||
|
const xa = rn(x - size * 0.47, 2);
|
||||||
|
const ya = rn(y - size * 0.47, 2);
|
||||||
|
anchor.attr("transform", null).attr("x", xa).attr("y", ya);
|
||||||
|
}
|
||||||
|
|
||||||
|
// change data
|
||||||
|
cells.burg[burg.cell] = 0;
|
||||||
|
cells.burg[cell] = id;
|
||||||
|
burg.cell = cell;
|
||||||
|
burg.state = newState;
|
||||||
|
burg.x = x;
|
||||||
|
burg.y = y;
|
||||||
|
if (burg.capital) pack.states[newState].center = burg.cell;
|
||||||
|
|
||||||
|
if (d3.event.shiftKey === false) toggleRelocateBurg();
|
||||||
|
}
|
||||||
|
|
||||||
|
function editBurgLegend() {
|
||||||
|
const id = elSelected.attr("data-id");
|
||||||
|
const name = elSelected.text();
|
||||||
|
editLegends("burg"+id, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSelectedBurg() {
|
||||||
|
const id = +elSelected.attr("data-id");
|
||||||
|
const capital = pack.burgs[id].capital;
|
||||||
|
|
||||||
|
if (capital) {
|
||||||
|
alertMessage.innerHTML = `You cannot remove the burg as it is a capital.<br><br>
|
||||||
|
You can change the capital using the Burgs Editor`;
|
||||||
|
$("#alert").dialog({resizable: false, title: "Remove burg",
|
||||||
|
buttons: {Ok: function() {$(this).dialog("close");}}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alertMessage.innerHTML = "Are you sure you want to remove the burg?";
|
||||||
|
$("#alert").dialog({resizable: false, title: "Remove burg",
|
||||||
|
buttons: {
|
||||||
|
Remove: function() {
|
||||||
|
$(this).dialog("close");
|
||||||
|
removeBurg(id); // see Editors module
|
||||||
|
$("#burgEditor").dialog("close");
|
||||||
|
},
|
||||||
|
Cancel: function() {$(this).dialog("close");}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeBurgEditor() {
|
||||||
|
document.getElementById("burgRelocate").classList.remove("pressed");
|
||||||
|
burgLabels.selectAll("text").call(d3.drag().on("drag", null)).classed("draggable", false);
|
||||||
|
unselect();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
332
modules/ui/burgs-editor.js
Normal file
|
|
@ -0,0 +1,332 @@
|
||||||
|
"use strict";
|
||||||
|
function editBurgs() {
|
||||||
|
if (customization) return;
|
||||||
|
closeDialogs("#burgsEditor, .stable");
|
||||||
|
if (!layerIsOn("toggleIcons")) toggleIcons();
|
||||||
|
if (!layerIsOn("toggleLabels")) toggleLabels();
|
||||||
|
|
||||||
|
const body = document.getElementById("burgsBody");
|
||||||
|
updateFilter();
|
||||||
|
burgsEditorAddLines();
|
||||||
|
|
||||||
|
if (modules.editBurgs) return;
|
||||||
|
modules.editBurgs = true;
|
||||||
|
|
||||||
|
$("#burgsEditor").dialog({title: "Burgs Editor", width: fitContent(), close: exitAddBurgMode,
|
||||||
|
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
|
||||||
|
});
|
||||||
|
|
||||||
|
// add listeners
|
||||||
|
document.getElementById("burgsEditorRefresh").addEventListener("click", burgsEditorAddLines);
|
||||||
|
document.getElementById("burgsFilterState").addEventListener("change", burgsEditorAddLines);
|
||||||
|
document.getElementById("burgsFilterCulture").addEventListener("change", burgsEditorAddLines);
|
||||||
|
document.getElementById("regenerateBurgNames").addEventListener("click", regenerateNames);
|
||||||
|
document.getElementById("addNewBurg").addEventListener("click", enterAddBurgMode);
|
||||||
|
document.getElementById("burgsExport").addEventListener("click", downloadBurgsData);
|
||||||
|
document.getElementById("burgNamesImport").addEventListener("click", e => burgsListToLoad.click());
|
||||||
|
document.getElementById("burgsListToLoad").addEventListener("change", importBurgNames);
|
||||||
|
document.getElementById("burgsRemoveAll").addEventListener("click", triggerAllBurgsRemove);
|
||||||
|
|
||||||
|
function updateFilter() {
|
||||||
|
const stateFilter = document.getElementById("burgsFilterState");
|
||||||
|
const selectedState = stateFilter.value || 1;
|
||||||
|
stateFilter.options.length = 0; // remove all options
|
||||||
|
stateFilter.options.add(new Option("all", -1, false, selectedState == -1));
|
||||||
|
pack.states.forEach(s => stateFilter.options.add(new Option(s.name, s.i, false, s.i == selectedState)));
|
||||||
|
|
||||||
|
const cultureFilter = document.getElementById("burgsFilterCulture");
|
||||||
|
const selectedCulture = cultureFilter.value || -1;
|
||||||
|
cultureFilter.options.length = 0; // remove all options
|
||||||
|
cultureFilter.options.add(new Option("all", -1, false, selectedCulture == -1));
|
||||||
|
pack.cultures.forEach(c => cultureFilter.options.add(new Option(c.name, c.i, false, c.i == selectedCulture)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// add line for each state
|
||||||
|
function burgsEditorAddLines() {
|
||||||
|
const selectedState = +document.getElementById("burgsFilterState").value;
|
||||||
|
const selectedCulture = +document.getElementById("burgsFilterCulture").value;
|
||||||
|
let filtered = pack.burgs.filter(b => b.i && !b.removed); // all valid burgs
|
||||||
|
if (selectedState != -1) filtered = filtered.filter(b => b.state === selectedState); // filtered by state
|
||||||
|
if (selectedCulture != -1) filtered = filtered.filter(b => b.culture === selectedCulture); // filtered by culture
|
||||||
|
|
||||||
|
const showState = selectedState == -1 ? "visible" : "hidden";
|
||||||
|
document.getElementById("burgStateHeader").style.display = `${selectedState == -1 ? "inline-block" : "none"}`;
|
||||||
|
body.innerHTML = "";
|
||||||
|
let lines = "", totalPopulation = 0;
|
||||||
|
|
||||||
|
for (const b of filtered) {
|
||||||
|
const population = rn(b.population * populationRate.value * urbanization.value);
|
||||||
|
totalPopulation += population;
|
||||||
|
const type = b.capital && b.port ? "a-capital-port" : b.capital ? "c-capital" : b.port ? "p-port" : "z-burg";
|
||||||
|
const state = pack.states[b.state].name;
|
||||||
|
const culture = pack.cultures[b.culture].name;
|
||||||
|
|
||||||
|
lines += `<div class="states" data-id=${b.i} data-name=${b.name} data-state=${state} data-culture=${culture} data-population=${population} data-type=${type}>
|
||||||
|
<span data-tip="Click to zoom into view" class="icon-dot-circled pointer"></span>
|
||||||
|
<input data-tip="Burg name. Click and type to change" class="burgName" value="${b.name}" autocorrect="off" spellcheck="false">
|
||||||
|
<span data-tip="Burg state" class="burgState ${showState}">${state}</span>
|
||||||
|
<select data-tip="Dominant culture. Click to change" class="stateCulture">${getCultureOptions(b.culture)}</select>
|
||||||
|
<span data-tip="Burg population" class="icon-male"></span>
|
||||||
|
<input data-tip="Burg population. Type to change" class="burgPopulation" value=${population}>
|
||||||
|
<div class="burgType">
|
||||||
|
<span data-tip="${b.capital ? ' This burg is a state capital' : 'Click to assign a capital status'}" class="icon-star-empty${b.capital ? '' : ' inactive pointer'}"></span>
|
||||||
|
<span data-tip="Click to toggle port status" class="icon-anchor pointer${b.port ? '' : ' inactive'}" style="font-size:.9em"></span>
|
||||||
|
</div>
|
||||||
|
<span data-tip="Remove burg" class="icon-trash-empty"></span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
body.insertAdjacentHTML("beforeend", lines);
|
||||||
|
|
||||||
|
// update footer
|
||||||
|
burgsFooterBurgs.innerHTML = filtered.length;
|
||||||
|
burgsFooterPopulation.innerHTML = filtered.length ? rn(totalPopulation / filtered.length) : 0;
|
||||||
|
|
||||||
|
// add listeners
|
||||||
|
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => burgHighlightOn(ev)));
|
||||||
|
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => burgHighlightOff(ev)));
|
||||||
|
body.querySelectorAll("div > input.burgName").forEach(el => el.addEventListener("input", changeBurgName));
|
||||||
|
body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.addEventListener("click", zoomIntoBurg));
|
||||||
|
body.querySelectorAll("div > select.burgCulture").forEach(el => el.addEventListener("click", updateCulturesList));
|
||||||
|
body.querySelectorAll("div > input.burgPopulation").forEach(el => el.addEventListener("change", changeBurgPopulation));
|
||||||
|
body.querySelectorAll("div > span.icon-star-empty").forEach(el => el.addEventListener("click", toggleCapitalStatus));
|
||||||
|
body.querySelectorAll("div > span.icon-anchor").forEach(el => el.addEventListener("click", togglePortStatus));
|
||||||
|
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", triggerBurgRemove));
|
||||||
|
|
||||||
|
applySorting(burgsHeader);
|
||||||
|
$("#burgsEditor").dialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCultureOptions(culture) {
|
||||||
|
let options = "";
|
||||||
|
pack.cultures.slice(1).forEach(c => options += `<option ${c.i === culture ? "selected" : ""} value="${c.i}">${c.name}</option>`);
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function burgHighlightOn(event) {
|
||||||
|
if (!layerIsOn("toggleLabels")) toggleLabels();
|
||||||
|
const burg = +event.target.dataset.id;
|
||||||
|
burgLabels.select("[data-id='" + burg + "']").classed("drag", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function burgHighlightOff() {
|
||||||
|
burgLabels.selectAll("text.drag").classed("drag", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeBurgName() {
|
||||||
|
if (this.value == "")tip("Please provide a name", false, "error");
|
||||||
|
const burg = +this.parentNode.dataset.id;
|
||||||
|
pack.burgs[burg].name = this.value;
|
||||||
|
this.parentNode.dataset.name = this.value;
|
||||||
|
const label = document.querySelector("#burgLabels [data-id='" + burg + "']");
|
||||||
|
if (label) label.innerHTML = this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomIntoBurg() {
|
||||||
|
const burg = +this.parentNode.dataset.id;
|
||||||
|
const label = document.querySelector("#burgLabels [data-id='" + burg + "']");
|
||||||
|
const x = +label.getAttribute("x"), y = +label.getAttribute("y");
|
||||||
|
zoomTo(x, y, 8, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCulturesList() {
|
||||||
|
const burg = +this.parentNode.dataset.id;
|
||||||
|
const v = +this.value;
|
||||||
|
pack.burgs[burg].culture = v;
|
||||||
|
this.parentNode.dataset.culture = pack.cultures[v].name;
|
||||||
|
this.options.length = 0;
|
||||||
|
pack.cultures.slice(1).forEach(c => this.options.add(new Option(c.name, c.i, false, c.i === v)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeBurgPopulation() {
|
||||||
|
const burg = +this.parentNode.dataset.id;
|
||||||
|
if (this.value == "" || isNaN(+this.value)) {
|
||||||
|
tip("Please provide a valid number", false, "error");
|
||||||
|
this.value = pack.burgs[burg].population * populationRate.value * urbanization.value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pack.burgs[burg].population = this.value / populationRate.value / urbanization.value;
|
||||||
|
this.parentNode.dataset.population = this.value;
|
||||||
|
|
||||||
|
const population = [];
|
||||||
|
body.querySelectorAll(":scope > div").forEach(el => population.push(+el.dataset.population));
|
||||||
|
pack.burgsFooterPopulation.innerHTML = rn(d3.mean(population));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCapitalStatus() {
|
||||||
|
const burg = +this.parentNode.parentNode.dataset.id, state = pack.burgs[burg].state;
|
||||||
|
if (pack.burgs[burg].capital) {tip("To change capital please assign capital status to another burg", false, "error"); return;}
|
||||||
|
if (!state) {tip("Neutral lands cannot have a capital", false, "error"); return;}
|
||||||
|
const old = pack.states[state].capital;
|
||||||
|
|
||||||
|
// change statuses
|
||||||
|
pack.states[state].capital = burg;
|
||||||
|
pack.burgs[burg].capital = true;
|
||||||
|
pack.burgs[old].capital = false;
|
||||||
|
moveBurgToGroup(burg, "cities");
|
||||||
|
moveBurgToGroup(old, "towns");
|
||||||
|
|
||||||
|
burgsEditorAddLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePortStatus() {
|
||||||
|
const burg = +this.parentNode.parentNode.dataset.id;
|
||||||
|
const anchor = document.querySelector("#anchors [data-id='" + burg + "']");
|
||||||
|
if (anchor) anchor.remove();
|
||||||
|
|
||||||
|
if (!pack.burgs[burg].port) {
|
||||||
|
const haven = pack.cells.haven[pack.burgs[burg].cell];
|
||||||
|
const port = haven ? pack.cells.f[haven] : -1;
|
||||||
|
if (!haven) tip("Port haven is not found, system won't be able to make a searoute", false, "warning");
|
||||||
|
pack.burgs[burg].port = port;
|
||||||
|
|
||||||
|
const g = pack.burgs[burg].capital ? "cities" : "towns";
|
||||||
|
const group = anchors.select("g#"+g);
|
||||||
|
const size = +group.attr("size");
|
||||||
|
group.append("use").attr("xlink:href", "#icon-anchor").attr("data-id", burg)
|
||||||
|
.attr("x", rn(pack.burgs[burg].x - size * .47, 2)).attr("y", rn(pack.burgs[burg].y - size * .47, 2))
|
||||||
|
.attr("width", size).attr("height", size);
|
||||||
|
} else {
|
||||||
|
pack.burgs[burg].port = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
burgsEditorAddLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerBurgRemove() {
|
||||||
|
const burg = +this.parentNode.dataset.id;
|
||||||
|
if (pack.burgs[burg].capital) {tip("You cannot remove the capital. Please change the capital first", false, "error"); return;}
|
||||||
|
removeBurg(burg);
|
||||||
|
burgsEditorAddLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
function regenerateNames() {
|
||||||
|
body.querySelectorAll(":scope > div").forEach(function(el) {
|
||||||
|
const burg = +el.dataset.id;
|
||||||
|
const culture = pack.burgs[burg].culture;
|
||||||
|
const name = Names.getCulture(culture);
|
||||||
|
el.querySelector(".burgName").value = name;
|
||||||
|
pack.burgs[burg].name = el.dataset.name = name;
|
||||||
|
burgLabels.select("[data-id='" + burg + "']").text(name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterAddBurgMode() {
|
||||||
|
if (this.classList.contains("pressed")) {exitAddBurgMode(); return;};
|
||||||
|
customization = 3;
|
||||||
|
this.classList.add("pressed");
|
||||||
|
tip("Click on the map to create a new burg. Hold Shift to add multiple", true);
|
||||||
|
viewbox.style("cursor", "crosshair").on("click", addBurgOnClick);
|
||||||
|
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addBurgOnClick() {
|
||||||
|
const point = d3.mouse(this);
|
||||||
|
const cell = findCell(point[0], point[1]);
|
||||||
|
if (pack.cells.h[cell] < 20) {tip("You cannot place state into the water. Please click on a land cell", false, "error"); return;}
|
||||||
|
if (pack.cells.burg[cell]) {tip("There is already a burg in this cell. Please select a free cell", false, "error"); return;}
|
||||||
|
addBurg(point); // add new burg
|
||||||
|
|
||||||
|
if (d3.event.shiftKey === false) {
|
||||||
|
exitAddBurgMode();
|
||||||
|
burgsEditorAddLines();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitAddBurgMode() {
|
||||||
|
customization = 0;
|
||||||
|
restoreDefaultEvents();
|
||||||
|
clearMainTip();
|
||||||
|
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
|
||||||
|
if (addBurgTool.classList.contains("pressed")) addBurgTool.classList.remove("pressed");
|
||||||
|
if (addNewBurg.classList.contains("pressed")) addNewBurg.classList.remove("pressed");
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadBurgsData() {
|
||||||
|
let data = "Id,Burg,State,Culture,Population,Capital,Port\n"; // headers
|
||||||
|
const valid = pack.burgs.filter(b => b.i && !b.removed); // all valid burgs
|
||||||
|
|
||||||
|
valid.forEach(b => {
|
||||||
|
data += b.i + ",";
|
||||||
|
data += b.name + ",";
|
||||||
|
data += pack.states[b.state].name + ",";
|
||||||
|
data += pack.cultures[b.culture].name + ",";
|
||||||
|
data += rn(b.population * populationRate.value * urbanization.value) + ",";
|
||||||
|
data += b.capital ? "capital," : ",";
|
||||||
|
data += b.port ? "port\n" : "\n";
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataBlob = new Blob([data], {type: "text/plain"});
|
||||||
|
const url = window.URL.createObjectURL(dataBlob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.download = "burgs_data" + Date.now() + ".csv";
|
||||||
|
link.href = url;
|
||||||
|
link.click();
|
||||||
|
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function importBurgNames() {
|
||||||
|
const el = document.getElementById("burgsListToLoad");
|
||||||
|
const fileToLoad = el.files[0];
|
||||||
|
el.value = "";
|
||||||
|
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
|
||||||
|
fileReader.onload = function(e) {
|
||||||
|
const dataLoaded = e.target.result;
|
||||||
|
const data = dataLoaded.split("\r\n");
|
||||||
|
if (!data.length) {tip("Cannot parse the list, please check the file format", false, "error"); return;}
|
||||||
|
|
||||||
|
let change = [];
|
||||||
|
let message = `Burgs will be renamed as below. Please confirm`;
|
||||||
|
message += `<div class="overflow-div"><table class="overflow-table"><tr><th>Id</th><th>Current name</th><th>New Name</th></tr>`;
|
||||||
|
|
||||||
|
for (let i=1; i < data.length && i < pack.burgs.length; i++) {
|
||||||
|
const v = data[i];
|
||||||
|
if (!v || v == pack.burgs[i].name) continue;
|
||||||
|
change.push({i, name: v});
|
||||||
|
message += `<tr><td style="width:20%">${i}</td><td style="width:40%">${pack.burgs[i].name}</td><td style="width:40%">${v}</td></tr>`;
|
||||||
|
}
|
||||||
|
message += `</tr></table></div>`;
|
||||||
|
alertMessage.innerHTML = message;
|
||||||
|
|
||||||
|
$("#alert").dialog({title: "Burgs bulk renaming", position: {my: "center", at: "center", of: "svg"},
|
||||||
|
buttons: {
|
||||||
|
Cancel: function() {$(this).dialog("close");},
|
||||||
|
Confirm: function() {
|
||||||
|
for (let i=0; i < change.length; i++) {
|
||||||
|
const id = change[i].i;
|
||||||
|
pack.burgs[id].name = change[i].name;
|
||||||
|
burgLabels.select("[data-id='" + id + "']").text(change[i].name);
|
||||||
|
}
|
||||||
|
$(this).dialog("close");
|
||||||
|
burgsEditorAddLines();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fileReader.readAsText(fileToLoad, "UTF-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerAllBurgsRemove() {
|
||||||
|
alertMessage.innerHTML = `Are you sure you want to remove all burgs except of capitals?
|
||||||
|
<br>To remove a capital you have to remove its state first`;
|
||||||
|
$("#alert").dialog({resizable: false, title: "Remove all burgs",
|
||||||
|
buttons: {
|
||||||
|
Remove: function() {
|
||||||
|
$(this).dialog("close");
|
||||||
|
removeAllBurgs();
|
||||||
|
},
|
||||||
|
Cancel: function() {$(this).dialog("close");}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAllBurgs() {
|
||||||
|
pack.burgs.filter(b => b.i && !b.capital).forEach(b => removeBurg(b.i));
|
||||||
|
burgsEditorAddLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
435
modules/ui/cultures-editor.js
Normal file
|
|
@ -0,0 +1,435 @@
|
||||||
|
"use strict";
|
||||||
|
function editCultures() {
|
||||||
|
if (customization) return;
|
||||||
|
closeDialogs("#culturesEditor, .stable");
|
||||||
|
if (!layerIsOn("toggleCultures")) toggleCultures();
|
||||||
|
if (layerIsOn("toggleStates")) toggleStates();
|
||||||
|
if (layerIsOn("toggleBiomes")) toggleBiomes();
|
||||||
|
|
||||||
|
const body = document.getElementById("culturesBody");
|
||||||
|
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
|
||||||
|
drawCultureCenters();
|
||||||
|
refreshCulturesEditor();
|
||||||
|
|
||||||
|
if (modules.editCultures) return;
|
||||||
|
modules.editCultures = true;
|
||||||
|
|
||||||
|
$("#culturesEditor").dialog({
|
||||||
|
title: "Cultures Editor", width: fitContent(), close: closeCulturesEditor,
|
||||||
|
position: {my: "right top", at: "right-10 top+10", of: "svg"}
|
||||||
|
});
|
||||||
|
|
||||||
|
// add listeners
|
||||||
|
document.getElementById("culturesEditorRefresh").addEventListener("click", refreshCulturesEditor);
|
||||||
|
document.getElementById("culturesPercentage").addEventListener("click", togglePercentageMode);
|
||||||
|
document.getElementById("culturesRecalculate").addEventListener("click", recalculateCultures);
|
||||||
|
document.getElementById("culturesManually").addEventListener("click", enterCultureManualAssignent);
|
||||||
|
document.getElementById("culturesManuallyApply").addEventListener("click", applyCultureManualAssignent);
|
||||||
|
document.getElementById("culturesManuallyCancel").addEventListener("click", exitCulturesManualAssignment);
|
||||||
|
document.getElementById("culturesEditNamesBase").addEventListener("click", editNamesbase);
|
||||||
|
document.getElementById("culturesAdd").addEventListener("click", addCulture);
|
||||||
|
document.getElementById("culturesExport").addEventListener("click", downloadCulturesData);
|
||||||
|
|
||||||
|
function refreshCulturesEditor() {
|
||||||
|
culturesCollectStatistics();
|
||||||
|
culturesEditorAddLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
function culturesCollectStatistics() {
|
||||||
|
const cells = pack.cells, cultures = pack.cultures;
|
||||||
|
cultures.forEach(c => c.cells = c.area = c.rural = c.urban = 0);
|
||||||
|
|
||||||
|
for (const i of cells.i) {
|
||||||
|
if (cells.h[i] < 20) continue;
|
||||||
|
const c = cells.culture[i];
|
||||||
|
cultures[c].cells += 1;
|
||||||
|
cultures[c].area += cells.area[i];
|
||||||
|
cultures[c].rural += cells.pop[i];
|
||||||
|
if (cells.burg[i]) cultures[c].urban += pack.burgs[cells.burg[i]].population;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add line for each culture
|
||||||
|
function culturesEditorAddLines() {
|
||||||
|
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
|
||||||
|
let lines = "", totalArea = 0, totalPopulation = 0;
|
||||||
|
|
||||||
|
for (const c of pack.cultures) {
|
||||||
|
if (c.removed) continue;
|
||||||
|
const area = c.area * (distanceScale.value ** 2);
|
||||||
|
const rural = c.rural * populationRate.value;
|
||||||
|
const urban = c.urban * populationRate.value * urbanization.value;
|
||||||
|
const population = rural + urban;
|
||||||
|
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
|
||||||
|
totalArea += area;
|
||||||
|
totalPopulation += population;
|
||||||
|
|
||||||
|
if (!c.i) {
|
||||||
|
// Uncultured (neutral) line
|
||||||
|
lines += `<div class="states" data-id=${c.i} data-name="${c.name}" data-color="" data-cells=${c.cells}
|
||||||
|
data-area=${area} data-population=${population} data-base=${c.base} data-type="" data-expansionism="">
|
||||||
|
<input class="stateColor placeholder" type="color">
|
||||||
|
<input data-tip="Culture name. Click and type to change" class="cultureName italic" value="${c.name}" autocorrect="off" spellcheck="false">
|
||||||
|
<span data-tip="Cells count" class="icon-check-empty"></span>
|
||||||
|
<div data-tip="Cells count" class="stateCells">${c.cells}</div>
|
||||||
|
<span class="icon-resize-full placeholder"></span>
|
||||||
|
<input class="statePower placeholder" type="number">
|
||||||
|
<select class="cultureType placeholder">${getTypeOptions(c.type)}</select>
|
||||||
|
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o"></span>
|
||||||
|
<div data-tip="Culture area" class="biomeArea">${si(area) + unit}</div>
|
||||||
|
<span data-tip="${populationTip}" class="icon-male"></span>
|
||||||
|
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
|
||||||
|
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw"></span>
|
||||||
|
<select data-tip="Culture namesbase. Click to change" class="cultureBase">${getBaseOptions(c.base)}</select>
|
||||||
|
</div>`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
lines += `<div class="states cultures" data-id=${c.i} data-name="${c.name}" data-color="${c.color}" data-cells=${c.cells}
|
||||||
|
data-area=${area} data-population=${population} data-base=${c.base} data-type=${c.type} data-expansionism=${c.expansionism}>
|
||||||
|
<input data-tip="Culture color. Click to change" class="stateColor" type="color" value="${c.color}">
|
||||||
|
<input data-tip="Culture name. Click and type to change" class="cultureName" value="${c.name}" autocorrect="off" spellcheck="false">
|
||||||
|
<span data-tip="Cells count" class="icon-check-empty"></span>
|
||||||
|
<div data-tip="Cells count" class="stateCells">${c.cells}</div>
|
||||||
|
<span data-tip="Culture expansionism (defines competitive size)" class="icon-resize-full"></span>
|
||||||
|
<input data-tip="Expansionism (defines competitive size). Change to re-calculate cultures based on new value" class="statePower" type="number" min=0 max=99 step=.1 value=${c.expansionism}>
|
||||||
|
<select data-tip="Culture type. Change to re-calculate cultures based on new value" class="cultureType">${getTypeOptions(c.type)}</select>
|
||||||
|
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o"></span>
|
||||||
|
<div data-tip="Culture area" class="biomeArea">${si(area) + unit}</div>
|
||||||
|
<span data-tip="${populationTip}" class="icon-male"></span>
|
||||||
|
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
|
||||||
|
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw"></span>
|
||||||
|
<select data-tip="Culture namesbase. Change and then click on the Re-generate button to get new names" class="cultureBase">${getBaseOptions(c.base)}</select>
|
||||||
|
<span data-tip="Remove culture" class="icon-trash-empty"></span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
body.innerHTML = lines;
|
||||||
|
|
||||||
|
// update footer
|
||||||
|
culturesFooterCultures.innerHTML = pack.cultures.filter(c => c.i && !c.removed).length;
|
||||||
|
culturesFooterCells.innerHTML = pack.cells.h.filter(h => h >= 20).length;
|
||||||
|
culturesFooterArea.innerHTML = si(totalArea) + unit;
|
||||||
|
culturesFooterPopulation.innerHTML = si(totalPopulation);
|
||||||
|
culturesFooterArea.dataset.area = totalArea;
|
||||||
|
culturesFooterPopulation.dataset.population = totalPopulation;
|
||||||
|
|
||||||
|
// add listeners
|
||||||
|
body.querySelectorAll("div.cultures").forEach(el => el.addEventListener("mouseenter", ev => cultureHighlightOn(ev)));
|
||||||
|
body.querySelectorAll("div.cultures").forEach(el => el.addEventListener("mouseleave", ev => cultureHighlightOff(ev)));
|
||||||
|
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectCultureOnLineClick));
|
||||||
|
body.querySelectorAll("div > input[type='color']").forEach(el => el.addEventListener("input", cultureChangeColor));
|
||||||
|
body.querySelectorAll("div > input.cultureName").forEach(el => el.addEventListener("input", cultureChangeName));
|
||||||
|
body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", cultureChangeExpansionism));
|
||||||
|
body.querySelectorAll("div > select.cultureType").forEach(el => el.addEventListener("change", cultureChangeType));
|
||||||
|
body.querySelectorAll("div > select.cultureBase").forEach(el => el.addEventListener("click", updateBaseOptions));
|
||||||
|
body.querySelectorAll("div > span.icon-arrows-cw").forEach(el => el.addEventListener("click", cultureRegenerateBurgs));
|
||||||
|
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", cultureRemove));
|
||||||
|
|
||||||
|
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
|
||||||
|
applySorting(culturesHeader);
|
||||||
|
$("#culturesEditor").dialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTypeOptions(type) {
|
||||||
|
let options = "";
|
||||||
|
const types = ["Generic", "River", "Lake", "Naval", "Nomadic", "Hunting", "Highland"];
|
||||||
|
types.forEach(t => options += `<option ${type === t ? "selected" : ""} value="${t}">${t}</option>`);
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBaseOptions(base) {
|
||||||
|
let options = "";
|
||||||
|
nameBases.forEach((n, i) => options += `<option ${base === i ? "selected" : ""} value="${i}">${n.name}</option>`);
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cultureHighlightOn(event) {
|
||||||
|
if (customization === 4) return;
|
||||||
|
const culture = +event.target.dataset.id;
|
||||||
|
const color = d3.interpolateLab(pack.cultures[culture].color, "#ff0000")(.8)
|
||||||
|
cults.select("#culture"+culture).raise().transition(animate).attr("stroke-width", 3).attr("stroke", color);
|
||||||
|
debug.select("#cultureCenter"+culture).raise().transition(animate).attr("r", 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cultureHighlightOff(event) {
|
||||||
|
if (customization === 4) return;
|
||||||
|
const culture = +event.target.dataset.id;
|
||||||
|
cults.select("#culture"+culture).transition().attr("stroke-width", .7).attr("stroke", pack.cultures[culture].color);
|
||||||
|
debug.select("#cultureCenter"+culture).transition().attr("r", 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cultureChangeColor() {
|
||||||
|
const culture = +this.parentNode.dataset.id;
|
||||||
|
pack.cultures[culture].color = this.value;
|
||||||
|
cults.select("#culture"+culture).attr("fill", this.value).attr("stroke", this.value);
|
||||||
|
debug.select("#cultureCenter"+culture).attr("fill", this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cultureChangeName() {
|
||||||
|
const culture = +this.parentNode.dataset.id;
|
||||||
|
this.parentNode.dataset.name = this.value;
|
||||||
|
pack.cultures[culture].name = this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cultureChangeExpansionism() {
|
||||||
|
const culture = +this.parentNode.dataset.id;
|
||||||
|
this.parentNode.dataset.expansionism = this.value;
|
||||||
|
pack.cultures[culture].expansionism = +this.value;
|
||||||
|
recalculateCultures();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cultureChangeType() {
|
||||||
|
const culture = +this.parentNode.dataset.id;
|
||||||
|
this.parentNode.dataset.type = this.value;
|
||||||
|
pack.cultures[culture].type = this.value;
|
||||||
|
recalculateCultures();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBaseOptions() {
|
||||||
|
const culture = +this.parentNode.dataset.id;
|
||||||
|
const v = +this.value;
|
||||||
|
this.parentNode.dataset.base = pack.cultures[culture].base = v;
|
||||||
|
this.options.length = 0;
|
||||||
|
nameBases.forEach((b, i) => this.options.add(new Option(b.name, i, false, i === v)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cultureRegenerateBurgs() {
|
||||||
|
if (customization === 4) return;
|
||||||
|
const culture = +this.parentNode.dataset.id;
|
||||||
|
const cBurgs = pack.burgs.filter(b => b.culture === culture);
|
||||||
|
cBurgs.forEach(b => {
|
||||||
|
b.name = Names.getCulture(culture);
|
||||||
|
labels.select("[data-id='" + b.i +"']").text(b.name);
|
||||||
|
});
|
||||||
|
tip(`Names for ${cBurgs.length} burgs are re-generated`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cultureRemove() {
|
||||||
|
if (customization === 4) return;
|
||||||
|
const culture = +this.parentNode.dataset.id;
|
||||||
|
cults.select("#culture"+culture).remove();
|
||||||
|
debug.select("#cultureCenter"+culture).remove();
|
||||||
|
|
||||||
|
pack.burgs.filter(b => b.culture === culture).forEach(b => b.culture = 0);
|
||||||
|
pack.cells.culture.forEach((c, i) => {if(c === culture) pack.cells.culture[i] = 0;});
|
||||||
|
pack.cultures[culture].removed = true;
|
||||||
|
|
||||||
|
refreshCulturesEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawCultureCenters() {
|
||||||
|
const tooltip = 'Drag to move the culture center (ancestral home)';
|
||||||
|
debug.select("#cultureCenters").remove();
|
||||||
|
const cultureCenters = debug.append("g").attr("id", "cultureCenters");
|
||||||
|
|
||||||
|
const data = pack.cultures.filter(c => c.i && !c.removed);
|
||||||
|
cultureCenters.selectAll("circle").data(data).enter().append("circle")
|
||||||
|
.attr("id", d => "cultureCenter"+d.i).attr("data-id", d => d.i)
|
||||||
|
.attr("r", 6).attr("fill", d => d.color)
|
||||||
|
.attr("cx", d => pack.cells.p[d.center][0]).attr("cy", d => pack.cells.p[d.center][1])
|
||||||
|
.on("mouseenter", d => {tip(tooltip, true); body.querySelector(`div[data-id='${d.i}']`).classList.add("selected"); cultureHighlightOn(event);})
|
||||||
|
.on("mouseleave", d => {tip('', true); body.querySelector(`div[data-id='${d.i}']`).classList.remove("selected"); cultureHighlightOff(event);})
|
||||||
|
.call(d3.drag().on("start", cultureCenterDrag));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cultureCenterDrag() {
|
||||||
|
const el = d3.select(this);
|
||||||
|
const c = +this.id.slice(13);
|
||||||
|
d3.event.on("drag", () => {
|
||||||
|
el.attr("cx", d3.event.x).attr("cy", d3.event.y);
|
||||||
|
const cell = findCell(d3.event.x, d3.event.y);
|
||||||
|
if (pack.cells.h[cell] < 20) return; // ignore dragging on water
|
||||||
|
pack.cultures[c].center = cell;
|
||||||
|
recalculateCultures();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePercentageMode() {
|
||||||
|
if (body.dataset.type === "absolute") {
|
||||||
|
body.dataset.type = "percentage";
|
||||||
|
const totalCells = +culturesFooterCells.innerHTML;
|
||||||
|
const totalArea = +culturesFooterArea.dataset.area;
|
||||||
|
const totalPopulation = +culturesFooterPopulation.dataset.population;
|
||||||
|
|
||||||
|
body.querySelectorAll(":scope > div").forEach(function(el) {
|
||||||
|
el.querySelector(".stateCells").innerHTML = rn(+el.dataset.cells / totalCells * 100) + "%";
|
||||||
|
el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100) + "%";
|
||||||
|
el.querySelector(".culturePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100) + "%";
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
body.dataset.type = "absolute";
|
||||||
|
culturesEditorAddLines();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-calculate cultures
|
||||||
|
function recalculateCultures() {
|
||||||
|
pack.cells.culture = new Int8Array(pack.cells.i.length);
|
||||||
|
pack.cultures.forEach(function(c) {
|
||||||
|
if (!c.i || c.removed) return;
|
||||||
|
pack.cells.culture[c.center] = c.i;
|
||||||
|
});
|
||||||
|
Cultures.expand();
|
||||||
|
drawCultures();
|
||||||
|
pack.burgs.forEach(b => b.culture = pack.cells.culture[b.cell]);
|
||||||
|
refreshCulturesEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterCultureManualAssignent() {
|
||||||
|
if (!layerIsOn("toggleCultures")) toggleCultures();
|
||||||
|
customization = 4;
|
||||||
|
cults.append("g").attr("id", "temp");
|
||||||
|
document.querySelectorAll("#culturesBottom > button").forEach(el => el.style.display = "none");
|
||||||
|
document.getElementById("culturesManuallyButtons").style.display = "inline-block";
|
||||||
|
debug.select("#cultureCenters").style("display", "none");
|
||||||
|
|
||||||
|
tip("Click on culture to select, drag the circle to change culture", true);
|
||||||
|
viewbox.style("cursor", "crosshair").call(d3.drag()
|
||||||
|
.on("drag", dragCultureBrush))
|
||||||
|
.on("click", selectCultureOnMapClick)
|
||||||
|
.on("touchmove mousemove", moveCultureBrush);
|
||||||
|
|
||||||
|
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
|
||||||
|
body.querySelector("div").classList.add("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCultureOnLineClick(i) {
|
||||||
|
if (customization !== 4) return;
|
||||||
|
body.querySelector("div.selected").classList.remove("selected");
|
||||||
|
this.classList.add("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCultureOnMapClick() {
|
||||||
|
const point = d3.mouse(this);
|
||||||
|
const i = findCell(point[0], point[1]);
|
||||||
|
if (pack.cells.h[i] < 20) return;
|
||||||
|
|
||||||
|
const assigned = cults.select("#temp").select("polygon[data-cell='"+i+"']");
|
||||||
|
const culture = assigned.size() ? +assigned.attr("data-culture") : pack.cells.culture[i];
|
||||||
|
|
||||||
|
body.querySelector("div.selected").classList.remove("selected");
|
||||||
|
body.querySelector("div[data-id='"+culture+"']").classList.add("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragCultureBrush() {
|
||||||
|
const p = d3.mouse(this);
|
||||||
|
const r = +culturesManuallyBrush.value;
|
||||||
|
moveCircle(p[0], p[1], r);
|
||||||
|
|
||||||
|
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
|
||||||
|
const selection = found.filter(isLand);
|
||||||
|
if (selection) changeCultureForSelection(selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// change culture within selection
|
||||||
|
function changeCultureForSelection(selection) {
|
||||||
|
const temp = cults.select("#temp");
|
||||||
|
const selected = body.querySelector("div.selected");
|
||||||
|
|
||||||
|
const cultureNew = +selected.dataset.id;
|
||||||
|
const color = pack.cultures[cultureNew].color;
|
||||||
|
|
||||||
|
selection.forEach(function(i) {
|
||||||
|
const exists = temp.select("polygon[data-cell='"+i+"']");
|
||||||
|
const cultureOld = exists.size() ? +exists.attr("data-culture") : pack.cells.culture[i];
|
||||||
|
if (cultureNew === cultureOld) return;
|
||||||
|
|
||||||
|
// change of append new element
|
||||||
|
if (exists.size()) exists.attr("data-culture", cultureNew).attr("fill", color).attr("stroke", color);
|
||||||
|
else temp.append("polygon").attr("data-cell", i).attr("data-culture", cultureNew).attr("points", getPackPolygon(i)).attr("fill", color).attr("stroke", color);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveCultureBrush() {
|
||||||
|
showMainTip();
|
||||||
|
const point = d3.mouse(this);
|
||||||
|
const radius = +culturesManuallyBrush.value;
|
||||||
|
moveCircle(point[0], point[1], radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCultureManualAssignent() {
|
||||||
|
const changed = cults.select("#temp").selectAll("polygon");
|
||||||
|
changed.each(function() {
|
||||||
|
const i = +this.dataset.cell;
|
||||||
|
const c = +this.dataset.culture;
|
||||||
|
pack.cells.culture[i] = c;
|
||||||
|
if (pack.cells.burg[i]) pack.burgs[pack.cells.burg[i]].culture = c;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (changed.size()) {
|
||||||
|
drawCultures();
|
||||||
|
refreshCulturesEditor();
|
||||||
|
}
|
||||||
|
exitCulturesManualAssignment();
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitCulturesManualAssignment() {
|
||||||
|
customization = 0;
|
||||||
|
cults.select("#temp").remove();
|
||||||
|
removeCircle();
|
||||||
|
document.querySelectorAll("#culturesBottom > button").forEach(el => el.style.display = "inline-block");
|
||||||
|
document.getElementById("culturesManuallyButtons").style.display = "none";
|
||||||
|
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
|
||||||
|
debug.select("#cultureCenters").style("display", null);
|
||||||
|
restoreDefaultEvents();
|
||||||
|
clearMainTip();
|
||||||
|
const selected = body.querySelector("div.selected");
|
||||||
|
if (selected) selected.classList.remove("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCulture() {
|
||||||
|
const defaultCultures = Cultures.getDefault();
|
||||||
|
let culture, base, name;
|
||||||
|
if (pack.cultures.length < defaultCultures.length) {
|
||||||
|
// add one of the default cultures
|
||||||
|
culture = pack.cultures.length;
|
||||||
|
base = defaultCultures[culture].base;
|
||||||
|
name = defaultCultures[culture].name;
|
||||||
|
} else {
|
||||||
|
// add random culture besed on one of the current ones
|
||||||
|
culture = rand(pack.cultures.length - 1);
|
||||||
|
name = Names.getCulture(culture, 5, 8, "");
|
||||||
|
base = pack.cultures[culture].base;
|
||||||
|
}
|
||||||
|
const i = pack.cultures.length;
|
||||||
|
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
|
||||||
|
const land = pack.cells.i.filter(isLand);
|
||||||
|
const center = land[Math.floor(Math.random() * land.length - 1)];
|
||||||
|
pack.cultures.push({name, color, base, center, i, expansionism:1, type:"Generic", cells:0, area:0, rural:0, urban:0});
|
||||||
|
drawCultureCenters();
|
||||||
|
culturesEditorAddLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadCulturesData() {
|
||||||
|
const unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value;
|
||||||
|
let data = "Id,Culture,Color,Cells,Expansionism,Type,Area "+unit+",Population,Namesbase\n"; // headers
|
||||||
|
|
||||||
|
body.querySelectorAll(":scope > div").forEach(function(el) {
|
||||||
|
data += el.dataset.id + ",";
|
||||||
|
data += el.dataset.name + ",";
|
||||||
|
data += el.dataset.color + ",";
|
||||||
|
data += el.dataset.cells + ",";
|
||||||
|
data += el.dataset.expansionism + ",";
|
||||||
|
data += el.dataset.type + ",";
|
||||||
|
data += el.dataset.area + ",";
|
||||||
|
data += el.dataset.population + ",";
|
||||||
|
const base = +el.dataset.base;
|
||||||
|
data += nameBases[base].name + "\n";
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataBlob = new Blob([data], {type: "text/plain"});
|
||||||
|
const url = window.URL.createObjectURL(dataBlob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.download = "cultures_data" + Date.now() + ".csv";
|
||||||
|
link.href = url;
|
||||||
|
link.click();
|
||||||
|
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCulturesEditor() {
|
||||||
|
debug.select("#cultureCenters").remove();
|
||||||
|
exitCulturesManualAssignment();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
189
modules/ui/editors.js
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
// module stub to store common functions for ui editors
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
restoreDefaultEvents(); // apply default viewbox events on load
|
||||||
|
|
||||||
|
// restore default viewbox events
|
||||||
|
function restoreDefaultEvents() {
|
||||||
|
svg.call(zoom);
|
||||||
|
viewbox.style("cursor", "default")
|
||||||
|
.on(".drag", null)
|
||||||
|
.on("click", clicked)
|
||||||
|
.on("touchmove mousemove", moved);
|
||||||
|
}
|
||||||
|
|
||||||
|
// on viewbox click event - run function based on target
|
||||||
|
function clicked() {
|
||||||
|
const el = d3.event.target;
|
||||||
|
if (!el || !el.parentElement || !el.parentElement.parentElement) return;
|
||||||
|
const parent = el.parentElement, grand = parent.parentElement;
|
||||||
|
if (parent.id === "rivers") editRiver(); else
|
||||||
|
if (grand.id === "routes") editRoute(); else
|
||||||
|
if (el.tagName === "textPath" && grand.parentNode.id === "labels") editLabel(); else
|
||||||
|
if (grand.id === "burgLabels") editBurg(); else
|
||||||
|
if (grand.id === "burgIcons") editBurg(); else
|
||||||
|
if (parent.id === "terrain") editReliefIcon(); else
|
||||||
|
if (parent.id === "markers") editMarker();
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear elSelected variable
|
||||||
|
function unselect() {
|
||||||
|
restoreDefaultEvents();
|
||||||
|
if (!elSelected) return;
|
||||||
|
elSelected.call(d3.drag().on("drag", null)).attr("class", null);
|
||||||
|
debug.selectAll("*").remove();
|
||||||
|
viewbox.style("cursor", "default");
|
||||||
|
elSelected = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// close all dialogs except stated
|
||||||
|
function closeDialogs(except = "#except") {
|
||||||
|
$(".dialog:visible").not(except).each(function() {
|
||||||
|
$(this).dialog("close");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// move brush radius circle
|
||||||
|
function moveCircle(x, y, r = 20) {
|
||||||
|
let circle = document.getElementById("brushCircle");
|
||||||
|
if (!circle) {
|
||||||
|
const html = `<circle id="brushCircle" cx=${x} cy=${y} r=${r}></circle>`;
|
||||||
|
document.getElementById("debug").insertAdjacentHTML("afterBegin", html);
|
||||||
|
} else {
|
||||||
|
circle.setAttribute("cx", x);
|
||||||
|
circle.setAttribute("cy", y);
|
||||||
|
circle.setAttribute("r", r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCircle() {
|
||||||
|
if (document.getElementById("brushCircle")) document.getElementById("brushCircle").remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// get browser-defined fit-content
|
||||||
|
function fitContent() {
|
||||||
|
return !window.chrome ? "-moz-max-content" : "fit-content";
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOM elements sorting on header click
|
||||||
|
$(".sortable").on("click", function() {
|
||||||
|
const el = $(this);
|
||||||
|
// remove sorting for all siblings except of clicked element
|
||||||
|
el.siblings().removeClass("icon-sort-name-up icon-sort-name-down icon-sort-number-up icon-sort-number-down");
|
||||||
|
const type = el.hasClass("alphabetically") ? "name" : "number";
|
||||||
|
let state = "no";
|
||||||
|
if (el.is("[class*='down']")) state = "asc";
|
||||||
|
if (el.is("[class*='up']")) state = "desc";
|
||||||
|
const sortby = el.attr("data-sortby");
|
||||||
|
const list = el.parent().next(); // get list container element (e.g. "countriesBody")
|
||||||
|
const lines = list.children("div"); // get list elements
|
||||||
|
if (state === "no" || state === "asc") { // sort desc
|
||||||
|
el.removeClass("icon-sort-" + type + "-down");
|
||||||
|
el.addClass("icon-sort-" + type + "-up");
|
||||||
|
lines.sort(function(a, b) {
|
||||||
|
let an = a.getAttribute("data-" + sortby);
|
||||||
|
if (an === "bottom") {return 1;}
|
||||||
|
let bn = b.getAttribute("data-" + sortby);
|
||||||
|
if (bn === "bottom") {return -1;}
|
||||||
|
if (type === "number") {an = +an; bn = +bn;}
|
||||||
|
if (an > bn) {return 1;}
|
||||||
|
if (an < bn) {return -1;}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (state === "desc") { // sort asc
|
||||||
|
el.removeClass("icon-sort-" + type + "-up");
|
||||||
|
el.addClass("icon-sort-" + type + "-down");
|
||||||
|
lines.sort(function(a, b) {
|
||||||
|
let an = a.getAttribute("data-" + sortby);
|
||||||
|
if (an === "bottom") {return 1;}
|
||||||
|
let bn = b.getAttribute("data-" + sortby);
|
||||||
|
if (bn === "bottom") {return -1;}
|
||||||
|
if (type === "number") {an = +an; bn = +bn;}
|
||||||
|
if (an < bn) {return 1;}
|
||||||
|
if (an > bn) {return -1;}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
lines.detach().appendTo(list);
|
||||||
|
});
|
||||||
|
|
||||||
|
function applySorting(headers) {
|
||||||
|
const header = headers.querySelector("[class*='icon-sort']");
|
||||||
|
if (!header) return;
|
||||||
|
const sortby = header.dataset.sortby;
|
||||||
|
const type = header.classList.contains("alphabetically") ? "name" : "number";
|
||||||
|
const desc = headers.querySelector("[class*='-down']") ? -1 : 1;
|
||||||
|
const list = headers.nextElementSibling;
|
||||||
|
const lines = Array.from(list.children);
|
||||||
|
|
||||||
|
lines.sort(function(a, b) {
|
||||||
|
let an = a.getAttribute("data-" + sortby);
|
||||||
|
let bn = b.getAttribute("data-" + sortby);
|
||||||
|
if (type === "number") {an = +an; bn = +bn;}
|
||||||
|
return (an - bn) * desc;
|
||||||
|
}).forEach(line => list.appendChild(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
// trigger trash button click on "Delete" keypress
|
||||||
|
function removeElementOnKey() {
|
||||||
|
$(".dialog:visible .icon-trash").click();
|
||||||
|
$("button:visible:contains('Remove')").click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addBurg(point) {
|
||||||
|
const cells = pack.cells;
|
||||||
|
const x = rn(point[0], 2), y = rn(point[1], 2);
|
||||||
|
const cell = findCell(x, point[1]);
|
||||||
|
const i = pack.burgs.length;
|
||||||
|
const culture = cells.culture[cell];
|
||||||
|
const name = Names.getCulture(culture);
|
||||||
|
const state = cells.state[cell];
|
||||||
|
const feature = cells.f[cell];
|
||||||
|
|
||||||
|
const population = Math.max((cells.s[cell] + cells.road[cell]) / 3 + i / 1000 + cell % 100 / 1000, .1);
|
||||||
|
pack.burgs.push({name, cell, x, y, state, i, culture, feature, capital: false, port: 0, population});
|
||||||
|
|
||||||
|
const townSize = burgIcons.select("#towns").attr("size") || 0.5;
|
||||||
|
burgIcons.select("#towns").append("circle").attr("id", "burg"+i).attr("data-id", i)
|
||||||
|
.attr("cx", x).attr("cy", y).attr("r", townSize);
|
||||||
|
burgLabels.select("#towns").append("text").attr("id", "burgLabel"+i).attr("data-id", i)
|
||||||
|
.attr("x", x).attr("y", y).attr("dy", `${townSize * -1.5}px`).text(name);
|
||||||
|
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveBurgToGroup(id, g) {
|
||||||
|
const label = document.querySelector("#burgLabels [data-id='" + id + "']");
|
||||||
|
const icon = document.querySelector("#burgIcons [data-id='" + id + "']");
|
||||||
|
const anchor = document.querySelector("#anchors [data-id='" + id + "']");
|
||||||
|
if (!label || !icon) {console.error("Cannot find label or icon elements"); return;}
|
||||||
|
|
||||||
|
document.querySelector("#burgLabels > #"+g).appendChild(label);
|
||||||
|
document.querySelector("#burgIcons > #"+g).appendChild(icon);
|
||||||
|
|
||||||
|
const iconSize = icon.parentNode.getAttribute("size");
|
||||||
|
icon.setAttribute("r", iconSize);
|
||||||
|
label.setAttribute("dy", `${iconSize * -1.5}px`);
|
||||||
|
|
||||||
|
if (anchor) {
|
||||||
|
document.querySelector("#anchors > #"+g).appendChild(anchor);
|
||||||
|
const anchorSize = +anchor.parentNode.getAttribute("size");
|
||||||
|
anchor.setAttribute("width", anchorSize);
|
||||||
|
anchor.setAttribute("height", anchorSize);
|
||||||
|
anchor.setAttribute("x", rn(pack.burgs[id].x - anchorSize * 0.47, 2));
|
||||||
|
anchor.setAttribute("y", rn(pack.burgs[id].y - anchorSize * 0.47, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeBurg(id) {
|
||||||
|
const label = document.querySelector("#burgLabels [data-id='" + id + "']");
|
||||||
|
const icon = document.querySelector("#burgIcons [data-id='" + id + "']");
|
||||||
|
const anchor = document.querySelector("#anchors [data-id='" + id + "']");
|
||||||
|
if (label) label.remove();
|
||||||
|
if (icon) icon.remove();
|
||||||
|
if (anchor) anchor.remove();
|
||||||
|
pack.burgs[id].removed = true;
|
||||||
|
const cell = pack.burgs[id].cell;
|
||||||
|
pack.cells.burg[cell] = 0;
|
||||||
|
}
|
||||||
267
modules/ui/general.js
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
// Module to store general UI functions
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// ask before closing the window
|
||||||
|
window.onbeforeunload = () => "Are you sure you want to navigate away?";
|
||||||
|
|
||||||
|
// fit full-screen map if window is resized
|
||||||
|
$(window).resize(function(e) {
|
||||||
|
// trick to prevent resize on download bar opening
|
||||||
|
if (autoResize === false) return;
|
||||||
|
mapWidthInput.value = window.innerWidth;
|
||||||
|
mapHeightInput.value = window.innerHeight;
|
||||||
|
changeMapSize();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tooltips
|
||||||
|
const tooltip = document.getElementById("tooltip");
|
||||||
|
|
||||||
|
// show tip for non-svg elemets with data-tip
|
||||||
|
document.getElementById("dialogs").addEventListener("mousemove", showDataTip);
|
||||||
|
document.getElementById("optionsContainer").addEventListener("mousemove", showDataTip);
|
||||||
|
|
||||||
|
function tip(tip = "Tip is undefined", main = false, error = false) {
|
||||||
|
const reg = "linear-gradient(0.1turn, #ffffff00, #5e5c5c66, #ffffff00)";
|
||||||
|
const red = "linear-gradient(0.1turn, #ffffff00, #e3141499, #ffffff00)";
|
||||||
|
tooltip.innerHTML = tip;
|
||||||
|
tooltip.style.background = error ? red : reg;
|
||||||
|
if (main) tooltip.dataset.main = tip;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMainTip() {
|
||||||
|
tooltip.style.background = "linear-gradient(0.1turn, #aaffff00, #3a26264d, #ffffff00)";
|
||||||
|
tooltip.innerHTML = tooltip.dataset.main;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearMainTip() {
|
||||||
|
tooltip.dataset.main = "";
|
||||||
|
tooltip.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDataTip(e) {
|
||||||
|
if (!e.target) return;
|
||||||
|
if (e.target.dataset.tip) {tip(e.target.dataset.tip); return;};
|
||||||
|
if (e.target.parentNode.dataset.tip) tip(e.target.parentNode.dataset.tip);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moved() {
|
||||||
|
const point = d3.mouse(this);
|
||||||
|
const i = findCell(point[0], point[1]); // pack ell id
|
||||||
|
if (i === undefined) return;
|
||||||
|
showLegend(d3.event, i);
|
||||||
|
const g = findGridCell(point[0], point[1]); // grid cell id
|
||||||
|
if (tooltip.dataset.main) showMainTip(); else showMapTooltip(d3.event, i, g);
|
||||||
|
if (toolsContent.style.display === "block" && cellInfo.style.display === "block") updateCellInfo(point, i, g);
|
||||||
|
}
|
||||||
|
|
||||||
|
// show legend on hover (if any)
|
||||||
|
function showLegend(e, i) {
|
||||||
|
let id = e.target.id || e.target.parentNode.id;
|
||||||
|
if (e.target.parentNode.parentNode.id === "burgLabels") id = "burg" + e.target.dataset.id; else
|
||||||
|
if (e.target.parentNode.parentNode.id === "burgIcons") id = "burg" + e.target.dataset.id;
|
||||||
|
|
||||||
|
const note = notes.find(note => note.id === id);
|
||||||
|
if (note !== undefined && note.legend !== "") {
|
||||||
|
document.getElementById("legend").style.display = "block";
|
||||||
|
document.getElementById("legendHeader").innerHTML = note.name;
|
||||||
|
document.getElementById("legendBody").innerHTML = note.legend;
|
||||||
|
} else {
|
||||||
|
document.getElementById("legend").style.display = "none";
|
||||||
|
document.getElementById("legendHeader").innerHTML = "";
|
||||||
|
document.getElementById("legendBody").innerHTML = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// show viewbox tooltip if main tooltip is blank
|
||||||
|
function showMapTooltip(e, i, g) {
|
||||||
|
tip(""); // clear tip
|
||||||
|
const tag = e.target.tagName;
|
||||||
|
const path = e.composedPath ? e.composedPath() : getComposedPath(e.target); // apply polyfill
|
||||||
|
const group = path[path.length - 7].id;
|
||||||
|
const subgroup = path[path.length - 8].id;
|
||||||
|
const land = pack.cells.h[i] >= 20;
|
||||||
|
|
||||||
|
// specific elements
|
||||||
|
if (group === "rivers") {tip("Click to edit the River"); return;}
|
||||||
|
if (group === "routes") {tip("Click to edit the Route"); return;}
|
||||||
|
if (group === "terrain") {tip("Click to edit the Relief Icon"); return;}
|
||||||
|
if (subgroup === "burgLabels" || subgroup === "burgIcons") {tip("Click to open Burg Editor"); return;}
|
||||||
|
if (group === "labels") {tip("Click to edit the Label"); return;}
|
||||||
|
if (group === "markers") {tip("Click to edit the Marker"); return;}
|
||||||
|
if (group === "ruler") {
|
||||||
|
if (tag === "rect") {tip("Drag to split the ruler into 2 parts"); return;}
|
||||||
|
if (tag === "circle") {tip("Drag to adjust the measurer"); return;}
|
||||||
|
if (tag === "path" || tag === "line") {tip("Drag to move the measurer"); return;}
|
||||||
|
if (tag === "text") {tip("Click to remove the measurer"); return;}
|
||||||
|
}
|
||||||
|
if (subgroup === "burgIcons") {tip("Click to edit the Burg"); return;}
|
||||||
|
if (subgroup === "burgLabels") {tip("Click to edit the Burg"); return;}
|
||||||
|
if (subgroup === "freshwater" && !land) {tip("Freshwater lake"); return;}
|
||||||
|
if (subgroup === "salt" && !land) {tip("Salt lake"); return;}
|
||||||
|
|
||||||
|
// covering elements
|
||||||
|
if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: "+ getFriendlyPrecipitation(i)); else
|
||||||
|
if (layerIsOn("togglePopulation")) tip("Population: "+ getFriendlyPopulation(i)); else
|
||||||
|
if (layerIsOn("toggleTemp")) tip("Temperature: " + convertTemperature(grid.cells.temp[g])); else
|
||||||
|
if (layerIsOn("toggleBiomes") && pack.cells.biome[i]) tip("Biome: " + biomesData.name[pack.cells.biome[i]]); else
|
||||||
|
if (layerIsOn("toggleStates") && pack.cells.state[i]) tip("State: " + pack.states[pack.cells.state[i]].name); else
|
||||||
|
if (layerIsOn("toggleCultures") && pack.cells.culture[i]) tip("Culture: " + pack.cultures[pack.cells.culture[i]].name); else
|
||||||
|
if (layerIsOn("toggleHeight")) tip("Height: " + getFriendlyHeight(pack.cells.h[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// get cell info on mouse move
|
||||||
|
function updateCellInfo(point, i, g) {
|
||||||
|
const cells = pack.cells;
|
||||||
|
infoX.innerHTML = rn(point[0]);
|
||||||
|
infoY.innerHTML = rn(point[1]);
|
||||||
|
infoCell.innerHTML = i;
|
||||||
|
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
|
||||||
|
infoArea.innerHTML = cells.area[i] ? si(cells.area[i] * distanceScale.value ** 2) + unit : "n/a";
|
||||||
|
infoHeight.innerHTML = getFriendlyHeight(cells.h[i]) + " (" + cells.h[i] + ")";
|
||||||
|
infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
|
||||||
|
infoPrec.innerHTML = pack.cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a";
|
||||||
|
infoState.innerHTML = ifDefined(cells.state[i]) !== "no" ? pack.states[cells.state[i]].name + " (" + cells.state[i] + ")" : "n/a";
|
||||||
|
infoCulture.innerHTML = ifDefined(cells.culture[i]) !== "no" ? pack.cultures[cells.culture[i]].name + " (" + cells.culture[i] + ")" : "n/a";
|
||||||
|
infoPopulation.innerHTML = getFriendlyPopulation(i);
|
||||||
|
infoBurg.innerHTML = cells.burg[i] ? pack.burgs[cells.burg[i]].name + " (" + cells.burg[i] + ")" : "no";
|
||||||
|
const f = cells.f[i];
|
||||||
|
infoFeature.innerHTML = f ? pack.features[f].group + " (" + f + ")" : "n/a";
|
||||||
|
infoBiome.innerHTML = biomesData.name[cells.biome[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// return value (v) if defined with number of decimals (d), else return "no" or attribute (r)
|
||||||
|
function ifDefined(v, r = "no", d) {
|
||||||
|
if (v === null || v === undefined) return r;
|
||||||
|
if (d) return v.toFixed(d);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get user-friendly (real-world) height value from map data
|
||||||
|
function getFriendlyHeight(h) {
|
||||||
|
const unit = heightUnit.value;
|
||||||
|
let unitRatio = 3.281; // default calculations are in feet
|
||||||
|
if (unit === "m") unitRatio = 1; // if meter
|
||||||
|
else if (unit === "f") unitRatio = 0.5468; // if fathom
|
||||||
|
|
||||||
|
let height = -990;
|
||||||
|
if (h >= 20) height = Math.pow(h - 18, +heightExponent.value);
|
||||||
|
else if (h < 20 && h > 0) height = (h - 20) / h * 50;
|
||||||
|
|
||||||
|
return rn(height * unitRatio) + " " + unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get user-friendly (real-world) precipitation value from map data
|
||||||
|
function getFriendlyPrecipitation(i) {
|
||||||
|
const prec = grid.cells.prec[pack.cells.g[i]];
|
||||||
|
return prec * 100 + " mm";
|
||||||
|
}
|
||||||
|
|
||||||
|
// get user-friendly (real-world) population value from map data
|
||||||
|
function getFriendlyPopulation(i) {
|
||||||
|
const rural = pack.cells.pop[i] * populationRate.value;
|
||||||
|
const urban = pack.cells.burg[i] ? pack.burgs[pack.cells.burg[i]].population * populationRate.value * urbanization.value : 0;
|
||||||
|
return si(rural+urban);
|
||||||
|
}
|
||||||
|
|
||||||
|
// assign lock behavior
|
||||||
|
document.querySelectorAll("[data-locked]").forEach(function(e) {
|
||||||
|
e.addEventListener("mouseover", function(event) {
|
||||||
|
if (this.className === "icon-lock") tip("Click to unlock the option and allow it to be randomized on new map generation");
|
||||||
|
else tip("Click to lock the option and always use the current value on new map generation");
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
e.addEventListener("click", function(event) {
|
||||||
|
const id = (this.id).slice(5);
|
||||||
|
if (this.className === "icon-lock") unlock(id);
|
||||||
|
else lock(id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// lock option
|
||||||
|
function lock(id) {
|
||||||
|
const input = document.querySelector("[data-stored='"+id+"']");
|
||||||
|
if (input) localStorage.setItem(id, input.value);
|
||||||
|
const el = document.getElementById("lock_" + id);
|
||||||
|
if(!el) return;
|
||||||
|
el.dataset.locked = 1;
|
||||||
|
el.className = "icon-lock";
|
||||||
|
}
|
||||||
|
|
||||||
|
// unlock option
|
||||||
|
function unlock(id) {
|
||||||
|
localStorage.removeItem(id);
|
||||||
|
const el = document.getElementById("lock_" + id);
|
||||||
|
if(!el) return;
|
||||||
|
el.dataset.locked = 0;
|
||||||
|
el.className = "icon-lock-open";
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if option is locked
|
||||||
|
function locked(id) {
|
||||||
|
const lockEl = document.getElementById("lock_" + id);
|
||||||
|
return lockEl.dataset.locked == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys
|
||||||
|
document.addEventListener("keydown", function(event) {
|
||||||
|
const active = document.activeElement.tagName;
|
||||||
|
if (active === "INPUT" || active === "SELECT" || active === "TEXTAREA") return; // don't trigger if user inputs a text
|
||||||
|
const key = event.keyCode, ctrl = event.ctrlKey, shift = event.shiftKey;
|
||||||
|
if (key === 118) regenerateMap(); // "F7" for new map
|
||||||
|
else if (key === 27) {closeDialogs(); hideOptions();} // Escape to close all dialogs
|
||||||
|
else if (key === 9) {toggleOptions(event); event.preventDefault();} // Tab to toggle options
|
||||||
|
else if (ctrl && key === 80) saveAsImage("png"); // Ctrl + "P" to save as PNG
|
||||||
|
else if (ctrl && key === 83) saveAsImage("svg"); // Ctrl + "S" to save as SVG
|
||||||
|
else if (ctrl && key === 77) saveMap(); // Ctrl + "M" to save MAP file
|
||||||
|
else if (ctrl && key === 76) mapToLoad.click(); // Ctrl + "L" to load MAP
|
||||||
|
else if (key === 46) removeElementOnKey(); // "Delete" to remove the selected element
|
||||||
|
|
||||||
|
else if (shift && key === 192) console.log(pack.cells); // Shift + "`" to log cells data
|
||||||
|
else if (shift && key === 66) console.table(pack.burgs); // Shift + "B" to log burgs data
|
||||||
|
else if (shift && key === 83) console.table(pack.states); // Shift + "S" to log states data
|
||||||
|
else if (shift && key === 67) console.table(pack.cultures); // Shift + "C" to log cultures data
|
||||||
|
else if (shift && key === 70) console.table(pack.features); // Shift + "F" to log features data
|
||||||
|
|
||||||
|
else if (key === 88) toggleTexture(); // "X" to toggle Texture layer
|
||||||
|
else if (key === 72) toggleHeight(); // "H" to toggle Heightmap layer
|
||||||
|
else if (key === 66) toggleBiomes(); // "B" to toggle Biomes layer
|
||||||
|
else if (key === 69) toggleCells(); // "E" to toggle Cells layer
|
||||||
|
else if (key === 71) toggleGrid(); // "G" to toggle Grid layer
|
||||||
|
else if (key === 79) toggleCoordinates(); // "O" to toggle Coordinates layer
|
||||||
|
else if (key === 87) toggleCompass(); // "W" to toggle Compass Rose layer
|
||||||
|
else if (key === 86) toggleRivers(); // "V" to toggle Rivers layer
|
||||||
|
else if (key === 82) toggleRelief(); // "R" to toggle Relief icons layer
|
||||||
|
else if (key === 67) toggleCultures(); // "C" to toggle Cultures layer
|
||||||
|
else if (key === 83) toggleStates(); // "S" to toggle States layer
|
||||||
|
else if (key === 68) toggleBorders(); // "D" to toggle Borders layer
|
||||||
|
else if (key === 85) toggleRoutes(); // "U" to toggle Routes layer
|
||||||
|
else if (key === 84) toggleTemp(); // "T" to toggle Temperature layer
|
||||||
|
else if (key === 80) togglePopulation(); // "P" to toggle Population layer
|
||||||
|
else if (key === 65) togglePrec(); // "A" to toggle Precipitation layer
|
||||||
|
else if (key === 76) toggleLabels(); // "L" to toggle Labels layer
|
||||||
|
else if (key === 73) toggleIcons(); // "I" to toggle Icons layer
|
||||||
|
else if (key === 77) toggleMarkers(); // "M" to toggle Markers layer
|
||||||
|
else if (key === 187) toggleRulers(); // Equal (=) to toggle Rulers
|
||||||
|
else if (key === 189) toggleScaleBar(); // Minus (-) to toggle Scale bar
|
||||||
|
|
||||||
|
else if (key === 37) zoom.translateBy(svg, 10, 0); // Left to scroll map left
|
||||||
|
else if (key === 39) zoom.translateBy(svg, -10, 0); // Right to scroll map right
|
||||||
|
else if (key === 38) zoom.translateBy(svg, 0, 10); // Up to scroll map up
|
||||||
|
else if (key === 40) zoom.translateBy(svg, 0, -10); // Up to scroll map up
|
||||||
|
else if (key === 107) zoom.scaleBy(svg, 1.2); // Numpad Plus to zoom map up
|
||||||
|
else if (key === 109) zoom.scaleBy(svg, 0.8); // Numpad Minus to zoom map out
|
||||||
|
else if (key === 48 || key === 96) resetZoom(1000); // 0 to reset zoom
|
||||||
|
else if (key === 49 || key === 97) zoom.scaleTo(svg, 1); // 1 to zoom to 1
|
||||||
|
else if (key === 50 || key === 98) zoom.scaleTo(svg, 2); // 2 to zoom to 2
|
||||||
|
else if (key === 51 || key === 99) zoom.scaleTo(svg, 3); // 3 to zoom to 3
|
||||||
|
else if (key === 52 || key === 100) zoom.scaleTo(svg, 4); // 4 to zoom to 4
|
||||||
|
else if (key === 53 || key === 101) zoom.scaleTo(svg, 5); // 5 to zoom to 5
|
||||||
|
else if (key === 54 || key === 102) zoom.scaleTo(svg, 6); // 6 to zoom to 6
|
||||||
|
else if (key === 55 || key === 103) zoom.scaleTo(svg, 7); // 7 to zoom to 7
|
||||||
|
else if (key === 56 || key === 104) zoom.scaleTo(svg, 8); // 8 to zoom to 8
|
||||||
|
else if (key === 57 || key === 105) zoom.scaleTo(svg, 9); // 9 to zoom to 9
|
||||||
|
|
||||||
|
else if (ctrl && key === 90) undo.click(); // Ctrl + "Z" to undo
|
||||||
|
else if (ctrl && key === 89) redo.click(); // Ctrl + "Y" to redo
|
||||||
|
});
|
||||||
1098
modules/ui/heightmap-editor.js
Normal file
312
modules/ui/labels-editor.js
Normal file
|
|
@ -0,0 +1,312 @@
|
||||||
|
"use strict";
|
||||||
|
function editLabel() {
|
||||||
|
if (customization) return;
|
||||||
|
closeDialogs(".stable");
|
||||||
|
if (!layerIsOn("toggleLabels")) toggleLabels();
|
||||||
|
|
||||||
|
const node = d3.event.target;
|
||||||
|
elSelected = d3.select(node.parentNode).call(d3.drag().on("start", dragLabel)).classed("draggable", true);
|
||||||
|
viewbox.on("touchmove mousemove", showEditorTips);
|
||||||
|
|
||||||
|
$("#labelEditor").dialog({
|
||||||
|
title: "Edit Label: " + node.innerHTML, resizable: false,
|
||||||
|
position: {my: "center top+10", at: "bottom", of: node, collision: "fit"},
|
||||||
|
close: closeLabelEditor
|
||||||
|
});
|
||||||
|
|
||||||
|
debug.append("g").attr("id", "controlPoints").attr("transform", elSelected.attr("transform"));
|
||||||
|
drawControlPointsAndLine();
|
||||||
|
selectLabelGroup(node);
|
||||||
|
updateValues(node);
|
||||||
|
|
||||||
|
if (modules.editLabel) return;
|
||||||
|
modules.editLabel = true;
|
||||||
|
|
||||||
|
// add listeners
|
||||||
|
document.getElementById("labelGroupShow").addEventListener("click", showGroupSection);
|
||||||
|
document.getElementById("labelGroupHide").addEventListener("click", hideGroupSection);
|
||||||
|
document.getElementById("labelGroupSelect").addEventListener("click", changeGroup);
|
||||||
|
document.getElementById("labelGroupInput").addEventListener("change", createNewGroup);
|
||||||
|
document.getElementById("labelGroupNew").addEventListener("click", toggleNewGroupInput);
|
||||||
|
document.getElementById("labelGroupRemove").addEventListener("click", removeLabelsGroup);
|
||||||
|
|
||||||
|
document.getElementById("labelTextShow").addEventListener("click", showTextSection);
|
||||||
|
document.getElementById("labelTextHide").addEventListener("click", hideTextSection);
|
||||||
|
document.getElementById("labelText").addEventListener("input", changeText);
|
||||||
|
document.getElementById("labelTextRandom").addEventListener("click", generateRandomName);
|
||||||
|
|
||||||
|
document.getElementById("labelSizeShow").addEventListener("click", showSizeSection);
|
||||||
|
document.getElementById("labelSizeHide").addEventListener("click", hideSizeSection);
|
||||||
|
document.getElementById("labelStartOffset").addEventListener("input", changeStartOffset);
|
||||||
|
document.getElementById("labelRelativeSize").addEventListener("input", changeRelativeSize);
|
||||||
|
|
||||||
|
document.getElementById("labelLegend").addEventListener("click", editLabelLegend);
|
||||||
|
document.getElementById("labelRemoveSingle").addEventListener("click", removeLabel);
|
||||||
|
|
||||||
|
function showEditorTips() {
|
||||||
|
showMainTip();
|
||||||
|
if (d3.event.target.parentNode.id === elSelected.attr("id")) tip("Drag to shift the label"); else
|
||||||
|
if (d3.event.target.parentNode.id === "controlPoints") {
|
||||||
|
if (d3.event.target.tagName === "circle") tip("Drag to move, click to delete the control point");
|
||||||
|
if (d3.event.target.tagName === "path") tip("Click to add a control point");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectLabelGroup(node) {
|
||||||
|
const group = node.parentNode.parentNode.id;
|
||||||
|
const select = document.getElementById("labelGroupSelect");
|
||||||
|
select.options.length = 0; // remove all options
|
||||||
|
|
||||||
|
labels.selectAll(":scope > g").each(function() {
|
||||||
|
if (this.id === "burgLabels") return;
|
||||||
|
select.options.add(new Option(this.id, this.id, false, this.id === group));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateValues(node) {
|
||||||
|
document.getElementById("labelText").value = node.innerHTML;
|
||||||
|
document.getElementById("labelStartOffset").value = parseFloat(node.getAttribute("startOffset"));
|
||||||
|
document.getElementById("labelRelativeSize").value = parseFloat(node.getAttribute("font-size"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawControlPointsAndLine() {
|
||||||
|
const path = document.getElementById("textPath_" + elSelected.attr("id"));
|
||||||
|
debug.select("#controlPoints").append("path").attr("d", path.getAttribute("d")).on("click", addInterimControlPoint);
|
||||||
|
const l = path.getTotalLength();
|
||||||
|
const increment = l / Math.max(Math.ceil(l / 100), 2);
|
||||||
|
for (let i=0; i <= l; i += increment) {addControlPoint(path.getPointAtLength(i));}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addControlPoint(point) {
|
||||||
|
debug.select("#controlPoints").append("circle")
|
||||||
|
.attr("cx", point.x).attr("cy", point.y).attr("r", 1)
|
||||||
|
.call(d3.drag().on("drag", dragControlPoint))
|
||||||
|
.on("click", clickControlPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragControlPoint() {
|
||||||
|
this.setAttribute("cx", d3.event.x);
|
||||||
|
this.setAttribute("cy", d3.event.y);
|
||||||
|
redrawLabelPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
function redrawLabelPath() {
|
||||||
|
const path = document.getElementById("textPath_" + elSelected.attr("id"));
|
||||||
|
lineGen.curve(d3.curveBundle.beta(1));
|
||||||
|
const points = [];
|
||||||
|
debug.select("#controlPoints").selectAll("circle").each(function() {
|
||||||
|
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
|
||||||
|
});
|
||||||
|
const d = round(lineGen(points));
|
||||||
|
path.setAttribute("d", d);
|
||||||
|
debug.select("#controlPoints > path").attr("d", d);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickControlPoint() {
|
||||||
|
this.remove();
|
||||||
|
redrawLabelPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addInterimControlPoint() {
|
||||||
|
const point = d3.mouse(this);
|
||||||
|
|
||||||
|
const dists = [];
|
||||||
|
debug.select("#controlPoints").selectAll("circle").each(function() {
|
||||||
|
const x = +this.getAttribute("cx");
|
||||||
|
const y = +this.getAttribute("cy");
|
||||||
|
dists.push((point[0] - x) ** 2 + (point[1] - y) ** 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
let index = dists.length;
|
||||||
|
if (dists.length > 1) {
|
||||||
|
const sorted = dists.slice(0).sort((a, b) => a-b);
|
||||||
|
const closest = dists.indexOf(sorted[0]);
|
||||||
|
const next = dists.indexOf(sorted[1]);
|
||||||
|
if (closest <= next) index = closest+1; else index = next+1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const before = ":nth-child(" + (index + 2) + ")";
|
||||||
|
debug.select("#controlPoints").insert("circle", before)
|
||||||
|
.attr("cx", point[0]).attr("cy", point[1]).attr("r", 1)
|
||||||
|
.call(d3.drag().on("drag", dragControlPoint))
|
||||||
|
.on("click", clickControlPoint);
|
||||||
|
|
||||||
|
redrawLabelPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragLabel() {
|
||||||
|
const tr = parseTransform(elSelected.attr("transform"));
|
||||||
|
const dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y;
|
||||||
|
|
||||||
|
d3.event.on("drag", function() {
|
||||||
|
const x = d3.event.x, y = d3.event.y;
|
||||||
|
const transform = `translate(${(dx+x)},${(dy+y)})`;
|
||||||
|
elSelected.attr("transform", transform);
|
||||||
|
debug.select("#controlPoints").attr("transform", transform);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showGroupSection() {
|
||||||
|
document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "none");
|
||||||
|
document.getElementById("labelGroupSection").style.display = "inline-block";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideGroupSection() {
|
||||||
|
document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "inline-block");
|
||||||
|
document.getElementById("labelGroupSection").style.display = "none";
|
||||||
|
document.getElementById("labelGroupInput").style.display = "none";
|
||||||
|
document.getElementById("labelGroupInput").value = "";
|
||||||
|
document.getElementById("labelGroupSelect").style.display = "inline-block";
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeGroup() {
|
||||||
|
document.getElementById(this.value).appendChild(elSelected.node());
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleNewGroupInput() {
|
||||||
|
if (labelGroupInput.style.display === "none") {
|
||||||
|
labelGroupInput.style.display = "inline-block";
|
||||||
|
labelGroupInput.focus();
|
||||||
|
labelGroupSelect.style.display = "none";
|
||||||
|
} else {
|
||||||
|
labelGroupInput.style.display = "none";
|
||||||
|
labelGroupSelect.style.display = "inline-block";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNewGroup() {
|
||||||
|
if (!this.value) {tip("Please provide a valid group name"); return;}
|
||||||
|
let group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
|
||||||
|
if (Number.isFinite(+group.charAt(0))) group = "g" + group;
|
||||||
|
|
||||||
|
if (document.getElementById(group)) {
|
||||||
|
tip("Element with this id already exists. Please provide a unique name", false, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// just rename if only 1 element left
|
||||||
|
const oldGroup = elSelected.node().parentNode;
|
||||||
|
if (oldGroup !== "states" && oldGroup !== "addedLabels" && oldGroup.childElementCount === 1) {
|
||||||
|
document.getElementById("labelGroupSelect").selectedOptions[0].remove();
|
||||||
|
document.getElementById("labelGroupSelect").options.add(new Option(group, group, false, true));
|
||||||
|
oldGroup.id = group;
|
||||||
|
toggleNewGroupInput();
|
||||||
|
document.getElementById("labelGroupInput").value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newGroup = elSelected.node().parentNode.cloneNode(false);
|
||||||
|
document.getElementById("labels").appendChild(newGroup);
|
||||||
|
newGroup.id = group;
|
||||||
|
document.getElementById("labelGroupSelect").options.add(new Option(group, group, false, true));
|
||||||
|
document.getElementById(group).appendChild(elSelected.node());
|
||||||
|
|
||||||
|
toggleNewGroupInput();
|
||||||
|
document.getElementById("labelGroupInput").value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLabelsGroup() {
|
||||||
|
const group = elSelected.node().parentNode.id;
|
||||||
|
const basic = group === "states" || group === "addedLabels";
|
||||||
|
const count = elSelected.node().parentNode.childElementCount;
|
||||||
|
alertMessage.innerHTML = `Are you sure you want to remove
|
||||||
|
${basic ? "all elements in the group" : "the entire label group"}?
|
||||||
|
<br><br>Labels to be removed: ${count}`;
|
||||||
|
$("#alert").dialog({resizable: false, title: "Remove route group",
|
||||||
|
buttons: {
|
||||||
|
Remove: function() {
|
||||||
|
$(this).dialog("close");
|
||||||
|
$("#labelEditor").dialog("close");
|
||||||
|
hideGroupSection();
|
||||||
|
labels.select("#"+group).selectAll("text").each(function() {
|
||||||
|
document.getElementById("textPath_" + this.id).remove();
|
||||||
|
this.remove();
|
||||||
|
});
|
||||||
|
if (!basic) labels.select("#"+group).remove();
|
||||||
|
},
|
||||||
|
Cancel: function() {$(this).dialog("close");}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTextSection() {
|
||||||
|
document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "none");
|
||||||
|
document.getElementById("labelTextSection").style.display = "inline-block";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideTextSection() {
|
||||||
|
document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "inline-block");
|
||||||
|
document.getElementById("labelTextSection").style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeText() {
|
||||||
|
const text = document.getElementById("labelText").value;
|
||||||
|
elSelected.select("textPath").text(text);
|
||||||
|
if (elSelected.attr("id").slice(0,10) === "stateLabel") {
|
||||||
|
const id = +elSelected.attr("id").slice(10);
|
||||||
|
pack.states[id].name = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateRandomName() {
|
||||||
|
let name = "";
|
||||||
|
if (elSelected.attr("id").slice(0,10) === "stateLabel") {
|
||||||
|
const id = +elSelected.attr("id").slice(10);
|
||||||
|
const culture = pack.states[id].culture;
|
||||||
|
name = Names.getState(Names.getCulture(culture, 4, 7, ""), culture);
|
||||||
|
} else {
|
||||||
|
const box = elSelected.node().getBBox();
|
||||||
|
const cell = findCell((box.x + box.width) / 2, (box.y + box.height) / 2);
|
||||||
|
const culture = pack.cells.culture[cell];
|
||||||
|
name = Names.getCulture(culture);
|
||||||
|
}
|
||||||
|
document.getElementById("labelText").value = name;
|
||||||
|
changeText();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSizeSection() {
|
||||||
|
document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "none");
|
||||||
|
document.getElementById("labelSizeSection").style.display = "inline-block";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideSizeSection() {
|
||||||
|
document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "inline-block");
|
||||||
|
document.getElementById("labelSizeSection").style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeStartOffset() {
|
||||||
|
elSelected.select("textPath").attr("startOffset", this.value + "%");
|
||||||
|
tip("Label offset: " + this.value + "%");
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeRelativeSize() {
|
||||||
|
elSelected.select("textPath").attr("font-size", this.value + "%");
|
||||||
|
tip("Label relative size: " + this.value + "%");
|
||||||
|
}
|
||||||
|
|
||||||
|
function editLabelLegend() {
|
||||||
|
const id = elSelected.attr("id");
|
||||||
|
const name = elSelected.text();
|
||||||
|
editLegends(id, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLabel() {
|
||||||
|
alertMessage.innerHTML = "Are you sure you want to remove the label?";
|
||||||
|
$("#alert").dialog({resizable: false, title: "Remove label",
|
||||||
|
buttons: {
|
||||||
|
Remove: function() {
|
||||||
|
$(this).dialog("close");
|
||||||
|
defs.select("#textPath_" + elSelected.attr("id")).remove();
|
||||||
|
elSelected.remove();
|
||||||
|
$("#labelEditor").dialog("close");
|
||||||
|
},
|
||||||
|
Cancel: function() {$(this).dialog("close");}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLabelEditor() {
|
||||||
|
debug.select("#controlPoints").remove();
|
||||||
|
unselect();
|
||||||
|
}
|
||||||
|
}
|
||||||
813
modules/ui/layers.js
Normal file
|
|
@ -0,0 +1,813 @@
|
||||||
|
// UI module stub to control map layers
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// on map regeneration restore layers if they was turned on
|
||||||
|
function restoreLayers() {
|
||||||
|
if (layerIsOn("toggleHeight")) drawHeightmap();
|
||||||
|
if (layerIsOn("toggleCells")) drawCells();
|
||||||
|
if (layerIsOn("toggleGrid")) drawGrid();
|
||||||
|
if (layerIsOn("toggleCoordinates")) drawCoordinates();
|
||||||
|
if (layerIsOn("toggleCompass")) compass.attr("display", "block");
|
||||||
|
if (layerIsOn("toggleTemp")) drawTemp();
|
||||||
|
if (layerIsOn("togglePrec")) drawPrec();
|
||||||
|
if (layerIsOn("togglePopulation")) drawPopulation();
|
||||||
|
if (layerIsOn("toggleBiomes")) drawBiomes();
|
||||||
|
if (layerIsOn("toggleRelief")) ReliefIcons();
|
||||||
|
if (layerIsOn("toggleStates") || layerIsOn("toggleBorders")) drawStatesWithBorders();
|
||||||
|
if (layerIsOn("toggleCultures")) drawCultures();
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreLayers(); // run on-load
|
||||||
|
|
||||||
|
// toggle layers on preset change
|
||||||
|
function changePreset(preset) {
|
||||||
|
const layers = getLayers(preset); // layers to be turned on
|
||||||
|
const ignore = ["toggleTexture", "toggleScaleBar"]; // never toggle this layers
|
||||||
|
|
||||||
|
document.getElementById("mapLayers").querySelectorAll("li").forEach(function(e) {
|
||||||
|
if (ignore.includes(e.id)) return; // ignore
|
||||||
|
if (layers.includes(e.id) && !layerIsOn(e.id)) e.click(); // turn on
|
||||||
|
else if (!layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off
|
||||||
|
});
|
||||||
|
layersPreset.value = preset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// retrun list of layers to be turned on
|
||||||
|
function getLayers(preset) {
|
||||||
|
switch(preset) {
|
||||||
|
case "political": return ["toggleStates", "toggleRivers", "toggleBorders", "toggleRoutes", "toggleLabels", "toggleIcons"];
|
||||||
|
case "cultural": return ["toggleCultures", "toggleRivers", "toggleBorders", "toggleRoutes", "toggleLabels", "toggleIcons"];
|
||||||
|
case "heightmap": return ["toggleHeight", "toggleRivers"];
|
||||||
|
case "biomes": return ["toggleBiomes", "toggleRivers"];
|
||||||
|
case "landmass": return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleHeight() {
|
||||||
|
if (!terrs.selectAll("*").size()) {
|
||||||
|
turnButtonOn("toggleHeight");
|
||||||
|
drawHeightmap();
|
||||||
|
} else {
|
||||||
|
if (customization === 1) {tip("You cannot turn off the layer when heightmap is in edit mode", false, "error"); return;}
|
||||||
|
turnButtonOff("toggleHeight");
|
||||||
|
terrs.selectAll("*").remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawHeightmap() {
|
||||||
|
console.time("drawHeightmap");
|
||||||
|
terrs.selectAll("*").remove();
|
||||||
|
const cells = pack.cells, vertices = pack.vertices, n = cells.i.length;
|
||||||
|
const used = new Uint8Array(cells.i.length);
|
||||||
|
const paths = new Array(101).fill("");
|
||||||
|
|
||||||
|
const scheme = getColorScheme();
|
||||||
|
const terracing = +styleHeightmapTerracingInput.value / 10; // add additional shifted darker layer for pseudo-3d effect
|
||||||
|
const skip = +styleHeightmapSkipOutput.value + 1;
|
||||||
|
const simplification = +styleHeightmapSimplificationInput.value;
|
||||||
|
switch (+styleHeightmapCurveInput.value) {
|
||||||
|
case 0: lineGen.curve(d3.curveBasisClosed); break;
|
||||||
|
case 1: lineGen.curve(d3.curveLinear); break;
|
||||||
|
case 2: lineGen.curve(d3.curveStep); break;
|
||||||
|
default: lineGen.curve(d3.curveBasisClosed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentLayer = 20;
|
||||||
|
const heights = cells.i.sort((a, b) => cells.h[a] - cells.h[b]);
|
||||||
|
for (const i of heights) {
|
||||||
|
const h = cells.h[i];
|
||||||
|
if (h > currentLayer) currentLayer += skip;
|
||||||
|
if (currentLayer > 100) break; // no layers possible with height > 100
|
||||||
|
if (h < currentLayer) continue;
|
||||||
|
if (used[i]) continue; // already marked
|
||||||
|
const onborder = cells.c[i].some(n => cells.h[n] < h);
|
||||||
|
if (!onborder) continue;
|
||||||
|
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] < h));
|
||||||
|
const chain = connectVertices(vertex, h);
|
||||||
|
if (chain.length < 3) continue;
|
||||||
|
const points = simplifyLine(chain).map(v => vertices.p[v]);
|
||||||
|
paths[h] += round(lineGen(points));
|
||||||
|
}
|
||||||
|
|
||||||
|
terrs.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("fill", scheme(.8)); // draw base layer
|
||||||
|
for (const i of d3.range(20, 101)) {
|
||||||
|
if (paths[i].length < 10) continue;
|
||||||
|
const color = getColor(i, scheme);
|
||||||
|
if (terracing) terrs.append("path").attr("d", paths[i]).attr("transform", "translate(.7,1.4)").attr("fill", d3.color(color).darker(terracing)).attr("data-height", i);
|
||||||
|
terrs.append("path").attr("d", paths[i]).attr("fill", color).attr("data-height", i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect vertices to chain
|
||||||
|
function connectVertices(start, h) {
|
||||||
|
const chain = []; // vertices chain to form a path
|
||||||
|
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
|
||||||
|
const prev = chain[chain.length - 1]; // previous vertex in chain
|
||||||
|
chain.push(current); // add current vertex to sequence
|
||||||
|
const c = vertices.c[current]; // cells adjacent to vertex
|
||||||
|
c.filter(c => cells.h[c] === h).forEach(c => used[c] = 1);
|
||||||
|
const c0 = c[0] >= n || cells.h[c[0]] < h;
|
||||||
|
const c1 = c[1] >= n || cells.h[c[1]] < h;
|
||||||
|
const c2 = c[2] >= n || cells.h[c[2]] < h;
|
||||||
|
const v = vertices.v[current]; // neighboring vertices
|
||||||
|
if (v[0] !== prev && c0 !== c1) current = v[0];
|
||||||
|
else if (v[1] !== prev && c1 !== c2) current = v[1];
|
||||||
|
else if (v[2] !== prev && c0 !== c2) current = v[2];
|
||||||
|
if (current === chain[chain.length - 1]) {console.error("Next vertex is not found"); break;}
|
||||||
|
}
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
|
||||||
|
function simplifyLine(chain) {
|
||||||
|
if (!simplification) return chain;
|
||||||
|
const n = simplification + 1; // filter each nth element
|
||||||
|
return chain.filter((d, i) => i % n === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.timeEnd("drawHeightmap");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColorScheme() {
|
||||||
|
const scheme = styleHeightmapSchemeInput.value;
|
||||||
|
if (scheme === "bright") return d3.scaleSequential(d3.interpolateSpectral);
|
||||||
|
if (scheme === "light") return d3.scaleSequential(d3.interpolateRdYlGn);
|
||||||
|
if (scheme === "green") return d3.scaleSequential(d3.interpolateGreens);
|
||||||
|
if (scheme === "monochrome") return d3.scaleSequential(d3.interpolateGreys);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColor(value, scheme = getColorScheme()) {
|
||||||
|
return scheme(1 - (value < 20 ? value - 5 : value) / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTemp() {
|
||||||
|
if (!temperature.selectAll("*").size()) {
|
||||||
|
turnButtonOn("toggleTemp");
|
||||||
|
drawTemp();
|
||||||
|
} else {
|
||||||
|
turnButtonOff("toggleTemp");
|
||||||
|
temperature.selectAll("*").remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawTemp() {
|
||||||
|
console.time("drawTemp");
|
||||||
|
temperature.selectAll("*").remove();
|
||||||
|
lineGen.curve(d3.curveBasisClosed);
|
||||||
|
const scheme = d3.scaleSequential(d3.interpolateSpectral);
|
||||||
|
const tMax = +temperatureEquatorOutput.max, tMin = +temperatureEquatorOutput.min, delta = tMax - tMin;
|
||||||
|
|
||||||
|
const cells = grid.cells, vertices = grid.vertices, n = cells.i.length;
|
||||||
|
const used = new Uint8Array(n); // to detect already passed cells
|
||||||
|
const min = d3.min(cells.temp), max = d3.max(cells.temp);
|
||||||
|
const step = Math.max(Math.round(Math.abs(min - max) / 5), 1);
|
||||||
|
const isolines = d3.range(min+step, max, step);
|
||||||
|
const chains = [], labels = []; // store label coordinates
|
||||||
|
|
||||||
|
for (const i of cells.i) {
|
||||||
|
const t = cells.temp[i];
|
||||||
|
if (used[i] || !isolines.includes(t)) continue;
|
||||||
|
const start = findStart(i, t);
|
||||||
|
if (!start) continue;
|
||||||
|
used[i] = 1;
|
||||||
|
//debug.append("circle").attr("r", 3).attr("cx", vertices.p[start][0]).attr("cy", vertices.p[start][1]).attr("fill", "red").attr("stroke", "black").attr("stroke-width", .3);
|
||||||
|
|
||||||
|
const chain = connectVertices(start, t); // vertices chain to form a path
|
||||||
|
const relaxed = chain.filter((v, i) => i%4 === 0 || vertices.c[v].some(c => c >= n));
|
||||||
|
if (relaxed.length < 6) continue;
|
||||||
|
const points = relaxed.map(v => vertices.p[v]);
|
||||||
|
chains.push([t, points]);
|
||||||
|
addLabel(points, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
// min temp isoline covers all map
|
||||||
|
temperature.append("path").attr("d", `M0,0 h${svgWidth} v${svgHeight} h${-svgWidth} Z`).attr("fill", scheme(1 - (min - tMin) / delta)).attr("stroke", "none");
|
||||||
|
|
||||||
|
for (const t of isolines) {
|
||||||
|
const path = chains.filter(c => c[0] === t).map(c => round(lineGen(c[1]))).join();
|
||||||
|
if (!path) continue;
|
||||||
|
const fill = scheme(1 - (t - tMin) / delta), stroke = d3.color(fill).darker(.2);
|
||||||
|
temperature.append("path").attr("d", path).attr("fill", fill).attr("stroke", stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempLabels = temperature.append("g").attr("id", "tempLabels").attr("fill-opacity", 1);
|
||||||
|
tempLabels.selectAll("text").data(labels).enter().append("text").attr("x", d => d[0]).attr("y", d => d[1]).text(d => convertTemperature(d[2]));
|
||||||
|
|
||||||
|
// find cell with temp < isotherm and find vertex to start path detection
|
||||||
|
function findStart(i, t) {
|
||||||
|
if (cells.b[i]) return cells.v[i].find(v => vertices.c[v].some(c => c >= n)); // map border cell
|
||||||
|
return cells.v[i][cells.c[i].findIndex(c => cells.temp[c] < t || !cells.temp[c])];
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLabel(points, t) {
|
||||||
|
const c = svgWidth / 2; // map center x coordinate
|
||||||
|
// add label on isoline top center
|
||||||
|
const tc = points[d3.scan(points, (a, b) => (a[1] - b[1]) + (Math.abs(a[0] - c) - Math.abs(b[0] - c)) / 2)];
|
||||||
|
pushLabel(tc[0], tc[1], t);
|
||||||
|
|
||||||
|
// add label on isoline bottom center
|
||||||
|
if (points.length > 20) {
|
||||||
|
const bc = points[d3.scan(points, (a, b) => (b[1] - a[1]) + (Math.abs(a[0] - c) - Math.abs(b[0] - c)) / 2)];
|
||||||
|
const dist2 = (tc[1] - bc[1]) ** 2 + (tc[0] - bc[0]) ** 2; // square distance between this and top point
|
||||||
|
if (dist2 > 100) pushLabel(bc[0], bc[1], t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushLabel(x, y, t) {
|
||||||
|
if (x < 20 || x > svgWidth - 20) return;
|
||||||
|
if (y < 20 || y > svgHeight - 20) return;
|
||||||
|
labels.push([x, y, t]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect vertices to chain
|
||||||
|
function connectVertices(start, t) {
|
||||||
|
const chain = []; // vertices chain to form a path
|
||||||
|
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
|
||||||
|
const prev = chain[chain.length - 1]; // previous vertex in chain
|
||||||
|
chain.push(current); // add current vertex to sequence
|
||||||
|
const c = vertices.c[current]; // cells adjacent to vertex
|
||||||
|
c.filter(c => cells.temp[c] === t).forEach(c => used[c] = 1);
|
||||||
|
const c0 = c[0] >= n || cells.temp[c[0]] < t;
|
||||||
|
const c1 = c[1] >= n || cells.temp[c[1]] < t;
|
||||||
|
const c2 = c[2] >= n || cells.temp[c[2]] < t;
|
||||||
|
const v = vertices.v[current]; // neighboring vertices
|
||||||
|
if (v[0] !== prev && c0 !== c1) current = v[0];
|
||||||
|
else if (v[1] !== prev && c1 !== c2) current = v[1];
|
||||||
|
else if (v[2] !== prev && c0 !== c2) current = v[2];
|
||||||
|
if (current === chain[chain.length - 1]) {console.error("Next vertex is not found"); break;}
|
||||||
|
}
|
||||||
|
chain.push(start);
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
console.timeEnd("drawTemp");
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleBiomes() {
|
||||||
|
if (!biomes.selectAll("path").size()) {
|
||||||
|
turnButtonOn("toggleBiomes");
|
||||||
|
drawBiomes();
|
||||||
|
} else {
|
||||||
|
biomes.selectAll("path").remove();
|
||||||
|
turnButtonOff("toggleBiomes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawBiomes() {
|
||||||
|
biomes.selectAll("path").remove();
|
||||||
|
const cells = pack.cells, vertices = pack.vertices, n = cells.i.length;
|
||||||
|
const used = new Uint8Array(cells.i.length);
|
||||||
|
const paths = new Array(biomesData.i.length).fill("");
|
||||||
|
|
||||||
|
for (const i of cells.i) {
|
||||||
|
if (!cells.biome[i]) continue; // no need to mark water
|
||||||
|
if (used[i]) continue; // already marked
|
||||||
|
const b = cells.biome[i];
|
||||||
|
const onborder = cells.c[i].some(n => cells.biome[n] !== b);
|
||||||
|
if (!onborder) continue;
|
||||||
|
const edgeVerticle = cells.v[i].find(v => vertices.c[v].some(i => cells.biome[i] !== b));
|
||||||
|
const chain = connectVertices(edgeVerticle, b);
|
||||||
|
if (chain.length < 3) continue;
|
||||||
|
const points = chain.map(v => vertices.p[v]);
|
||||||
|
paths[b] += "M" + points.join("L") + "Z";
|
||||||
|
}
|
||||||
|
|
||||||
|
paths.forEach(function(d, i) {
|
||||||
|
if (d.length < 10) return;
|
||||||
|
const color = biomesData.color[i];
|
||||||
|
biomes.append("path").attr("d", d).attr("fill", color).attr("stroke", color).attr("id", "biome"+i);
|
||||||
|
});
|
||||||
|
|
||||||
|
// connect vertices to chain
|
||||||
|
function connectVertices(start, b) {
|
||||||
|
const chain = []; // vertices chain to form a path
|
||||||
|
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
|
||||||
|
const prev = chain[chain.length - 1]; // previous vertex in chain
|
||||||
|
chain.push(current); // add current vertex to sequence
|
||||||
|
const c = vertices.c[current]; // cells adjacent to vertex
|
||||||
|
c.filter(c => cells.biome[c] === b).forEach(c => used[c] = 1);
|
||||||
|
const c0 = c[0] >= n || cells.biome[c[0]] !== b;
|
||||||
|
const c1 = c[1] >= n || cells.biome[c[1]] !== b;
|
||||||
|
const c2 = c[2] >= n || cells.biome[c[2]] !== b;
|
||||||
|
const v = vertices.v[current]; // neighboring vertices
|
||||||
|
if (v[0] !== prev && c0 !== c1) current = v[0];
|
||||||
|
else if (v[1] !== prev && c1 !== c2) current = v[1];
|
||||||
|
else if (v[2] !== prev && c0 !== c2) current = v[2];
|
||||||
|
if (current === chain[chain.length - 1]) {console.error("Next vertex is not found"); break;}
|
||||||
|
}
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePrec() {
|
||||||
|
if (!prec.selectAll("circle").size()) {
|
||||||
|
turnButtonOn("togglePrec");
|
||||||
|
drawPrec();
|
||||||
|
} else {
|
||||||
|
turnButtonOff("togglePrec");
|
||||||
|
const hide = d3.transition().duration(1000).ease(d3.easeSinIn);
|
||||||
|
prec.selectAll("text").attr("opacity", 1).transition(hide).attr("opacity", 0);
|
||||||
|
prec.selectAll("circle").transition(hide).attr("r", 0).remove();
|
||||||
|
prec.transition().delay(1000).attr("display", "none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawPrec() {
|
||||||
|
prec.selectAll("circle").remove();
|
||||||
|
const cells = grid.cells, p = grid.points;
|
||||||
|
prec.attr("display", "block");
|
||||||
|
const show = d3.transition().duration(800).ease(d3.easeSinIn);
|
||||||
|
prec.selectAll("text").attr("opacity", 0).transition(show).attr("opacity", 1);
|
||||||
|
|
||||||
|
const data = cells.i.filter(i => cells.h[i] >= 20 && cells.prec[i]);
|
||||||
|
prec.selectAll("circle").data(data).enter().append("circle")
|
||||||
|
.attr("cx", d => p[d][0]).attr("cy", d => p[d][1]).attr("r", 0)
|
||||||
|
.transition(show).attr("r", d => rn(Math.max(Math.sqrt(cells.prec[d] * .5), .8),2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePopulation() {
|
||||||
|
if (!population.selectAll("line").size()) {
|
||||||
|
turnButtonOn("togglePopulation");
|
||||||
|
drawPopulation();
|
||||||
|
} else {
|
||||||
|
turnButtonOff("togglePopulation");
|
||||||
|
const hide = d3.transition().duration(1000).ease(d3.easeSinIn);
|
||||||
|
population.select("#rural").selectAll("line").transition(hide).attr("y2", d => d[1]).remove();
|
||||||
|
population.select("#urban").selectAll("line").transition(hide).delay(1000).attr("y2", d => d[1]).remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawPopulation() {
|
||||||
|
population.selectAll("line").remove();
|
||||||
|
const cells = pack.cells, p = cells.p, burgs = pack.burgs;
|
||||||
|
|
||||||
|
// pack.cells.pop.reduce((s=0,v) => s+v)
|
||||||
|
// pack.burgs.map(b => b.population).reduce((s=0,v) => s+v)
|
||||||
|
const show = d3.transition().duration(2000).ease(d3.easeSinIn);
|
||||||
|
|
||||||
|
const rural = Array.from(cells.i.filter(i => cells.pop[i] > 0), i => [p[i][0], p[i][1], p[i][1] - cells.pop[i] / 8]);
|
||||||
|
population.select("#rural").selectAll("line").data(rural).enter().append("line")
|
||||||
|
.attr("x1", d => d[0]).attr("y1", d => d[1])
|
||||||
|
.attr("x2", d => d[0]).attr("y2", d => d[1])
|
||||||
|
.transition(show).attr("y2", d => d[2]);
|
||||||
|
|
||||||
|
const urban = burgs.filter(b => b.i).map(b => [b.x, b.y, b.y - b.population / 8 * urbanization.value]);
|
||||||
|
population.select("#urban").selectAll("line").data(urban).enter().append("line")
|
||||||
|
.attr("x1", d => d[0]).attr("y1", d => d[1])
|
||||||
|
.attr("x2", d => d[0]).attr("y2", d => d[1])
|
||||||
|
.transition(show).delay(500).attr("y2", d => d[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCells() {
|
||||||
|
if (!cells.selectAll("path").size()) {
|
||||||
|
turnButtonOn("toggleCells");
|
||||||
|
drawCells();
|
||||||
|
} else {
|
||||||
|
cells.selectAll("path").remove();
|
||||||
|
turnButtonOff("toggleCells");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawCells() {
|
||||||
|
cells.selectAll("path").remove();
|
||||||
|
const data = customization === 1 ? grid.cells.i : pack.cells.i;
|
||||||
|
const polygon = customization === 1 ? getGridPolygon : getPackPolygon;
|
||||||
|
let path = "";
|
||||||
|
data.forEach(i => path += "M" + polygon(i));
|
||||||
|
cells.append("path").attr("d", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCultures() {
|
||||||
|
if (!cults.selectAll("path").size()) {
|
||||||
|
turnButtonOn("toggleCultures");
|
||||||
|
drawCultures();
|
||||||
|
} else {
|
||||||
|
cults.selectAll("path").remove();
|
||||||
|
turnButtonOff("toggleCultures");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawCultures() {
|
||||||
|
console.time("drawCultures");
|
||||||
|
|
||||||
|
cults.selectAll("path").remove();
|
||||||
|
const cells = pack.cells, vertices = pack.vertices, cultures = pack.cultures, n = cells.i.length;
|
||||||
|
const used = new Uint8Array(cells.i.length);
|
||||||
|
const paths = new Array(cultures.length).fill("");
|
||||||
|
|
||||||
|
for (const i of cells.i) {
|
||||||
|
if (!cells.culture[i]) continue;
|
||||||
|
if (used[i]) continue;
|
||||||
|
used[i] = 1;
|
||||||
|
const c = cells.culture[i];
|
||||||
|
const onborder = cells.c[i].some(n => cells.culture[n] !== c);
|
||||||
|
if (!onborder) continue;
|
||||||
|
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.culture[i] !== c));
|
||||||
|
const chain = connectVertices(vertex, c);
|
||||||
|
if (chain.length < 3) continue;
|
||||||
|
const points = chain.map(v => vertices.p[v]);
|
||||||
|
paths[c] += "M" + points.join("L") + "Z";
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = paths.map((p, i) => [p, i, cultures[i].color]).filter(d => d[0].length > 10);
|
||||||
|
cults.selectAll("path").data(data).enter().append("path").attr("d", d => d[0]).attr("fill", d => d[2]).attr("id", d => "culture"+d[1]);
|
||||||
|
|
||||||
|
// connect vertices to chain
|
||||||
|
function connectVertices(start, t) {
|
||||||
|
const chain = []; // vertices chain to form a path
|
||||||
|
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
|
||||||
|
const prev = chain[chain.length - 1]; // previous vertex in chain
|
||||||
|
chain.push(current); // add current vertex to sequence
|
||||||
|
const c = vertices.c[current]; // cells adjacent to vertex
|
||||||
|
c.filter(c => cells.culture[c] === t).forEach(c => used[c] = 1);
|
||||||
|
const c0 = c[0] >= n || cells.culture[c[0]] !== t;
|
||||||
|
const c1 = c[1] >= n || cells.culture[c[1]] !== t;
|
||||||
|
const c2 = c[2] >= n || cells.culture[c[2]] !== t;
|
||||||
|
const v = vertices.v[current]; // neighboring vertices
|
||||||
|
if (v[0] !== prev && c0 !== c1) current = v[0];
|
||||||
|
else if (v[1] !== prev && c1 !== c2) current = v[1];
|
||||||
|
else if (v[2] !== prev && c0 !== c2) current = v[2];
|
||||||
|
if (current === chain[chain.length - 1]) {console.error("Next vertex is not found"); break;}
|
||||||
|
}
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
console.timeEnd("drawCultures");
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleStates() {
|
||||||
|
if (!layerIsOn("toggleStates")) {
|
||||||
|
turnButtonOn("toggleStates");
|
||||||
|
regions.attr("display", null);
|
||||||
|
drawStatesWithBorders();
|
||||||
|
} else {
|
||||||
|
regions.attr("display", "none").selectAll("path").remove();
|
||||||
|
turnButtonOff("toggleStates");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawStatesWithBorders() {
|
||||||
|
console.time("drawStatesWithBorders");
|
||||||
|
regions.selectAll("path").remove();
|
||||||
|
borders.selectAll("path").remove();
|
||||||
|
|
||||||
|
const cells = pack.cells, vertices = pack.vertices, states = pack.states, n = cells.i.length;
|
||||||
|
const used = new Uint8Array(cells.i.length);
|
||||||
|
const body = new Array(states.length).fill(""); // store path around each state
|
||||||
|
const gap = new Array(states.length).fill(""); // store path along water for each state to fill the gaps
|
||||||
|
const border = new Array(states.length).fill(""); // store path along land for all states to render borders
|
||||||
|
|
||||||
|
for (const i of cells.i) {
|
||||||
|
if (!cells.state[i] || used[i]) continue;
|
||||||
|
used[i] = 1;
|
||||||
|
const s = cells.state[i];
|
||||||
|
const onborder = cells.c[i].some(n => cells.state[n] !== s);
|
||||||
|
if (!onborder) continue;
|
||||||
|
|
||||||
|
const borderWith = cells.c[i].map(c => cells.state[c]).find(n => n !== s);
|
||||||
|
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.state[i] === borderWith));
|
||||||
|
const chain = connectVertices(vertex, s, borderWith);
|
||||||
|
if (chain.length < 3) continue;
|
||||||
|
body[s] += "M" + chain.map(v => vertices.p[v[0]]).join("L");
|
||||||
|
gap[s] += "M" + vertices.p[chain[0][0]] + chain.reduce((r,v,i,d) => !i ? r : !v[2] ? r + "L" + vertices.p[v[0]] : d[i+1] && !d[i+1][2] ? r + "M" + vertices.p[v[0]] : r, "");
|
||||||
|
border[s] += "M" + vertices.p[chain[0][0]] + chain.reduce((r,v,i,d) => !i ? r : v[2] && s > v[1] ? r + "L" + vertices.p[v[0]] : d[i+1] && d[i+1][2] && s > d[i+1][1] ? r + "M" + vertices.p[v[0]] : r, "");
|
||||||
|
|
||||||
|
// debug.append("circle").attr("r", 2).attr("cx", cells.p[i][0]).attr("cy", cells.p[i][1]).attr("fill", "blue");
|
||||||
|
// const p = chain.map(v => vertices.p[v[0]])
|
||||||
|
// debug.selectAll(".circle").data(p).enter().append("circle").attr("cx", d => d[0]).attr("cy", d => d[1]).attr("r", 1).attr("fill", "red");
|
||||||
|
// const poly = polylabel([p], 1.0); // pole of inaccessibility
|
||||||
|
// debug.append("circle").attr("r", 2).attr("cx", poly[0]).attr("cy", poly[1]).attr("fill", "green");
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, states[i].color]).filter(d => d[0]);
|
||||||
|
statesBody.selectAll("path").data(bodyData).enter().append("path").attr("d", d => d[0]).attr("fill", d => d[2]).attr("stroke", "none").attr("id", d => "state"+d[1]);
|
||||||
|
const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, states[i].color]).filter(d => d[0]);
|
||||||
|
statesBody.selectAll(".path").data(gapData).enter().append("path").attr("d", d => d[0]).attr("fill", "none").attr("stroke", d => d[2]).attr("id", d => "state-gap"+d[1]);
|
||||||
|
|
||||||
|
defs.select("#statePaths").selectAll("clipPath").remove();
|
||||||
|
defs.select("#statePaths").selectAll("clipPath").data(bodyData).enter().append("clipPath").attr("id", d => "state-clip"+d[1]).append("use").attr("href", d => "#state"+d[1]);
|
||||||
|
statesHalo.selectAll(".path").data(bodyData).enter().append("path").attr("d", d => d[0]).attr("stroke", d => d3.color(d[2]).darker().hex()).attr("id", d => "state-border"+d[1]).attr("clip-path", d => "url(#state-clip"+d[1]+")");
|
||||||
|
|
||||||
|
const borderData = border.map((p, i) => [p.length > 10 ? p : null, i]).filter(d => d[0]);
|
||||||
|
borders.selectAll("path").data(borderData).enter().append("path").attr("d", d => d[0]).attr("id", d => "border"+d[1]);
|
||||||
|
|
||||||
|
// connect vertices to chain
|
||||||
|
function connectVertices(start, t, state) {
|
||||||
|
const chain = []; // vertices chain to form a path
|
||||||
|
let land = vertices.c[start].some(c => cells.h[c] >= 20 && cells.state[c] !== t);
|
||||||
|
function check(i) {state = cells.state[i]; land = cells.h[i] >= 20;}
|
||||||
|
|
||||||
|
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
|
||||||
|
const prev = chain[chain.length - 1] ? chain[chain.length - 1][0] : -1; // previous vertex in chain
|
||||||
|
chain.push([current, state, land]); // add current vertex to sequence
|
||||||
|
const c = vertices.c[current]; // cells adjacent to vertex
|
||||||
|
c.filter(c => cells.state[c] === t).forEach(c => used[c] = 1);
|
||||||
|
const c0 = c[0] >= n || cells.state[c[0]] !== t;
|
||||||
|
const c1 = c[1] >= n || cells.state[c[1]] !== t;
|
||||||
|
const c2 = c[2] >= n || cells.state[c[2]] !== t;
|
||||||
|
const v = vertices.v[current]; // neighboring vertices
|
||||||
|
if (v[0] !== prev && c0 !== c1) {current = v[0]; check(c0 ? c[0] : c[1]);} else
|
||||||
|
if (v[1] !== prev && c1 !== c2) {current = v[1]; check(c1 ? c[1] : c[2]);} else
|
||||||
|
if (v[2] !== prev && c0 !== c2) {current = v[2]; check(c2 ? c[2] : c[0]);}
|
||||||
|
if (current === chain[chain.length - 1][0]) {console.error("Next vertex is not found"); break;}
|
||||||
|
}
|
||||||
|
chain.push([start, state, land]); // add starting vertex to sequence to close the path
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
console.timeEnd("drawStatesWithBorders");
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleBorders() {
|
||||||
|
if (!layerIsOn("toggleBorders")) {
|
||||||
|
turnButtonOn("toggleBorders");
|
||||||
|
$('#borders').fadeIn();
|
||||||
|
} else {
|
||||||
|
turnButtonOff("toggleBorders");
|
||||||
|
$('#borders').fadeOut();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleGrid() {
|
||||||
|
if (!gridOverlay.selectAll("*").size()) {
|
||||||
|
turnButtonOn("toggleGrid");
|
||||||
|
drawGrid();
|
||||||
|
calculateFriendlyGridSize();
|
||||||
|
} else {
|
||||||
|
turnButtonOff("toggleGrid");
|
||||||
|
gridOverlay.selectAll("*").remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawGrid() {
|
||||||
|
console.time("drawGrid");
|
||||||
|
gridOverlay.selectAll("*").remove();
|
||||||
|
const type = styleGridType.value;
|
||||||
|
const size = +styleGridSize.value;
|
||||||
|
if (type === "pointyHex" || type === "flatHex") {
|
||||||
|
const points = getHexGridPoints(size, type);
|
||||||
|
const hex = "m" + getHex(size, type).slice(0, 4).join("l");
|
||||||
|
const d = points.map(p => "M" + p + hex).join("");
|
||||||
|
gridOverlay.append("path").attr("d", d);
|
||||||
|
} else if (type === "square") {
|
||||||
|
const pathX = d3.range(size, svgWidth, size).map(x => "M" + rn(x, 2) + ",0v" + svgHeight);
|
||||||
|
const pathY = d3.range(size, svgHeight, size).map(y => "M0," + rn(y, 2) + "h" + svgWidth);
|
||||||
|
gridOverlay.append("path").attr("d", pathX + pathY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate hexes centers
|
||||||
|
function getHexGridPoints(size, type) {
|
||||||
|
const points = [];
|
||||||
|
const rt3 = Math.sqrt(3);
|
||||||
|
const off = type === "pointyHex" ? rn(rt3 * size / 2, 2) : rn(size * 3 / 2, 2);
|
||||||
|
const ySpace = type === "pointyHex" ? rn(size * 3 / 2, 2) : rn(rt3 * size / 2, 2);
|
||||||
|
const xSpace = type === "pointyHex" ? rn(rt3 * size, 2) : rn(size * 3, 2);
|
||||||
|
for (let y = 0, l = 0; y < graphHeight+ySpace; y += ySpace, l++) {
|
||||||
|
for (let x = l % 2 ? 0 : off; x < graphWidth+xSpace; x += xSpace) {points.push([rn(x, 2), rn(y, 2)]);}
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate hex points
|
||||||
|
function getHex(radius, type) {
|
||||||
|
let x0 = 0, y0 = 0;
|
||||||
|
const s = type === "pointyHex" ? 0 : Math.PI / -6;
|
||||||
|
const thirdPi = Math.PI / 3;
|
||||||
|
let angles = [s, s + thirdPi, s + 2 * thirdPi, s + 3 * thirdPi, s + 4 * thirdPi, s + 5 * thirdPi];
|
||||||
|
return angles.map(function(a) {
|
||||||
|
const x1 = Math.sin(a) * radius;
|
||||||
|
const y1 = -Math.cos(a) * radius;
|
||||||
|
const dx = rn(x1 - x0, 2);
|
||||||
|
const dy = rn(y1 - y0, 2);
|
||||||
|
x0 = x1, y0 = y1;
|
||||||
|
return [rn(dx, 2), rn(dy, 2)];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.timeEnd("drawGrid");
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCoordinates() {
|
||||||
|
if (!coordinates.selectAll("*").size()) {
|
||||||
|
turnButtonOn("toggleCoordinates");
|
||||||
|
drawCoordinates();
|
||||||
|
} else {
|
||||||
|
turnButtonOff("toggleCoordinates");
|
||||||
|
coordinates.selectAll("*").remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawCoordinates() {
|
||||||
|
if (!layerIsOn("toggleCoordinates")) return;
|
||||||
|
coordinates.selectAll("*").remove(); // remove every time
|
||||||
|
const eqY = +document.getElementById("equatorOutput").value;
|
||||||
|
const eqD = +document.getElementById("equidistanceOutput").value;
|
||||||
|
const merX = svgWidth / 2; // x of zero meridian
|
||||||
|
const steps = [.5, 1, 2, 5, 10, 15, 30]; // possible steps
|
||||||
|
const goal = merX / eqD / scale ** 0.4 * 12;
|
||||||
|
const step = steps.reduce((p, c) => Math.abs(c - goal) < Math.abs(p - goal) ? c : p);
|
||||||
|
const p = getViewPoint(2 + scale, 2 + scale); // on border point on viexBox
|
||||||
|
const desired = +coordinates.attr("data-size")
|
||||||
|
const size = Math.max(desired + 1 - scale, 2);
|
||||||
|
coordinates.attr("font-size", size);
|
||||||
|
|
||||||
|
// map coordinates extent
|
||||||
|
const extent = getViewBoxExtent();
|
||||||
|
const latS = mapCoordinates.latS + (1 - extent[1][1] / svgHeight) * mapCoordinates.latT;
|
||||||
|
const latN = mapCoordinates.latN - (extent[0][1] / svgHeight) * mapCoordinates.latT;
|
||||||
|
const lonW = mapCoordinates.lonW + (extent[0][0] / svgWidth) * mapCoordinates.lonT;
|
||||||
|
const lonE = mapCoordinates.lonE - (1 - extent[1][0] / svgWidth) * mapCoordinates.lonT;
|
||||||
|
|
||||||
|
const grid = coordinates.append("g").attr("id", "coordinateGrid");
|
||||||
|
const lalitude = coordinates.append("g").attr("id", "lalitude");
|
||||||
|
const longitude = coordinates.append("g").attr("id", "longitude");
|
||||||
|
|
||||||
|
// rander lalitude lines
|
||||||
|
d3.range(nextStep(latS), nextStep(latN)+0.01, step).forEach(function(l) {
|
||||||
|
const c = eqY - l / 90 * eqD;
|
||||||
|
const lat = l < 0 ? Math.abs(l) + "°S" : l + "°N";
|
||||||
|
grid.append("line").attr("x1", 0).attr("x2", svgWidth).attr("y1", c).attr("y2", c).attr("l", l);
|
||||||
|
const nearBorder = c - size <= extent[0][1] || c + size / 2 >= extent[1][1];
|
||||||
|
if (nearBorder || !Number.isInteger(l)) return;
|
||||||
|
lalitude.append("text").attr("x", p.x).attr("y", c).text(lat);
|
||||||
|
});
|
||||||
|
|
||||||
|
// rander longitude lines
|
||||||
|
d3.range(nextStep(lonW), nextStep(lonE)+0.01, step).forEach(function(l) {
|
||||||
|
const c = merX + l / 90 * eqD;
|
||||||
|
const lon = l < 0 ? Math.abs(l) + "°W" : l + "°E";
|
||||||
|
grid.append("line").attr("x1", c).attr("x2", c).attr("y1", 0).attr("y2", svgHeight).attr("l", l);
|
||||||
|
const nearBorder = c - size * 1.5 <= extent[0][0] || c + size >= extent[1][0];
|
||||||
|
if (nearBorder || !Number.isInteger(l)) return;
|
||||||
|
longitude.append("text").attr("x", c).attr("y", p.y).text(lon);
|
||||||
|
});
|
||||||
|
|
||||||
|
function nextStep(v) {return (v / step | 0) * step;}
|
||||||
|
}
|
||||||
|
|
||||||
|
// conver svg point into viewBox point
|
||||||
|
function getViewPoint(x, y) {
|
||||||
|
const view = document.getElementById('viewbox');
|
||||||
|
const svg = document.getElementById('map');
|
||||||
|
const pt = svg.createSVGPoint();
|
||||||
|
pt.x = x, pt.y = y;
|
||||||
|
return pt.matrixTransform(view.getScreenCTM().inverse());
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCompass() {
|
||||||
|
if (!layerIsOn("toggleCompass")) {
|
||||||
|
turnButtonOn("toggleCompass");
|
||||||
|
$('#compass').fadeIn();
|
||||||
|
if (!compass.selectAll("*").size()) {
|
||||||
|
const tr = `translate(80 80) scale(.25)`;
|
||||||
|
d3.select("#rose").attr("transform", tr);
|
||||||
|
compass.append("use").attr("xlink:href","#rose");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$('#compass').fadeOut();
|
||||||
|
turnButtonOff("toggleCompass");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRelief() {
|
||||||
|
if (!layerIsOn("toggleRelief")) {
|
||||||
|
turnButtonOn("toggleRelief");
|
||||||
|
if (!terrain.selectAll("*").size()) ReliefIcons();
|
||||||
|
$('#terrain').fadeIn();
|
||||||
|
} else {
|
||||||
|
$('#terrain').fadeOut();
|
||||||
|
turnButtonOff("toggleRelief");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTexture() {
|
||||||
|
if (!layerIsOn("toggleTexture")) {
|
||||||
|
turnButtonOn("toggleTexture");
|
||||||
|
// append default texture image selected by default. Don't append on load to not harm performance
|
||||||
|
if (!texture.selectAll("*").size()) {
|
||||||
|
const link = getAbsolutePath(styleTextureInput.value);
|
||||||
|
texture.append("image").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%")
|
||||||
|
.attr('xlink:href', link).attr('preserveAspectRatio', "xMidYMid slice");
|
||||||
|
}
|
||||||
|
$('#texture').fadeIn();
|
||||||
|
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
|
||||||
|
} else {
|
||||||
|
$('#texture').fadeOut();
|
||||||
|
turnButtonOff("toggleTexture");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRivers() {
|
||||||
|
if (!layerIsOn("toggleRivers")) {
|
||||||
|
turnButtonOn("toggleRivers");
|
||||||
|
$('#rivers').fadeIn();
|
||||||
|
} else {
|
||||||
|
$('#rivers').fadeOut();
|
||||||
|
turnButtonOff("toggleRivers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRoutes() {
|
||||||
|
if (!layerIsOn("toggleRoutes")) {
|
||||||
|
turnButtonOn("toggleRoutes");
|
||||||
|
$('#routes').fadeIn();
|
||||||
|
} else {
|
||||||
|
$('#routes').fadeOut();
|
||||||
|
turnButtonOff("toggleRoutes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMarkers() {
|
||||||
|
if (!layerIsOn("toggleMarkers")) {
|
||||||
|
turnButtonOn("toggleMarkers");
|
||||||
|
$('#markers').fadeIn();
|
||||||
|
} else {
|
||||||
|
$('#markers').fadeOut();
|
||||||
|
turnButtonOff("toggleMarkers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleLabels() {
|
||||||
|
if (!layerIsOn("toggleLabels")) {
|
||||||
|
turnButtonOn("toggleLabels");
|
||||||
|
$('#labels').fadeIn();
|
||||||
|
} else {
|
||||||
|
turnButtonOff("toggleLabels");
|
||||||
|
$('#labels').fadeOut();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleIcons() {
|
||||||
|
if (!layerIsOn("toggleIcons")) {
|
||||||
|
turnButtonOn("toggleIcons");
|
||||||
|
$('#icons').fadeIn();
|
||||||
|
} else {
|
||||||
|
turnButtonOff("toggleIcons");
|
||||||
|
$('#icons').fadeOut();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRulers() {
|
||||||
|
if (!layerIsOn("toggleRulers")) {
|
||||||
|
turnButtonOn("toggleRulers");
|
||||||
|
$('#ruler').fadeIn();
|
||||||
|
} else {
|
||||||
|
$('#ruler').fadeOut();
|
||||||
|
turnButtonOff("toggleRulers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleScaleBar() {
|
||||||
|
if (!layerIsOn("toggleScaleBar")) {
|
||||||
|
turnButtonOn("toggleScaleBar");
|
||||||
|
$('#scaleBar').fadeIn();
|
||||||
|
} else {
|
||||||
|
$('#scaleBar').fadeOut();
|
||||||
|
turnButtonOff("toggleScaleBar");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function layerIsOn(el) {
|
||||||
|
const buttonoff = document.getElementById(el).classList.contains("buttonoff");
|
||||||
|
return !buttonoff;
|
||||||
|
}
|
||||||
|
|
||||||
|
function turnButtonOff(el) {
|
||||||
|
document.getElementById(el).classList.add("buttonoff");
|
||||||
|
layersPreset.value = "custom";
|
||||||
|
}
|
||||||
|
|
||||||
|
function turnButtonOn(el) {
|
||||||
|
document.getElementById(el).classList.remove("buttonoff");
|
||||||
|
layersPreset.value = "custom";
|
||||||
|
}
|
||||||
|
|
||||||
|
// move layers on mapLayers dragging (jquery sortable)
|
||||||
|
$("#mapLayers").sortable({items: "li:not(.solid)", cancel: ".solid", update: moveLayer});
|
||||||
|
function moveLayer(event, ui) {
|
||||||
|
const el = getLayer(ui.item.attr("id"));
|
||||||
|
if (el) {
|
||||||
|
const prev = getLayer(ui.item.prev().attr("id"));
|
||||||
|
const next = getLayer(ui.item.next().attr("id"));
|
||||||
|
if (prev) el.insertAfter(prev); else if (next) el.insertBefore(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// define connection between option layer buttons and actual svg groups to move the element
|
||||||
|
function getLayer(id) {
|
||||||
|
if (id === "toggleHeight") return $("#terrs");
|
||||||
|
if (id === "toggleBiomes") return $("#biomes");
|
||||||
|
if (id === "toggleCells") return $("#cells");
|
||||||
|
if (id === "toggleGrid") return $("#gridOverlay");
|
||||||
|
if (id === "toggleCoordinates") return $("#coordinates");
|
||||||
|
if (id === "toggleCompass") return $("#compass");
|
||||||
|
if (id === "toggleRivers") return $("#rivers");
|
||||||
|
if (id === "toggleRelief") return $("#terrain");
|
||||||
|
if (id === "toggleCultures") return $("#cults");
|
||||||
|
if (id === "toggleStates") return $("#regions");
|
||||||
|
if (id === "toggleBorders") return $("#borders");
|
||||||
|
if (id === "toggleRoutes") return $("#routes");
|
||||||
|
if (id === "toggleTemp") return $("#temperature");
|
||||||
|
if (id === "togglePrec") return $("#prec");
|
||||||
|
if (id === "togglePopulation") return $("#population");
|
||||||
|
if (id === "toggleTexture") return $("#texture");
|
||||||
|
if (id === "toggleLabels") return $("#labels");
|
||||||
|
if (id === "toggleIcons") return $("#icons");
|
||||||
|
if (id === "toggleMarkers") return $("#markers");
|
||||||
|
if (id === "toggleRulers") return $("#ruler");
|
||||||
|
}
|
||||||
147
modules/ui/legends-editor.js
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
"use strict";
|
||||||
|
function editLegends(id, name) {
|
||||||
|
// update list of objects
|
||||||
|
const select = document.getElementById("legendSelect");
|
||||||
|
for (let i = select.options.length; i < notes.length; i++) {
|
||||||
|
select.options.add(new Option(notes[i].id, notes[i].id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// select an object
|
||||||
|
if (id) {
|
||||||
|
let note = notes.find(note => note.id === id);
|
||||||
|
if (note === undefined) {
|
||||||
|
if (!name) name = id;
|
||||||
|
note = {id, name, legend: ""};
|
||||||
|
notes.push(note);
|
||||||
|
select.options.add(new Option(id, id));
|
||||||
|
}
|
||||||
|
select.value = id;
|
||||||
|
legendName.value = note.name;
|
||||||
|
legendText.value = note.legend;
|
||||||
|
}
|
||||||
|
|
||||||
|
// open a dialog
|
||||||
|
$("#legendEditor").dialog({
|
||||||
|
title: "Legends Editor", minWidth: Math.min(svgWidth, 400),
|
||||||
|
position: {my: "center", at: "center", of: "svg"}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (modules.editLegends) return;
|
||||||
|
modules.editLegends = true;
|
||||||
|
|
||||||
|
// add listeners
|
||||||
|
document.getElementById("legendSelect").addEventListener("change", changeObject);
|
||||||
|
document.getElementById("legendName").addEventListener("input", changeName);
|
||||||
|
document.getElementById("legendText").addEventListener("input", changeText);
|
||||||
|
document.getElementById("legendFocus").addEventListener("click", validateHighlightElement);
|
||||||
|
document.getElementById("legendDownload").addEventListener("click", downloadLegends);
|
||||||
|
document.getElementById("legendUpload").addEventListener("click", () => legendsToLoad.click());
|
||||||
|
document.getElementById("legendsToLoad").addEventListener("change", uploadLegends);
|
||||||
|
document.getElementById("legendRemove").addEventListener("click", triggerLegendRemove);
|
||||||
|
|
||||||
|
function changeObject() {
|
||||||
|
const note = notes.find(note => note.id === this.value);
|
||||||
|
legendName.value = note.name;
|
||||||
|
legendText.value = note.legend;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeName() {
|
||||||
|
const id = document.getElementById("legendSelect").value;
|
||||||
|
const note = notes.find(note => note.id === id);
|
||||||
|
note.name = this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeText() {
|
||||||
|
const id = document.getElementById("legendSelect").value;
|
||||||
|
const note = notes.find(note => note.id === id);
|
||||||
|
note.legend = this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateHighlightElement() {
|
||||||
|
const select = document.getElementById("legendSelect");
|
||||||
|
const element = document.getElementById(select.value);
|
||||||
|
|
||||||
|
// if element is not found
|
||||||
|
if (element === null) {
|
||||||
|
alertMessage.innerHTML = "Related element is not found. Would you like to remove the note (legend item)?";
|
||||||
|
$("#alert").dialog({resizable: false, title: "Element not found",
|
||||||
|
buttons: {
|
||||||
|
Remove: function() {$(this).dialog("close"); removeLegend();},
|
||||||
|
Keep: function() {$(this).dialog("close");}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
highlightElement(element); // if element is found
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightElement(element) {
|
||||||
|
if (debug.select(".highlighted").size()) return; // allow only 1 highlight element simultaniosly
|
||||||
|
const box = element.getBBox();
|
||||||
|
const transform = element.getAttribute("transform") || null;
|
||||||
|
const t = d3.transition().duration(1000).ease(d3.easeBounceOut);
|
||||||
|
const r = d3.transition().duration(500).ease(d3.easeLinear);
|
||||||
|
|
||||||
|
const highlight = debug.append("rect").attr("x", box.x).attr("y", box.y)
|
||||||
|
.attr("width", box.width).attr("height", box.height).attr("transform", transform);
|
||||||
|
|
||||||
|
highlight.classed("highlighted", 1)
|
||||||
|
.transition(t).style("outline-offset", "0px")
|
||||||
|
.transition(r).style("outline-color", "transparent").remove();
|
||||||
|
|
||||||
|
const tr = parseTransform(transform);
|
||||||
|
let x = box.x + box.width / 2;
|
||||||
|
if (tr[0]) x += tr[0];
|
||||||
|
let y = box.y + box.height / 2;
|
||||||
|
if (tr[1]) y += tr[1];
|
||||||
|
if (scale >= 2) zoomTo(x, y, scale, 1600);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadLegends() {
|
||||||
|
const legendString = JSON.stringify(notes);
|
||||||
|
const dataBlob = new Blob([legendString],{type:"text/plain"});
|
||||||
|
const url = window.URL.createObjectURL(dataBlob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.download = "legends" + Date.now() + ".txt";
|
||||||
|
link.href = url;
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadLegends() {
|
||||||
|
const fileToLoad = this.files[0];
|
||||||
|
this.value = "";
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
fileReader.onload = function(fileLoadedEvent) {
|
||||||
|
const dataLoaded = fileLoadedEvent.target.result;
|
||||||
|
if (dataLoaded) {
|
||||||
|
notes = JSON.parse(dataLoaded);
|
||||||
|
document.getElementById("legendSelect").options.length = 0;
|
||||||
|
editLegends(notes[0].id, notes[0].name);
|
||||||
|
} else {
|
||||||
|
tip("Cannot load a file. Please check the data format", false, "error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileReader.readAsText(fileToLoad, "UTF-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerLegendRemove() {
|
||||||
|
alertMessage.innerHTML = "Are you sure you want to remove the selected legend?";
|
||||||
|
$("#alert").dialog({resizable: false, title: "Remove legend element",
|
||||||
|
buttons: {
|
||||||
|
Remove: function() {$(this).dialog("close"); removeLegend();},
|
||||||
|
Keep: function() {$(this).dialog("close");}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLegend() {
|
||||||
|
const select = document.getElementById("legendSelect");
|
||||||
|
const index = notes.findIndex(n => n.id === select.value);
|
||||||
|
notes.splice(index, 1);
|
||||||
|
select.options.length = 0;
|
||||||
|
if (!notes.length) {$("#legendEditor").dialog("close"); return;}
|
||||||
|
editLegends(notes[0].id, notes[0].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
474
modules/ui/markers-editor.js
Normal file
|
|
@ -0,0 +1,474 @@
|
||||||
|
"use strict";
|
||||||
|
function editMarker() {
|
||||||
|
if (customization) return;
|
||||||
|
closeDialogs("#markerEditor, .stable");
|
||||||
|
$("#markerEditor").dialog();
|
||||||
|
|
||||||
|
elSelected = d3.select(d3.event.target).call(d3.drag().on("start", dragMarker)).classed("draggable", true);
|
||||||
|
updateInputs();
|
||||||
|
|
||||||
|
if (modules.editMarker) return;
|
||||||
|
modules.editMarker = true;
|
||||||
|
|
||||||
|
$("#markerEditor").dialog({
|
||||||
|
title: "Edit Marker", resizable: false,
|
||||||
|
position: {my: "center top+30", at: "bottom", of: d3.event, collision: "fit"},
|
||||||
|
close: closeMarkerEditor
|
||||||
|
});
|
||||||
|
|
||||||
|
// add listeners
|
||||||
|
document.getElementById("markerGroup").addEventListener("click", toggleGroupSection);
|
||||||
|
document.getElementById("markerAddGroup").addEventListener("click", toggleGroupInput);
|
||||||
|
document.getElementById("markerSelectGroup").addEventListener("change", changeGroup);
|
||||||
|
document.getElementById("markerInputGroup").addEventListener("change", createGroup);
|
||||||
|
document.getElementById("markerRemoveGroup").addEventListener("click", removeGroup);
|
||||||
|
|
||||||
|
document.getElementById("markerIcon").addEventListener("click", toggleIconSection);
|
||||||
|
document.getElementById("markerIconSize").addEventListener("input", changeIconSize);
|
||||||
|
document.getElementById("markerIconShiftX").addEventListener("input", changeIconShiftX);
|
||||||
|
document.getElementById("markerIconShiftY").addEventListener("input", changeIconShiftY);
|
||||||
|
document.getElementById("markerIconCustom").addEventListener("input", applyCustomUnicodeIcon);
|
||||||
|
|
||||||
|
document.getElementById("markerStyle").addEventListener("click", toggleStyleSection);
|
||||||
|
document.getElementById("markerSize").addEventListener("input", changeMarkerSize);
|
||||||
|
document.getElementById("markerBaseStroke").addEventListener("input", changePinStroke);
|
||||||
|
document.getElementById("markerBaseFill").addEventListener("input", changePinFill);
|
||||||
|
document.getElementById("markerIconStrokeWidth").addEventListener("input", changeIconStrokeWidth);
|
||||||
|
document.getElementById("markerIconStroke").addEventListener("input", changeIconStroke);
|
||||||
|
document.getElementById("markerIconFill").addEventListener("input", changeIconFill);
|
||||||
|
|
||||||
|
document.getElementById("markerToggleBubble").addEventListener("click", togglePinVisibility);
|
||||||
|
document.getElementById("markerLegendButton").addEventListener("click", editMarkerLegend);
|
||||||
|
document.getElementById("markerAdd").addEventListener("click", toggleAddMarker);
|
||||||
|
document.getElementById("markerRemove").addEventListener("click", removeMarker);
|
||||||
|
|
||||||
|
updateGroupOptions();
|
||||||
|
|
||||||
|
function dragMarker() {
|
||||||
|
const tr = parseTransform(this.getAttribute("transform"));
|
||||||
|
const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y;
|
||||||
|
|
||||||
|
d3.event.on("drag", function() {
|
||||||
|
const transform = `translate(${(x + d3.event.x)},${(y + d3.event.y)})`;
|
||||||
|
this.setAttribute("transform", transform);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateInputs() {
|
||||||
|
const id = elSelected.attr("data-id");
|
||||||
|
const symbol = d3.select("#defs-markers").select(id);
|
||||||
|
const icon = symbol.select("text");
|
||||||
|
|
||||||
|
markerSelectGroup.value = id.slice(1);
|
||||||
|
markerIconSize.value = parseFloat(icon.attr("font-size"));
|
||||||
|
markerIconShiftX.value = parseFloat(icon.attr("x"));
|
||||||
|
markerIconShiftY.value = parseFloat(icon.attr("y"));
|
||||||
|
|
||||||
|
markerSize.value = elSelected.attr("data-size");
|
||||||
|
markerBaseStroke.value = symbol.select("path").attr("fill");
|
||||||
|
markerBaseFill.value = symbol.select("circle").attr("fill");
|
||||||
|
|
||||||
|
markerIconStrokeWidth.value = icon.attr("stroke-width");
|
||||||
|
markerIconStroke.value = icon.attr("stroke");
|
||||||
|
markerIconFill.value = icon.attr("fill");
|
||||||
|
|
||||||
|
markerToggleBubble.className = symbol.select("circle").attr("opacity") === "0" ? "icon-info" : "icon-info-circled";
|
||||||
|
|
||||||
|
const table = document.getElementById("markerIconTable");
|
||||||
|
let selected = table.getElementsByClassName("selected");
|
||||||
|
if (selected.length) selected[0].removeAttribute("class");
|
||||||
|
selected = document.querySelectorAll("#markerIcon" + icon.text().codePointAt());
|
||||||
|
if (selected.length) selected[0].className = "selected";
|
||||||
|
markerIconCustom.value = selected.length ? "" : icon.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleGroupSection() {
|
||||||
|
if (markerGroupSection.style.display === "inline-block") {
|
||||||
|
markerEditor.querySelectorAll("button:not(#markerGroup)").forEach(b => b.style.display = "inline-block");
|
||||||
|
markerGroupSection.style.display = "none";
|
||||||
|
} else {
|
||||||
|
markerEditor.querySelectorAll("button:not(#markerGroup)").forEach(b => b.style.display = "none");
|
||||||
|
markerGroupSection.style.display = "inline-block";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateGroupOptions() {
|
||||||
|
markerSelectGroup.innerHTML = "";
|
||||||
|
d3.select("#defs-markers").selectAll("symbol").each(function() {
|
||||||
|
markerSelectGroup.options.add(new Option(this.id, this.id));
|
||||||
|
});
|
||||||
|
markerSelectGroup.value = elSelected.attr("data-id").slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleGroupInput() {
|
||||||
|
if (markerInputGroup.style.display === "inline-block") {
|
||||||
|
markerSelectGroup.style.display = "inline-block";
|
||||||
|
markerInputGroup.style.display = "none";
|
||||||
|
} else {
|
||||||
|
markerSelectGroup.style.display = "none";
|
||||||
|
markerInputGroup.style.display = "inline-block";
|
||||||
|
markerInputGroup.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeGroup() {
|
||||||
|
elSelected.attr("xlink:href", "#"+this.value);
|
||||||
|
elSelected.attr("data-id", "#"+this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGroup() {
|
||||||
|
let newGroup = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
|
||||||
|
if (Number.isFinite(+newGroup.charAt(0))) newGroup = "m" + newGroup;
|
||||||
|
if (document.getElementById(newGroup)) {
|
||||||
|
tip("Element with this id already exists. Please provide a unique name", false, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
markerInputGroup.value = "";
|
||||||
|
// clone old group assigning new id
|
||||||
|
const id = elSelected.attr("data-id");
|
||||||
|
const clone = d3.select("#defs-markers").select(id).node().cloneNode(true);
|
||||||
|
clone.id = newGroup;
|
||||||
|
document.getElementById("defs-markers").insertBefore(clone, null);
|
||||||
|
elSelected.attr("xlink:href", "#"+newGroup).attr("data-id", "#"+newGroup);
|
||||||
|
|
||||||
|
// select new group
|
||||||
|
markerSelectGroup.options.add(new Option(newGroup, newGroup, false, true));
|
||||||
|
toggleGroupInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeGroup() {
|
||||||
|
const id = elSelected.attr("data-id");
|
||||||
|
const used = document.querySelectorAll("use[data-id='"+id+"']");
|
||||||
|
const count = used.length === 1 ? "1 element" : used.length + " elements";
|
||||||
|
alertMessage.innerHTML = "Are you sure you want to remove the marker (" + count + ")?";
|
||||||
|
|
||||||
|
$("#alert").dialog({resizable: false, title: "Remove marker",
|
||||||
|
buttons: {
|
||||||
|
Remove: function() {
|
||||||
|
$(this).dialog("close");
|
||||||
|
if (id !== "#marker0") d3.select("#defs-markers").select(id).remove();
|
||||||
|
used.forEach(e => e.remove());
|
||||||
|
updateGroupOptions();
|
||||||
|
updateGroupOptions();
|
||||||
|
$("#markerEditor").dialog("close");
|
||||||
|
},
|
||||||
|
Cancel: function() {$(this).dialog("close");}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleIconSection() {
|
||||||
|
if (markerIconSection.style.display === "inline-block") {
|
||||||
|
markerEditor.querySelectorAll("button:not(#markerIcon)").forEach(b => b.style.display = "inline-block");
|
||||||
|
markerIconSection.style.display = "none";
|
||||||
|
} else {
|
||||||
|
markerEditor.querySelectorAll("button:not(#markerIcon)").forEach(b => b.style.display = "none");
|
||||||
|
markerIconSection.style.display = "inline-block";
|
||||||
|
if (!markerIconTable.innerHTML) drawIconsList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawIconsList() {
|
||||||
|
let icons = [
|
||||||
|
// emoticons in FF:
|
||||||
|
["2693", "⚓", "Anchor"],
|
||||||
|
["26EA", "⛪", "Church"],
|
||||||
|
["1F3EF", "🏯", "Japanese Castle"],
|
||||||
|
["1F3F0", "🏰", "Castle"],
|
||||||
|
["1F5FC", "🗼", "Tower"],
|
||||||
|
["1F3E0", "🏠", "House"],
|
||||||
|
["1F3AA", "🎪", "Tent"],
|
||||||
|
["1F3E8", "🏨", "Hotel"],
|
||||||
|
["1F4B0", "💰", "Money bag"],
|
||||||
|
["1F4A8", "💨", "Dashing away"],
|
||||||
|
["1F334", "🌴", "Palm"],
|
||||||
|
["1F335", "🌵", "Cactus"],
|
||||||
|
["1F33E", "🌾", "Sheaf"],
|
||||||
|
["1F5FB", "🗻", "Mountain"],
|
||||||
|
["1F30B", "🌋", "Volcano"],
|
||||||
|
["1F40E", "🐎", "Horse"],
|
||||||
|
["1F434", "🐴", "Horse Face"],
|
||||||
|
["1F42E", "🐮", "Cow"],
|
||||||
|
["1F43A", "🐺", "Wolf Face"],
|
||||||
|
["1F435", "🐵", "Monkey face"],
|
||||||
|
["1F437", "🐷", "Pig face"],
|
||||||
|
["1F414", "🐔", "Chiken"],
|
||||||
|
["1F411", "🐑", "Eve"],
|
||||||
|
["1F42B", "🐫", "Camel"],
|
||||||
|
["1F418", "🐘", "Elephant"],
|
||||||
|
["1F422", "🐢", "Turtle"],
|
||||||
|
["1F40C", "🐌", "Snail"],
|
||||||
|
["1F40D", "🐍", "Snake"],
|
||||||
|
["1F433", "🐳", "Whale"],
|
||||||
|
["1F42C", "🐬", "Dolphin"],
|
||||||
|
["1F420", "🐟", "Fish"],
|
||||||
|
["1F432", "🐲", "Dragon Head"],
|
||||||
|
["1F479", "👹", "Ogre"],
|
||||||
|
["1F47B", "👻", "Ghost"],
|
||||||
|
["1F47E", "👾", "Alien"],
|
||||||
|
["1F480", "💀", "Skull"],
|
||||||
|
["1F374", "🍴", "Fork and knife"],
|
||||||
|
["1F372", "🍲", "Food"],
|
||||||
|
["1F35E", "🍞", "Bread"],
|
||||||
|
["1F357", "🍗", "Poultry leg"],
|
||||||
|
["1F347", "🍇", "Grapes"],
|
||||||
|
["1F34F", "🍏", "Apple"],
|
||||||
|
["1F352", "🍒", "Cherries"],
|
||||||
|
["1F36F", "🍯", "Honey pot"],
|
||||||
|
["1F37A", "🍺", "Beer"],
|
||||||
|
["1F377", "🍷", "Wine glass"],
|
||||||
|
["1F3BB", "🎻", "Violin"],
|
||||||
|
["1F3B8", "🎸", "Guitar"],
|
||||||
|
["26A1", "⚡", "Electricity"],
|
||||||
|
["1F320", "🌠", "Shooting star"],
|
||||||
|
["1F319", "🌙", "Crescent moon"],
|
||||||
|
["1F525", "🔥", "Fire"],
|
||||||
|
["1F4A7", "💧", "Droplet"],
|
||||||
|
["1F30A", "🌊", "Wave"],
|
||||||
|
["231B", "⌛", "Hourglass"],
|
||||||
|
["1F3C6", "🏆", "Goblet"],
|
||||||
|
["26F2", "⛲", "Fountain"],
|
||||||
|
["26F5", "⛵", "Sailboat"],
|
||||||
|
["26FA", "⛺", "Tend"],
|
||||||
|
["1F489", "💉", "Syringe"],
|
||||||
|
["1F4D6", "📚", "Books"],
|
||||||
|
["1F3AF", "🎯", "Archery"],
|
||||||
|
["1F52E", "🔮", "Magic ball"],
|
||||||
|
["1F3AD", "🎭", "Performing arts"],
|
||||||
|
["1F3A8", "🎨", "Artist palette"],
|
||||||
|
["1F457", "👗", "Dress"],
|
||||||
|
["1F451", "👑", "Crown"],
|
||||||
|
["1F48D", "💍", "Ring"],
|
||||||
|
["1F48E", "💎", "Gem"],
|
||||||
|
["1F514", "🔔", "Bell"],
|
||||||
|
["1F3B2", "🎲", "Die"],
|
||||||
|
// black and white icons in FF:
|
||||||
|
["26A0", "⚠", "Alert"],
|
||||||
|
["2317", "⌗", "Hash"],
|
||||||
|
["2318", "⌘", "POI"],
|
||||||
|
["2307", "⌇", "Wavy"],
|
||||||
|
["21E6", "⇦", "Left arrow"],
|
||||||
|
["21E7", "⇧", "Top arrow"],
|
||||||
|
["21E8", "⇨", "Right arrow"],
|
||||||
|
["21E9", "⇩", "Left arrow"],
|
||||||
|
["21F6", "⇶", "Three arrows"],
|
||||||
|
["2699", "⚙", "Gear"],
|
||||||
|
["269B", "⚛", "Atom"],
|
||||||
|
["0024", "$", "Dollar"],
|
||||||
|
["2680", "⚀", "Die1"],
|
||||||
|
["2681", "⚁", "Die2"],
|
||||||
|
["2682", "⚂", "Die3"],
|
||||||
|
["2683", "⚃", "Die4"],
|
||||||
|
["2684", "⚄", "Die5"],
|
||||||
|
["2685", "⚅", "Die6"],
|
||||||
|
["26B4", "⚴", "Pallas"],
|
||||||
|
["26B5", "⚵", "Juno"],
|
||||||
|
["26B6", "⚶", "Vesta"],
|
||||||
|
["26B7", "⚷", "Chiron"],
|
||||||
|
["26B8", "⚸", "Lilith"],
|
||||||
|
["263F", "☿", "Mercury"],
|
||||||
|
["2640", "♀", "Venus"],
|
||||||
|
["2641", "♁", "Earth"],
|
||||||
|
["2642", "♂", "Mars"],
|
||||||
|
["2643", "♃", "Jupiter"],
|
||||||
|
["2644", "♄", "Saturn"],
|
||||||
|
["2645", "♅", "Uranus"],
|
||||||
|
["2646", "♆", "Neptune"],
|
||||||
|
["2647", "♇", "Pluto"],
|
||||||
|
["26B3", "⚳", "Ceres"],
|
||||||
|
["2654", "♔", "Chess king"],
|
||||||
|
["2655", "♕", "Chess queen"],
|
||||||
|
["2656", "♖", "Chess rook"],
|
||||||
|
["2657", "♗", "Chess bishop"],
|
||||||
|
["2658", "♘", "Chess knight"],
|
||||||
|
["2659", "♙", "Chess pawn"],
|
||||||
|
["2660", "♠", "Spade"],
|
||||||
|
["2663", "♣", "Club"],
|
||||||
|
["2665", "♥", "Heart"],
|
||||||
|
["2666", "♦", "Diamond"],
|
||||||
|
["2698", "⚘", "Flower"],
|
||||||
|
["2625", "☥", "Ankh"],
|
||||||
|
["2626", "☦", "Orthodox"],
|
||||||
|
["2627", "☧", "Chi Rho"],
|
||||||
|
["2628", "☨", "Lorraine"],
|
||||||
|
["2629", "☩", "Jerusalem"],
|
||||||
|
["2670", "♰", "Syriac cross"],
|
||||||
|
["2020", "†", "Dagger"],
|
||||||
|
["262A", "☪", "Muslim"],
|
||||||
|
["262D", "☭", "Soviet"],
|
||||||
|
["262E", "☮", "Peace"],
|
||||||
|
["262F", "☯", "Yin yang"],
|
||||||
|
["26A4", "⚤", "Heterosexuality"],
|
||||||
|
["26A2", "⚢", "Female homosexuality"],
|
||||||
|
["26A3", "⚣", "Male homosexuality"],
|
||||||
|
["26A5", "⚥", "Male and female"],
|
||||||
|
["26AD", "⚭", "Rings"],
|
||||||
|
["2690", "⚐", "White flag"],
|
||||||
|
["2691", "⚑", "Black flag"],
|
||||||
|
["263C", "☼", "Sun"],
|
||||||
|
["263E", "☾", "Moon"],
|
||||||
|
["2668", "♨", "Hot springs"],
|
||||||
|
["2600", "☀", "Black sun"],
|
||||||
|
["2601", "☁", "Cloud"],
|
||||||
|
["2602", "☂", "Umbrella"],
|
||||||
|
["2603", "☃", "Snowman"],
|
||||||
|
["2604", "☄", "Comet"],
|
||||||
|
["2605", "★", "Black star"],
|
||||||
|
["2606", "☆", "White star"],
|
||||||
|
["269D", "⚝", "Outlined star"],
|
||||||
|
["2618", "☘", "Shamrock"],
|
||||||
|
["21AF", "↯", "Lightning"],
|
||||||
|
["269C", "⚜", "FleurDeLis"],
|
||||||
|
["2622", "☢", "Radiation"],
|
||||||
|
["2623", "☣", "Biohazard"],
|
||||||
|
["2620", "☠", "Skull"],
|
||||||
|
["2638", "☸", "Dharma"],
|
||||||
|
["2624", "☤", "Caduceus"],
|
||||||
|
["2695", "⚕", "Aeculapius staff"],
|
||||||
|
["269A", "⚚", "Hermes staff"],
|
||||||
|
["2697", "⚗", "Alembic"],
|
||||||
|
["266B", "♫", "Music"],
|
||||||
|
["2702", "✂", "Scissors"],
|
||||||
|
["2696", "⚖", "Scales"],
|
||||||
|
["2692", "⚒", "Hammer and pick"],
|
||||||
|
["2694", "⚔", "Swords"]
|
||||||
|
];
|
||||||
|
|
||||||
|
const table = document.getElementById("markerIconTable");
|
||||||
|
table.addEventListener("click", selectIcon, false);
|
||||||
|
table.addEventListener("mouseover", hoverIcon, false);
|
||||||
|
let row = "";
|
||||||
|
|
||||||
|
for (let i=0; i < icons.length; i++) {
|
||||||
|
if (i%16 === 0) row = table.insertRow(0);
|
||||||
|
const cell = row.insertCell(0);
|
||||||
|
const icon = String.fromCodePoint(parseInt(icons[i][0], 16));
|
||||||
|
cell.innerHTML = icon;
|
||||||
|
cell.id = "markerIcon" + icon.codePointAt();
|
||||||
|
cell.dataset.desc = icons[i][2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectIcon(e) {
|
||||||
|
if (e.target !== e.currentTarget) {
|
||||||
|
const table = document.getElementById("markerIconTable");
|
||||||
|
const selected = table.getElementsByClassName("selected");
|
||||||
|
if (selected.length) selected[0].removeAttribute("class");
|
||||||
|
e.target.className = "selected";
|
||||||
|
const id = elSelected.attr("data-id");
|
||||||
|
const icon = e.target.innerHTML;
|
||||||
|
d3.select("#defs-markers").select(id).select("text").text(icon);
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hoverIcon(e) {
|
||||||
|
if (e.target !== e.currentTarget) tip(e.target.innerHTML + " " + e.target.dataset.desc);
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeIconSize() {
|
||||||
|
const id = elSelected.attr("data-id");
|
||||||
|
d3.select("#defs-markers").select(id).select("text").attr("font-size", this.value + "px");
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeIconShiftX() {
|
||||||
|
const id = elSelected.attr("data-id");
|
||||||
|
d3.select("#defs-markers").select(id).select("text").attr("x", this.value + "%");
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeIconShiftY() {
|
||||||
|
const id = elSelected.attr("data-id");
|
||||||
|
d3.select("#defs-markers").select(id).select("text").attr("y", this.value + "%");
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCustomUnicodeIcon() {
|
||||||
|
if (!this.value) return;
|
||||||
|
const id = elSelected.attr("data-id");
|
||||||
|
d3.select("#defs-markers").select(id).select("text").text(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleStyleSection() {
|
||||||
|
if (markerStyleSection.style.display === "inline-block") {
|
||||||
|
markerEditor.querySelectorAll("button:not(#markerStyle)").forEach(b => b.style.display = "inline-block");
|
||||||
|
markerStyleSection.style.display = "none";
|
||||||
|
} else {
|
||||||
|
markerEditor.querySelectorAll("button:not(#markerStyle)").forEach(b => b.style.display = "none");
|
||||||
|
markerStyleSection.style.display = "inline-block";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeMarkerSize() {
|
||||||
|
const id = elSelected.attr("data-id");
|
||||||
|
document.querySelectorAll("use[data-id='"+id+"']").forEach(e => e.dataset.size = markerSize.value);
|
||||||
|
invokeActiveZooming();
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePinStroke() {
|
||||||
|
const id = elSelected.attr("data-id");
|
||||||
|
d3.select(id).select("path").attr("fill", this.value);
|
||||||
|
d3.select(id).select("circle").attr("stroke", this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePinFill() {
|
||||||
|
const id = elSelected.attr("data-id");
|
||||||
|
d3.select(id).select("circle").attr("fill", this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeIconStrokeWidth() {
|
||||||
|
const id = elSelected.attr("data-id");
|
||||||
|
d3.select("#defs-markers").select(id).select("text").attr("stroke-width", this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeIconStroke() {
|
||||||
|
const id = elSelected.attr("data-id");
|
||||||
|
d3.select("#defs-markers").select(id).select("text").attr("stroke", this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeIconFill() {
|
||||||
|
const id = elSelected.attr("data-id");
|
||||||
|
d3.select("#defs-markers").select(id).select("text").attr("fill", this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePinVisibility() {
|
||||||
|
const id = elSelected.attr("data-id");
|
||||||
|
let show = 1;
|
||||||
|
if (this.className === "icon-info-circled") {this.className = "icon-info"; show = 0; }
|
||||||
|
else this.className = "icon-info-circled";
|
||||||
|
d3.select(id).select("circle").attr("opacity", show);
|
||||||
|
d3.select(id).select("path").attr("opacity", show);
|
||||||
|
}
|
||||||
|
|
||||||
|
function editMarkerLegend() {
|
||||||
|
const id = elSelected.attr("id");
|
||||||
|
editLegends(id, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAddMarker() {
|
||||||
|
document.getElementById("addMarker").click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeMarker() {
|
||||||
|
alertMessage.innerHTML = "Are you sure you want to remove the marker?";
|
||||||
|
$("#alert").dialog({resizable: false, title: "Remove marker",
|
||||||
|
buttons: {
|
||||||
|
Remove: function() {
|
||||||
|
$(this).dialog("close");
|
||||||
|
elSelected.remove();
|
||||||
|
$("#markerEditor").dialog("close");
|
||||||
|
},
|
||||||
|
Cancel: function() {$(this).dialog("close");}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMarkerEditor() {
|
||||||
|
unselect();
|
||||||
|
if (addMarker.classList.contains("pressed")) addMarker.classList.remove("pressed");
|
||||||
|
if (markerAdd.classList.contains("pressed")) markerAdd.classList.remove("pressed");
|
||||||
|
restoreDefaultEvents();
|
||||||
|
clearMainTip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
277
modules/ui/measurers.js
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
// UI measurers: rulers (linear, curve, area) and Scale Bar
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Linear measurer (one is added by default)
|
||||||
|
function addRuler(x1, y1, x2, y2) {
|
||||||
|
const cx = rn((x1 + x2) / 2, 2), cy = rn((y1 + y2) / 2, 2);
|
||||||
|
const size = rn(1 / scale ** .3 * 2, 1);
|
||||||
|
const dash = rn(30 / distanceScale.value, 2);
|
||||||
|
|
||||||
|
// body
|
||||||
|
const rulerNew = ruler.append("g").call(d3.drag().on("start", dragRuler));
|
||||||
|
rulerNew.append("line").attr("x1", x1).attr("y1", y1).attr("x2", x2).attr("y2", y2).attr("class", "white").attr("stroke-width", size);
|
||||||
|
rulerNew.append("line").attr("x1", x1).attr("y1", y1).attr("x2", x2).attr("y2", y2).attr("class", "gray").attr("stroke-width", size).attr("stroke-dasharray", dash);
|
||||||
|
rulerNew.append("circle").attr("r", 2 * size).attr("stroke-width", .5 * size).attr("cx", x1).attr("cy", y1).attr("data-edge", "left").call(d3.drag().on("drag", dragRulerEdge));
|
||||||
|
rulerNew.append("circle").attr("r", 2 * size).attr("stroke-width", .5 * size).attr("cx", x2).attr("cy", y2).attr("data-edge", "right").call(d3.drag().on("drag", dragRulerEdge));
|
||||||
|
|
||||||
|
// label and center
|
||||||
|
const angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
|
||||||
|
const rotate = `rotate(${angle} ${cx} ${cy})`;
|
||||||
|
const dist = rn(Math.hypot(x1 - x2, y1 - y2));
|
||||||
|
const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
|
||||||
|
rulerNew.append("rect").attr("x", cx - size * 1.5).attr("y", cy - size * 1.5).attr("width", size * 3).attr("height", size * 3).attr("transform", rotate).attr("stroke-width", .5 * size).call(d3.drag().on("start", rulerCenterDrag));
|
||||||
|
rulerNew.append("text").attr("x", cx).attr("y", cy).attr("dx", ".3em").attr("dy", "-.3em").attr("transform", rotate).attr("font-size", 10 * size).text(label).on("click", removeParent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragRuler() {
|
||||||
|
const tr = parseTransform(this.getAttribute("transform"));
|
||||||
|
const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y;
|
||||||
|
|
||||||
|
d3.event.on("drag", function() {
|
||||||
|
const transform = `translate(${(x + d3.event.x)},${(y + d3.event.y)})`;
|
||||||
|
this.setAttribute("transform", transform);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragRulerEdge() {
|
||||||
|
const ruler = d3.select(this.parentNode);
|
||||||
|
const x = d3.event.x, y = d3.event.y;
|
||||||
|
|
||||||
|
d3.select(this).attr("cx", x).attr("cy", y);
|
||||||
|
const line = ruler.selectAll("line");
|
||||||
|
const left = this.dataset.edge === "left";
|
||||||
|
const x0 = left ? +line.attr("x2") : +line.attr("x1");
|
||||||
|
const y0 = left ? +line.attr("y2") : +line.attr("y1");
|
||||||
|
if (left) line.attr("x1", x).attr("y1", y); else line.attr("x2", x).attr("y2", y);
|
||||||
|
|
||||||
|
const cx = rn((x + x0) / 2, 2), cy = rn((y + y0) / 2, 2);
|
||||||
|
const dist = Math.hypot(x0 - x, y0 - y);
|
||||||
|
const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
|
||||||
|
const atan = x0 > x ? Math.atan2(y0 - y, x0 - x) : Math.atan2(y - y0, x - x0);
|
||||||
|
const angle = rn(atan * 180 / Math.PI, 3);
|
||||||
|
const rotate = `rotate(${angle} ${cx} ${cy})`;
|
||||||
|
|
||||||
|
const size = rn(1 / scale ** .3 * 2, 1);
|
||||||
|
ruler.select("rect").attr("x", cx - size * 1.5).attr("y", cy - size * 1.5).attr("transform", rotate);
|
||||||
|
ruler.select("text").attr("x", cx).attr("y", cy).attr("transform", rotate).text(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rulerCenterDrag() {
|
||||||
|
let xc1, yc1, xc2, yc2, r1, r2;
|
||||||
|
const rulerOld = d3.select(this.parentNode); // current ruler
|
||||||
|
let x = d3.event.x, y = d3.event.y; // current coords
|
||||||
|
const line = rulerOld.selectAll("line"); // current lines
|
||||||
|
const x1 = +line.attr("x1"), y1 = +line.attr("y1"), x2 = +line.attr("x2"), y2 = +line.attr("y2"); // initial line edge points
|
||||||
|
const size = rn(1 / scale ** .3 * 2, 1);
|
||||||
|
const dash = +rulerOld.select(".gray").attr("stroke-dasharray");
|
||||||
|
|
||||||
|
const rulerNew = ruler.insert("g", ":first-child");
|
||||||
|
rulerNew.attr("transform", rulerOld.attr("transform")).call(d3.drag().on("start", dragRuler));
|
||||||
|
rulerNew.append("line").attr("class", "white").attr("stroke-width", size);
|
||||||
|
rulerNew.append("line").attr("class", "gray").attr("stroke-dasharray", dash).attr("stroke-width", size);
|
||||||
|
rulerNew.append("text").attr("dx", ".3em").attr("dy", "-.3em").on("click", removeParent).attr("font-size", 10 * size).attr("stroke-width", size);
|
||||||
|
|
||||||
|
d3.event.on("drag", function() {
|
||||||
|
x = d3.event.x, y = d3.event.y;
|
||||||
|
|
||||||
|
// change first part
|
||||||
|
let dist = rn(Math.hypot(x1 - x, y1 - y));
|
||||||
|
let label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
|
||||||
|
let atan = x1 > x ? Math.atan2(y1 - y, x1 - x) : Math.atan2(y - y1, x - x1);
|
||||||
|
xc1 = rn((x + x1) / 2, 2), yc1 = rn((y + y1) / 2, 2);
|
||||||
|
r1 = `rotate(${rn(atan * 180 / Math.PI, 3)} ${xc1} ${yc1})`;
|
||||||
|
line.attr("x1", x1).attr("y1", y1).attr("x2", x).attr("y2", y);
|
||||||
|
rulerOld.select("rect").attr("x", x - size * 1.5).attr("y", y - size * 1.5).attr("transform", null);
|
||||||
|
rulerOld.select("text").attr("x", xc1).attr("y", yc1).attr("transform", r1).text(label);
|
||||||
|
|
||||||
|
// change second (new) part
|
||||||
|
dist = rn(Math.hypot(x2 - x, y2 - y));
|
||||||
|
label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
|
||||||
|
atan = x2 > x ? Math.atan2(y2 - y, x2 - x) : Math.atan2(y - y2, x - x2);
|
||||||
|
xc2 = rn((x + x2) / 2, 2), yc2 = rn((y + y2) / 2, 2);
|
||||||
|
r2 = `rotate(${rn(atan * 180 / Math.PI, 3)} ${xc2} ${yc2})`;
|
||||||
|
rulerNew.selectAll("line").attr("x1", x).attr("y1", y).attr("x2", x2).attr("y2", y2);
|
||||||
|
rulerNew.select("text").attr("x", xc2).attr("y", yc2).attr("transform", r2).text(label);
|
||||||
|
});
|
||||||
|
|
||||||
|
d3.event.on("end", function() {
|
||||||
|
// contols for 1st part
|
||||||
|
rulerOld.select("circle[data-edge='left']").attr("cx", x1).attr("cy", y1);
|
||||||
|
rulerOld.select("circle[data-edge='right']").attr("cx", x).attr("cy", y);
|
||||||
|
rulerOld.select("rect").attr("x", xc1 - size * 1.5).attr("y", yc1 - size * 1.5).attr("transform", r1);
|
||||||
|
|
||||||
|
// contols for 2nd part
|
||||||
|
rulerNew.append("circle").attr("cx", x).attr("cy", y).attr("r", 2 * size).attr("stroke-width", 0.5 * size).attr("data-edge", "left").call(d3.drag().on("drag", dragRulerEdge));
|
||||||
|
rulerNew.append("circle").attr("cx", x2).attr("cy", y2).attr("r", 2 * size).attr("stroke-width", 0.5 * size).attr("data-edge", "right").call(d3.drag().on("drag", dragRulerEdge));
|
||||||
|
rulerNew.append("rect").attr("x", xc2 - size * 1.5).attr("y", yc2 - size * 1.5).attr("width", size * 3).attr("height", size * 3).attr("transform", r2).attr("stroke-width", .5 * size).call(d3.drag().on("start", rulerCenterDrag));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawOpisometer() {
|
||||||
|
lineGen.curve(d3.curveBasis);
|
||||||
|
const size = rn(1 / scale ** .3 * 2, 1);
|
||||||
|
const dash = rn(30 / distanceScale.value, 2);
|
||||||
|
const p0 = d3.mouse(this);
|
||||||
|
const points = [[p0[0], p0[1]]];
|
||||||
|
let length = 0;
|
||||||
|
|
||||||
|
const rulerNew = ruler.append("g").call(d3.drag().on("start", dragRuler));
|
||||||
|
const curve = rulerNew.append("path").attr("class", "white").attr("stroke-width", size);
|
||||||
|
const curveGray = rulerNew.append("path").attr("class", "gray").attr("stroke-width", size).attr("stroke-dasharray", dash);
|
||||||
|
const text = rulerNew.append("text").attr("dy", "-.3em").attr("font-size", 10 * size).on("click", removeParent);
|
||||||
|
const start = rulerNew.append("circle").attr("r", 2 * size).attr("stroke-width", .5 * size).attr("data-edge", "start").call(d3.drag().on("start", dragOpisometerEnd));
|
||||||
|
const end = rulerNew.append("circle").attr("r", 2 * size).attr("stroke-width", .5 * size).attr("data-edge", "end").call(d3.drag().on("start", dragOpisometerEnd));
|
||||||
|
|
||||||
|
d3.event.on("drag", function() {
|
||||||
|
const p = d3.mouse(this);
|
||||||
|
const diff = Math.hypot(last(points)[0] - p[0], last(points)[1] - p[1]);
|
||||||
|
if (diff > 3) points.push([p[0], p[1]]); else return;
|
||||||
|
|
||||||
|
const path = round(lineGen(points));
|
||||||
|
curve.attr("d", path);
|
||||||
|
curveGray.attr("d", path);
|
||||||
|
length = curve.node().getTotalLength();
|
||||||
|
const label = rn(length * distanceScale.value) + " " + distanceUnit.value;
|
||||||
|
text.attr("x", p[0]).attr("y", p[1]).text(label);
|
||||||
|
});
|
||||||
|
|
||||||
|
d3.event.on("end", function() {
|
||||||
|
restoreDefaultEvents();
|
||||||
|
clearMainTip();
|
||||||
|
addOpisometer.classList.remove("pressed");
|
||||||
|
|
||||||
|
const c = curve.node().getPointAtLength(length / 2);
|
||||||
|
const p = curve.node().getPointAtLength(length / 2 - 1);
|
||||||
|
const atan = p.x > c.x ? Math.atan2(p.y - c.y, p.x - c.x) : Math.atan2(c.y - p.y, c.x - p.x);
|
||||||
|
const angle = rn(atan * 180 / Math.PI, 3);
|
||||||
|
const rotate = `rotate(${angle} ${c.x} ${c.y})`;
|
||||||
|
|
||||||
|
rulerNew.attr("data-points", JSON.stringify(points));
|
||||||
|
text.attr("x", c.x).attr("y", c.y).attr("transform", rotate);
|
||||||
|
start.attr("cx", points[0][0]).attr("cy", points[0][1]);
|
||||||
|
end.attr("cx", last(points)[0]).attr("cy", last(points)[1]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragOpisometerEnd() {
|
||||||
|
const ruler = d3.select(this.parentNode);
|
||||||
|
const curve = ruler.select(".white");
|
||||||
|
const curveGray = ruler.select(".gray");
|
||||||
|
const text = ruler.select("text");
|
||||||
|
|
||||||
|
const points = JSON.parse(ruler.attr("data-points"));
|
||||||
|
const x0 = +this.getAttribute("cx"), y0 = +this.getAttribute("cy");
|
||||||
|
if (x0 === points[0][0] && y0 === points[0][1]) points.reverse();
|
||||||
|
lineGen.curve(d3.curveBasis);
|
||||||
|
let length = 0;
|
||||||
|
|
||||||
|
d3.event.on("drag", function() {
|
||||||
|
const p = d3.mouse(this);
|
||||||
|
d3.select(this).attr("cx", p[0]).attr("cy", p[1]);
|
||||||
|
|
||||||
|
const diff = Math.hypot(last(points)[0] - p[0], last(points)[1] - p[1]);
|
||||||
|
if (diff > 3) points.push([p[0], p[1]]); else return;
|
||||||
|
|
||||||
|
const path = round(lineGen(points));
|
||||||
|
curve.attr("d", path);
|
||||||
|
curveGray.attr("d", path);
|
||||||
|
length = curve.node().getTotalLength();
|
||||||
|
const label = rn(length * distanceScale.value) + " " + distanceUnit.value;
|
||||||
|
text.text(label);
|
||||||
|
});
|
||||||
|
|
||||||
|
d3.event.on("end", function() {
|
||||||
|
const c = curve.node().getPointAtLength(length / 2);
|
||||||
|
const p = curve.node().getPointAtLength(length / 2 - 1);
|
||||||
|
const atan = p.x > c.x ? Math.atan2(p.y - c.y, p.x - c.x) : Math.atan2(c.y - p.y, c.x - p.x);
|
||||||
|
const angle = rn(atan * 180 / Math.PI, 3);
|
||||||
|
const rotate = `rotate(${angle} ${c.x} ${c.y})`;
|
||||||
|
|
||||||
|
ruler.attr("data-points", JSON.stringify(points));
|
||||||
|
text.attr("x", c.x).attr("y", c.y).attr("transform", rotate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawPlanimeter() {
|
||||||
|
lineGen.curve(d3.curveBasisClosed);
|
||||||
|
const size = rn(1 / scale ** .3 * 2, 1);
|
||||||
|
const p0 = d3.mouse(this);
|
||||||
|
const points = [[p0[0], p0[1]]];
|
||||||
|
|
||||||
|
const rulerNew = ruler.append("g").call(d3.drag().on("start", dragRuler));
|
||||||
|
const curve = rulerNew.append("path").attr("class", "planimeter").attr("stroke-width", size);
|
||||||
|
const text = rulerNew.append("text").attr("font-size", 10 * size).on("click", removeParent);
|
||||||
|
|
||||||
|
d3.event.on("drag", function() {
|
||||||
|
const p = d3.mouse(this);
|
||||||
|
const diff = Math.hypot(last(points)[0] - p[0], last(points)[1] - p[1]);
|
||||||
|
if (diff > 5) points.push([p[0], p[1]]); else return;
|
||||||
|
curve.attr("d", round(lineGen(points)));
|
||||||
|
});
|
||||||
|
|
||||||
|
d3.event.on("end", function() {
|
||||||
|
restoreDefaultEvents();
|
||||||
|
clearMainTip();
|
||||||
|
addPlanimeter.classList.remove("pressed");
|
||||||
|
|
||||||
|
const polygonArea = rn(Math.abs(d3.polygonArea(points)));
|
||||||
|
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
|
||||||
|
const area = si(polygonArea * distanceScale.value ** 2) + " " + unit;
|
||||||
|
const c = polylabel([points], 1.0); // pole of inaccessibility
|
||||||
|
text.attr("x", c[0]).attr("y", c[1]).text(area);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw default scale bar
|
||||||
|
function drawScaleBar() {
|
||||||
|
if (scaleBar.style("display") === "none") return; // no need to re-draw hidden element
|
||||||
|
scaleBar.selectAll("*").remove(); // fully redraw every time
|
||||||
|
|
||||||
|
const dScale = distanceScale.value;
|
||||||
|
const unit = distanceUnit.value;
|
||||||
|
|
||||||
|
// calculate size
|
||||||
|
const init = 100; // actual length in pixels if scale, dScale and size = 1;
|
||||||
|
const size = +barSize.value;
|
||||||
|
let val = init * size * dScale / scale; // bar length in distance unit
|
||||||
|
if (val > 900) val = rn(val, -3); // round to 1000
|
||||||
|
else if (val > 90) val = rn(val, -2); // round to 100
|
||||||
|
else if (val > 9) val = rn(val, -1); // round to 10
|
||||||
|
else val = rn(val) // round to 1
|
||||||
|
const l = val * scale / dScale; // actual length in pixels on this scale
|
||||||
|
|
||||||
|
scaleBar.append("line").attr("x1", 0.5).attr("y1", 0).attr("x2", l+size-0.5).attr("y2", 0).attr("stroke-width", size).attr("stroke", "white");
|
||||||
|
scaleBar.append("line").attr("x1", 0).attr("y1", size).attr("x2", l+size).attr("y2", size).attr("stroke-width", size).attr("stroke", "#3d3d3d");
|
||||||
|
const dash = size + " " + rn(l / 5 - size, 2);
|
||||||
|
scaleBar.append("line").attr("x1", 0).attr("y1", 0).attr("x2", l+size).attr("y2", 0)
|
||||||
|
.attr("stroke-width", rn(size * 3, 2)).attr("stroke-dasharray", dash).attr("stroke", "#3d3d3d");
|
||||||
|
|
||||||
|
const fontSize = rn(5 * size, 1);
|
||||||
|
scaleBar.selectAll("text").data(d3.range(0,6)).enter().append("text")
|
||||||
|
.attr("x", d => rn(d * l/5, 2)).attr("y", 0).attr("dy", "-.5em")
|
||||||
|
.attr("font-size", fontSize).text(d => rn(d * l/5 * dScale / scale) + (d<5 ? "" : " " + unit));
|
||||||
|
|
||||||
|
if (barLabel.value !== "") {
|
||||||
|
scaleBar.append("text").attr("x", (l+1) / 2).attr("y", 2 * size)
|
||||||
|
.attr("dominant-baseline", "text-before-edge")
|
||||||
|
.attr("font-size", fontSize).text(barLabel.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bbox = scaleBar.node().getBBox();
|
||||||
|
// append backbround rectangle
|
||||||
|
scaleBar.insert("rect", ":first-child").attr("x", -10).attr("y", -20).attr("width", bbox.width + 10).attr("height", bbox.height + 15)
|
||||||
|
.attr("stroke-width", size).attr("stroke", "none").attr("filter", "url(#blur5)")
|
||||||
|
.attr("fill", barBackColor.value).attr("opacity", +barBackOpacity.value);
|
||||||
|
|
||||||
|
fitScaleBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
// fit ScaleBar to map size
|
||||||
|
function fitScaleBar() {
|
||||||
|
if (!scaleBar.select("rect").size()) return;
|
||||||
|
const px = isNaN(+barPosX.value) ? 100 : barPosX.value / 100;
|
||||||
|
const py = isNaN(+barPosY.value) ? 100 : barPosY.value / 100;
|
||||||
|
const bbox = scaleBar.select("rect").node().getBBox();
|
||||||
|
const x = rn(svgWidth * px - bbox.width + 10), y = rn(svgHeight * py - bbox.height + 20);
|
||||||
|
scaleBar.attr("transform", `translate(${x},${y})`);
|
||||||
|
}
|
||||||
177
modules/ui/namesbase-editor.js
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
"use strict";
|
||||||
|
function editNamesbase() {
|
||||||
|
if (customization) return;
|
||||||
|
closeDialogs("#namesbaseEditor, .stable");
|
||||||
|
$("#namesbaseEditor").dialog();
|
||||||
|
|
||||||
|
if (modules.editNamesbase) return;
|
||||||
|
modules.editNamesbase = true;
|
||||||
|
|
||||||
|
// add listeners
|
||||||
|
document.getElementById("namesbaseSelect").addEventListener("change", updateInputs);
|
||||||
|
document.getElementById("namesbaseTextarea").addEventListener("change", updateNamesData);
|
||||||
|
document.getElementById("namesbaseUpdateExamples").addEventListener("click", updateExamples);
|
||||||
|
document.getElementById("namesbaseExamples").addEventListener("click", updateExamples);
|
||||||
|
document.getElementById("namesbaseName").addEventListener("input", updateBaseName);
|
||||||
|
document.getElementById("namesbaseMin").addEventListener("input", updateBaseMin);
|
||||||
|
document.getElementById("namesbaseMax").addEventListener("input", updateBaseMax);
|
||||||
|
document.getElementById("namesbaseDouble").addEventListener("input", updateBaseDublication);
|
||||||
|
document.getElementById("namesbaseMulti").addEventListener("input", updateBaseMiltiwordRate);
|
||||||
|
document.getElementById("namesbaseAdd").addEventListener("click", namesbaseAdd);
|
||||||
|
document.getElementById("namesbaseDefault").addEventListener("click", namesbaseRestoreDefault);
|
||||||
|
document.getElementById("namesbaseDownload").addEventListener("click", namesbaseDownload);
|
||||||
|
document.getElementById("namesbaseUpload").addEventListener("click", e => namesbaseToLoad.click());
|
||||||
|
document.getElementById("namesbaseToLoad").addEventListener("change", namesbaseUpload);
|
||||||
|
|
||||||
|
createBasesList();
|
||||||
|
updateInputs();
|
||||||
|
|
||||||
|
$("#namesbaseEditor").dialog({
|
||||||
|
title: "Namesbase Editor", width: 468,
|
||||||
|
position: {my: "center", at: "center", of: "svg"}
|
||||||
|
});
|
||||||
|
|
||||||
|
function createBasesList() {
|
||||||
|
const select = document.getElementById("namesbaseSelect");
|
||||||
|
select.innerHTML = "";
|
||||||
|
nameBases.forEach(function(b, i) {
|
||||||
|
const option = new Option(b.name, i);
|
||||||
|
select.options.add(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateInputs() {
|
||||||
|
const base = +document.getElementById("namesbaseSelect").value;
|
||||||
|
if (!nameBases[base]) {tip(`Namesbase ${base} is not defined`, false, "error"); return;}
|
||||||
|
document.getElementById("namesbaseTextarea").value = nameBase[base].join(", ");
|
||||||
|
document.getElementById("namesbaseName").value = nameBases[base].name;
|
||||||
|
document.getElementById("namesbaseMin").value = nameBases[base].min;
|
||||||
|
document.getElementById("namesbaseMax").value = nameBases[base].max;
|
||||||
|
document.getElementById("namesbaseDouble").value = nameBases[base].d;
|
||||||
|
document.getElementById("namesbaseMulti").value = nameBases[base].m;
|
||||||
|
updateExamples();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateExamples() {
|
||||||
|
const base = +document.getElementById("namesbaseSelect").value;
|
||||||
|
let examples = "";
|
||||||
|
for (let i=0; i < 10; i++) {
|
||||||
|
const example = Names.getBase(base);
|
||||||
|
if (example === undefined) {
|
||||||
|
examples = "Cannot generate examples. Please verify the data";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (i) examples += ", ";
|
||||||
|
examples += example;
|
||||||
|
}
|
||||||
|
document.getElementById("namesbaseExamples").innerHTML = examples;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNamesData() {
|
||||||
|
const base = +document.getElementById("namesbaseSelect").value;
|
||||||
|
const data = document.getElementById("namesbaseTextarea").value.replace(/ /g, "").split(",");
|
||||||
|
if (data.length < 3) {
|
||||||
|
tip("The names data provided is not correct", false, "error");
|
||||||
|
document.getElementById("namesbaseTextarea").value = nameBase[base].join(", ");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nameBase[base] = data;
|
||||||
|
Names.updateChain(base);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBaseName() {
|
||||||
|
const base = +document.getElementById("namesbaseSelect").value;
|
||||||
|
const select = document.getElementById("namesbaseSelect");
|
||||||
|
select.options[namesbaseSelect.selectedIndex].innerHTML = this.value;
|
||||||
|
nameBases[base].name = this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBaseMin() {
|
||||||
|
const base = +document.getElementById("namesbaseSelect").value;
|
||||||
|
if (+this.value > nameBases[base].max) {tip("Minimal length cannot be greater than maximal", false, "error"); return;}
|
||||||
|
nameBases[base].min = +this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBaseMax() {
|
||||||
|
const base = +document.getElementById("namesbaseSelect").value;
|
||||||
|
if (+this.value < nameBases[base].min) {tip("Maximal length should be greater than minimal", false, "error"); return;}
|
||||||
|
nameBases[base].max = +this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBaseDublication() {
|
||||||
|
const base = +document.getElementById("namesbaseSelect").value;
|
||||||
|
nameBases[base].d = this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBaseMiltiwordRate() {
|
||||||
|
if (isNaN(+this.value) || +this.value < 0 || +this.value > 1) {tip("Please provide a number within [0-1] range", false, "error"); return;}
|
||||||
|
const base = +document.getElementById("namesbaseSelect").value;
|
||||||
|
nameBases[base].m = +this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function namesbaseAdd() {
|
||||||
|
const base = nameBases.length;
|
||||||
|
nameBases.push({name: "Base" + base, min: 5, max: 12, d: "", m: 0});
|
||||||
|
nameBase[base] = ["This", "is", "an", "example", "data", "Please", "replace", "with", "an", "actual", "names", "data", "with", "at", "least", "100", "names"];
|
||||||
|
document.getElementById("namesbaseSelect").add(new Option("Base" + base, base));
|
||||||
|
document.getElementById("namesbaseSelect").value = base;
|
||||||
|
document.getElementById("namesbaseTextarea").value = nameBase[base].join(", ");
|
||||||
|
document.getElementById("namesbaseName").value = "Base" + base;
|
||||||
|
document.getElementById("namesbaseMin").value = 5;
|
||||||
|
document.getElementById("namesbaseMax").value = 12;
|
||||||
|
document.getElementById("namesbaseDouble").value = "";
|
||||||
|
document.getElementById("namesbaseMulti").value = 0;
|
||||||
|
document.getElementById("namesbaseExamples").innerHTML = "Please provide names data";
|
||||||
|
}
|
||||||
|
|
||||||
|
function namesbaseRestoreDefault() {
|
||||||
|
alertMessage.innerHTML = `Are you sure you want to restore default namesbase?`;
|
||||||
|
$("#alert").dialog({resizable: false, title: "Restore default data",
|
||||||
|
buttons: {
|
||||||
|
Restore: function() {
|
||||||
|
$(this).dialog("close");
|
||||||
|
applyDefaultNamesData();
|
||||||
|
createBasesList();
|
||||||
|
updateInputs();
|
||||||
|
Names.updateChains();
|
||||||
|
},
|
||||||
|
Cancel: function() {$(this).dialog("close");}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function namesbaseDownload() {
|
||||||
|
const data = nameBases.map((b,i) => `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${nameBase[i]}`);
|
||||||
|
const dataBlob = new Blob([data.join("\r\n")], {type:"text/plain"});
|
||||||
|
const url = window.URL.createObjectURL(dataBlob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.download = "namesbase" + Date.now() + ".txt";
|
||||||
|
link.href = url;
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function namesbaseUpload() {
|
||||||
|
const fileToLoad = this.files[0];
|
||||||
|
this.value = "";
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
|
||||||
|
fileReader.onload = function(fileLoadedEvent) {
|
||||||
|
const dataLoaded = fileLoadedEvent.target.result;
|
||||||
|
const data = dataLoaded.split("\r\n");
|
||||||
|
if (!data || !data[0]) {tip("Cannot load a namesbase. Please check the data format", false, "error"); return;}
|
||||||
|
|
||||||
|
nameBases = [], nameBase = [];
|
||||||
|
data.forEach(d => {
|
||||||
|
const e = d.split("|");
|
||||||
|
nameBases.push({name:e[0], min:e[1], max:e[2], d:e[3], m:d[4]});
|
||||||
|
nameBase.push(e[5].split(","));
|
||||||
|
});
|
||||||
|
|
||||||
|
createBasesList();
|
||||||
|
updateInputs();
|
||||||
|
Names.updateChains();
|
||||||
|
};
|
||||||
|
|
||||||
|
fileReader.readAsText(fileToLoad, "UTF-8");
|
||||||
|
}
|
||||||
|
}
|
||||||
954
modules/ui/options.js
Normal file
|
|
@ -0,0 +1,954 @@
|
||||||
|
// UI module to control the options (style, preferences)
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
$("#optionsContainer").draggable({handle: ".drag-trigger", snap: "svg", snapMode: "both"});
|
||||||
|
$("#mapLayers").disableSelection();
|
||||||
|
|
||||||
|
// show control elements and remove loading screen on map load
|
||||||
|
d3.select("#loading").transition().duration(5000).style("opacity", 0).remove();
|
||||||
|
d3.select("#initial").transition().duration(5000).attr("opacity", 0).remove();
|
||||||
|
d3.select("#optionsContainer").transition().duration(5000).style("opacity", 1);
|
||||||
|
d3.select("#tooltip").transition().duration(5000).style("opacity", 1);
|
||||||
|
|
||||||
|
// remove glow if tip is aknowledged
|
||||||
|
if (localStorage.getItem("disable_click_arrow_tooltip")) {
|
||||||
|
clearMainTip();
|
||||||
|
optionsTrigger.classList.remove("glow");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show options pane on trigger click
|
||||||
|
function showOptions(event) {
|
||||||
|
if (!localStorage.getItem("disable_click_arrow_tooltip")) {
|
||||||
|
clearMainTip();
|
||||||
|
localStorage.setItem("disable_click_arrow_tooltip", true);
|
||||||
|
optionsTrigger.classList.remove("glow");
|
||||||
|
}
|
||||||
|
|
||||||
|
regenerate.style.display = "none";
|
||||||
|
options.style.display = "block";
|
||||||
|
optionsTrigger.style.display = "none";
|
||||||
|
|
||||||
|
if (event) event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide options pane on trigger click
|
||||||
|
function hideOptions(event) {
|
||||||
|
options.style.display = "none";
|
||||||
|
optionsTrigger.style.display = "block";
|
||||||
|
if (event) event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
// To toggle options on hotkey press
|
||||||
|
function toggleOptions(event) {
|
||||||
|
if (options.style.display === "none") showOptions(event);
|
||||||
|
else hideOptions(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle "New Map!" pane on hover
|
||||||
|
optionsTrigger.addEventListener("mouseenter", function() {
|
||||||
|
if (optionsTrigger.classList.contains("glow")) return;
|
||||||
|
if (options.style.display === "none") regenerate.style.display = "block";
|
||||||
|
});
|
||||||
|
|
||||||
|
collapsible.addEventListener("mouseleave", function() {
|
||||||
|
regenerate.style.display = "none";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Activate options tab on click
|
||||||
|
options.querySelector("div.tab").addEventListener("click", function(event) {
|
||||||
|
if (event.target.tagName !== "BUTTON") return;
|
||||||
|
const id = event.target.id;
|
||||||
|
const active = options.querySelector(".tab > button.active");
|
||||||
|
if (active && id === active.id) return; // already active tab is clicked
|
||||||
|
|
||||||
|
if (active) active.classList.remove("active");
|
||||||
|
document.getElementById(id).classList.add("active");
|
||||||
|
options.querySelectorAll(".tabcontent").forEach(e => e.style.display = "none");
|
||||||
|
|
||||||
|
if (id === "styleTab") styleContent.style.display = "block"; else
|
||||||
|
if (id === "optionsTab") optionsContent.style.display = "block"; else
|
||||||
|
if (id === "toolsTab" && !customization) toolsContent.style.display = "block"; else
|
||||||
|
if (id === "toolsTab" && customization) customizationMenu.style.display = "block"; else
|
||||||
|
if (id === "aboutTab") aboutContent.style.display = "block";
|
||||||
|
});
|
||||||
|
|
||||||
|
options.querySelectorAll("i.collapsible").forEach(el => el.addEventListener("click", collapse));
|
||||||
|
function collapse(e) {
|
||||||
|
const trigger = e.target;
|
||||||
|
const section = trigger.parentElement.nextElementSibling;
|
||||||
|
|
||||||
|
if (section.style.display === "none") {
|
||||||
|
section.style.display = "block";
|
||||||
|
trigger.classList.replace("icon-down-open", "icon-up-open");
|
||||||
|
} else {
|
||||||
|
section.style.display = "none";
|
||||||
|
trigger.classList.replace("icon-up-open", "icon-down-open");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle style sections on element select
|
||||||
|
styleElementSelect.addEventListener("change", selectStyleElement);
|
||||||
|
function selectStyleElement() {
|
||||||
|
const sel = styleElementSelect.value;
|
||||||
|
let el = viewbox.select("#"+sel);
|
||||||
|
|
||||||
|
styleElements.querySelectorAll("tbody").forEach(e => e.style.display = "none"); // hide all sections
|
||||||
|
const off = el.style("display") === "none" || !el.selectAll("*").size(); // check if layer is off
|
||||||
|
if (off) {
|
||||||
|
styleIsOff.style.display = "block";
|
||||||
|
setTimeout(() => styleIsOff.style.display = "none", 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// active group element
|
||||||
|
const group = styleGroupSelect.value;
|
||||||
|
if (sel == "ocean") el = oceanLayers.select("rect");
|
||||||
|
else if (sel == "routes" || sel == "labels" || sel == "lakes" || sel == "anchors" || sel == "burgIcons") {
|
||||||
|
el = d3.select("#"+sel).select("g#"+group).size()
|
||||||
|
? d3.select("#"+sel).select("g#"+group)
|
||||||
|
: d3.select("#"+sel).select("g");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sel !== "landmass") {
|
||||||
|
// opacity
|
||||||
|
styleOpacity.style.display = "block";
|
||||||
|
styleOpacityInput.value = styleOpacityOutput.value = el.attr("opacity") || 1;
|
||||||
|
|
||||||
|
// filter
|
||||||
|
styleFilter.style.display = "block";
|
||||||
|
if (sel == "ocean") el = oceanLayers;
|
||||||
|
styleFilterInput.value = el.attr("filter") || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill
|
||||||
|
if (sel === "rivers" || sel === "lakes" || sel === "landmass" || sel === "prec") {
|
||||||
|
styleFill.style.display = "block";
|
||||||
|
styleFillInput.value = styleFillOutput.value = el.attr("fill");
|
||||||
|
}
|
||||||
|
|
||||||
|
// stroke color and width
|
||||||
|
if (sel === "routes" || sel === "lakes" || sel === "borders" || sel === "cults" || sel === "cells" || sel === "gridOverlay" || sel === "coastline" || sel === "prec" || sel === "icons" || sel === "coordinates") {
|
||||||
|
styleStroke.style.display = "block";
|
||||||
|
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke");
|
||||||
|
styleStrokeWidth.style.display = "block";
|
||||||
|
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// stroke dash
|
||||||
|
if (sel === "routes" || sel === "borders" || sel === "gridOverlay" || sel === "temperature" || sel === "population" || sel === "coordinates") {
|
||||||
|
styleStrokeDash.style.display = "block";
|
||||||
|
styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || "";
|
||||||
|
styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit";
|
||||||
|
}
|
||||||
|
|
||||||
|
// clipping
|
||||||
|
if (sel === "cells" || sel === "gridOverlay" || sel === "coordinates" || sel === "compass" || sel === "terrain" || sel === "temperature" || sel === "routes" || sel === "texture" || sel === "biomes") {
|
||||||
|
styleClipping.style.display = "block";
|
||||||
|
styleClippingInput.value = el.attr("mask") || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// shift (translate)
|
||||||
|
if (sel === "gridOverlay") {
|
||||||
|
styleShift.style.display = "block";
|
||||||
|
const tr = parseTransform(el.attr("transform"));
|
||||||
|
styleShiftX.value = tr[0];
|
||||||
|
styleShiftY.value = tr[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sel === "compass") {
|
||||||
|
styleCompass.style.display = "block";
|
||||||
|
const tr = parseTransform(d3.select("#rose").attr("transform"));
|
||||||
|
styleCompassShiftX.value = tr[0];
|
||||||
|
styleCompassShiftY.value = tr[1];
|
||||||
|
styleCompassSizeInput.value = styleCompassSizeOutput.value = tr[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
// show specific sections
|
||||||
|
if (sel === "terrs") styleHeightmap.style.display = "block";
|
||||||
|
if (sel === "gridOverlay") styleGrid.style.display = "block";
|
||||||
|
if (sel === "terrain") styleRelief.style.display = "block";
|
||||||
|
if (sel === "texture") styleTexture.style.display = "block";
|
||||||
|
if (sel === "routes" || sel === "labels" || sel == "anchors" || sel == "burgIcons" || sel === "lakes") {styleGroup.style.display = "block";}
|
||||||
|
|
||||||
|
if (sel === "population") {
|
||||||
|
stylePopulation.style.display = "block";
|
||||||
|
stylePopulationRuralStrokeInput.value = stylePopulationRuralStrokeOutput.value = population.select("#rural").attr("stroke");
|
||||||
|
stylePopulationUrbanStrokeInput.value = stylePopulationUrbanStrokeOutput.value = population.select("#urban").attr("stroke");
|
||||||
|
styleStrokeWidth.style.display = "block";
|
||||||
|
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sel === "labels") {
|
||||||
|
styleFill.style.display = "block";
|
||||||
|
styleStroke.style.display = "block";
|
||||||
|
styleStrokeWidth.style.display = "block";
|
||||||
|
loadDefaultFonts();
|
||||||
|
styleFont.style.display = "block";
|
||||||
|
styleSize.style.display = "block";
|
||||||
|
styleVisibility.style.display = "block";
|
||||||
|
styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#3e3e4b";
|
||||||
|
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#3a3a3a";
|
||||||
|
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || 0;
|
||||||
|
styleSelectFont.value = fonts.indexOf(el.attr("data-font"));
|
||||||
|
styleInputFont.style.display = "none";
|
||||||
|
styleInputFont.value = "";
|
||||||
|
styleFontSize.value = el.attr("data-size");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sel == "burgIcons") {
|
||||||
|
styleFill.style.display = "block";
|
||||||
|
styleStroke.style.display = "block";
|
||||||
|
styleStrokeWidth.style.display = "block";
|
||||||
|
styleStrokeDash.style.display = "block";
|
||||||
|
styleRadius.style.display = "block";
|
||||||
|
styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#ffffff";
|
||||||
|
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#3e3e4b";
|
||||||
|
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || .24;
|
||||||
|
styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || "";
|
||||||
|
styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit";
|
||||||
|
styleRadiusInput.value = el.attr("size") || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sel == "anchors") {
|
||||||
|
styleFill.style.display = "block";
|
||||||
|
styleStroke.style.display = "block";
|
||||||
|
styleStrokeWidth.style.display = "block";
|
||||||
|
styleIconSize.style.display = "block";
|
||||||
|
styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#ffffff";
|
||||||
|
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#3e3e4b";
|
||||||
|
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || .24;
|
||||||
|
styleIconSizeInput.value = el.attr("size") || 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sel === "ocean") {
|
||||||
|
styleOcean.style.display = "block";
|
||||||
|
styleOceanBack.value = styleOceanBackOutput.value = svg.attr("background-color");
|
||||||
|
styleOceanFore.value = styleOceanForeOutput.value = oceanLayers.select("rect").attr("fill");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sel === "coastline") {
|
||||||
|
styleCoastline.style.display = "block";
|
||||||
|
if (styleCoastlineAuto.checked) styleFilter.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sel === "temperature") {
|
||||||
|
styleStrokeWidth.style.display = "block";
|
||||||
|
styleTemperature.style.display = "block";
|
||||||
|
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
|
||||||
|
styleTemperatureFillOpacityInput.value = styleTemperatureFillOpacityOutput.value = el.attr("fill-opacity") || .1;
|
||||||
|
styleTemperatureFillInput.value = styleTemperatureFillOutput.value = el.attr("fill") || "#000";
|
||||||
|
styleTemperatureFontSizeInput.value = styleTemperatureFontSizeOutput.value = el.attr("font-size") || "8px";;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sel === "coordinates") {
|
||||||
|
styleSize.style.display = "block";
|
||||||
|
styleFontSize.value = el.attr("data-size");
|
||||||
|
}
|
||||||
|
|
||||||
|
// update group options
|
||||||
|
styleGroupSelect.options.length = 0; // remove all options
|
||||||
|
if (sel === "routes" || sel === "labels" || sel === "lakes" || sel === "anchors" || sel === "burgIcons") {
|
||||||
|
document.getElementById(sel).querySelectorAll("g").forEach(el => {
|
||||||
|
if (el.id === "burgLabels") return;
|
||||||
|
const count = el.childElementCount;
|
||||||
|
styleGroupSelect.options.add(new Option(`${el.id} (${count})`, el.id, false, false));
|
||||||
|
});
|
||||||
|
styleGroupSelect.value = el.attr("id");
|
||||||
|
} else {
|
||||||
|
styleGroupSelect.options.add(new Option(sel, sel, false, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle style inputs change
|
||||||
|
styleGroupSelect.addEventListener("change", selectStyleElement);
|
||||||
|
|
||||||
|
function getEl() {
|
||||||
|
const el = styleElementSelect.value, g = styleGroupSelect.value;
|
||||||
|
if (g === el) return svg.select("#"+el); else return svg.select("#"+el).select("#"+g);
|
||||||
|
}
|
||||||
|
|
||||||
|
styleFillInput.addEventListener("input", function() {
|
||||||
|
styleFillOutput.value = this.value;
|
||||||
|
getEl().attr('fill', this.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
styleStrokeInput.addEventListener("input", function() {
|
||||||
|
styleStrokeOutput.value = this.value;
|
||||||
|
getEl().attr('stroke', this.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
styleStrokeWidthInput.addEventListener("input", function() {
|
||||||
|
styleStrokeWidthOutput.value = this.value;
|
||||||
|
getEl().attr('stroke-width', +this.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
styleStrokeDasharrayInput.addEventListener("input", function() {
|
||||||
|
getEl().attr('stroke-dasharray', this.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
styleStrokeLinecapInput.addEventListener("change", function() {
|
||||||
|
getEl().attr('stroke-linecap', this.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
styleOpacityInput.addEventListener("input", function() {
|
||||||
|
styleOpacityOutput.value = this.value;
|
||||||
|
getEl().attr('opacity', this.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
styleFilterInput.addEventListener("change", function() {
|
||||||
|
if (styleGroupSelect.value === "ocean") {oceanLayers.attr('filter', this.value); return;}
|
||||||
|
getEl().attr('filter', this.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
styleTextureInput.addEventListener("change", function() {
|
||||||
|
texture.select("image").attr("xlink:href", getAbsolutePath(this.value));
|
||||||
|
});
|
||||||
|
|
||||||
|
styleTextureShiftX.addEventListener("input", function() {
|
||||||
|
texture.select("image").attr("x", this.value).attr("width", svgWidth - this.valueAsNumber);
|
||||||
|
});
|
||||||
|
|
||||||
|
styleTextureShiftY.addEventListener("input", function() {
|
||||||
|
texture.select("image").attr("y", this.value).attr("height", svgHeight - this.valueAsNumber);
|
||||||
|
});
|
||||||
|
|
||||||
|
styleClippingInput.addEventListener("change", function() {
|
||||||
|
getEl().attr('mask', this.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
styleGridType.addEventListener("change", function() {
|
||||||
|
if (layerIsOn("toggleGrid")) drawGrid();
|
||||||
|
});
|
||||||
|
|
||||||
|
styleGridSize.addEventListener("input", function() {
|
||||||
|
if (layerIsOn("toggleGrid")) drawGrid();
|
||||||
|
styleGridSizeOutput.value = this.value;
|
||||||
|
calculateFriendlyGridSize();
|
||||||
|
});
|
||||||
|
|
||||||
|
function calculateFriendlyGridSize() {
|
||||||
|
const size = styleGridSize.value * Math.cos(30 * Math.PI / 180) * 2;;
|
||||||
|
const friendly = "(" + rn(size * distanceScale.value) + " " + distanceUnit.value + ")";
|
||||||
|
styleGridSizeFriendly.value = friendly;
|
||||||
|
}
|
||||||
|
|
||||||
|
styleShiftX.addEventListener("input", shiftElement);
|
||||||
|
styleShiftY.addEventListener("input", shiftElement);
|
||||||
|
|
||||||
|
function shiftElement() {
|
||||||
|
const x = styleShiftX.value || 0;
|
||||||
|
const y = styleShiftY.value || 0;
|
||||||
|
getEl().attr("transform", `translate(${x},${y})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
styleOceanBack.addEventListener("input", function() {
|
||||||
|
svg.style("background-color", this.value);
|
||||||
|
styleOceanBackOutput.value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
styleOceanFore.addEventListener("input", function() {
|
||||||
|
oceanLayers.select("rect").attr("fill", this.value);
|
||||||
|
styleOceanForeOutput.value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
styleOceanPattern.addEventListener("change", function() {
|
||||||
|
svg.select("pattern#oceanic rect").attr("filter", this.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
outlineLayersInput.addEventListener("change", function() {
|
||||||
|
oceanLayers.selectAll("path").remove();
|
||||||
|
OceanLayers();
|
||||||
|
});
|
||||||
|
|
||||||
|
styleReliefSizeInput.addEventListener("input", function() {
|
||||||
|
styleReliefSizeOutput.value = this.value;
|
||||||
|
const size = +this.value;
|
||||||
|
|
||||||
|
terrain.selectAll("use").each(function(d) {
|
||||||
|
const newSize = this.getAttribute("data-size") * size;
|
||||||
|
const shift = (newSize - +this.getAttribute("width")) / 2;
|
||||||
|
this.setAttribute("width", newSize);
|
||||||
|
this.setAttribute("height", newSize);
|
||||||
|
const x = +this.getAttribute("x");
|
||||||
|
const y = +this.getAttribute("y");
|
||||||
|
this.setAttribute("x", x - shift);
|
||||||
|
this.setAttribute("y", y - shift);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
styleReliefDensityInput.addEventListener("input", function() {
|
||||||
|
styleReliefDensityOutput.value = rn(this.value * 100) + "%";
|
||||||
|
ReliefIcons();
|
||||||
|
});
|
||||||
|
|
||||||
|
styleTemperatureFillOpacityInput.addEventListener("input", function() {
|
||||||
|
temperature.attr("fill-opacity", this.value);
|
||||||
|
styleTemperatureFillOpacityOutput.value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
styleTemperatureFontSizeInput.addEventListener("input", function() {
|
||||||
|
temperature.attr("font-size", this.value + "px");
|
||||||
|
styleTemperatureFontSizeOutput.value = this.value + "px";
|
||||||
|
});
|
||||||
|
|
||||||
|
styleTemperatureFillInput.addEventListener("input", function() {
|
||||||
|
temperature.attr("fill", this.value);
|
||||||
|
styleTemperatureFillOutput.value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
stylePopulationRuralStrokeInput.addEventListener("input", function() {
|
||||||
|
population.select("#rural").attr("stroke", this.value);
|
||||||
|
stylePopulationRuralStrokeOutput.value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
stylePopulationUrbanStrokeInput.addEventListener("input", function() {
|
||||||
|
population.select("#urban").attr("stroke", this.value);
|
||||||
|
stylePopulationUrbanStrokeOutput.value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
styleCompassSizeInput.addEventListener("input", function() {
|
||||||
|
styleCompassSizeOutput.value = this.value;
|
||||||
|
shiftCompass();
|
||||||
|
});
|
||||||
|
|
||||||
|
styleCompassShiftX.addEventListener("input", shiftCompass);
|
||||||
|
styleCompassShiftY.addEventListener("input", shiftCompass);
|
||||||
|
|
||||||
|
function shiftCompass() {
|
||||||
|
const tr = `translate(${styleCompassShiftX.value} ${styleCompassShiftY.value}) scale(${styleCompassSizeInput.value})`;
|
||||||
|
d3.select("#rose").attr("transform", tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
styleSelectFont.addEventListener("change", changeFont);
|
||||||
|
function changeFont() {
|
||||||
|
const value = styleSelectFont.value;
|
||||||
|
const font = fonts[value].split(':')[0].replace(/\+/g, " ");
|
||||||
|
getEl().attr("font-family", font).attr("data-font", fonts[value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
styleFontAdd.addEventListener("click", function() {
|
||||||
|
if (styleInputFont.style.display === "none") {
|
||||||
|
styleInputFont.style.display = "inline-block";
|
||||||
|
styleInputFont.focus();
|
||||||
|
styleSelectFont.style.display = "none";
|
||||||
|
} else {
|
||||||
|
styleInputFont.style.display = "none";
|
||||||
|
styleSelectFont.style.display = "inline-block";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
styleInputFont.addEventListener("change", function() {
|
||||||
|
if (!this.value) {tip("Please provide a valid Google font name or link to a @font-face declaration"); return;}
|
||||||
|
fetchFonts(this.value).then(fetched => {
|
||||||
|
if (!fetched) return;
|
||||||
|
styleFontAdd.click();
|
||||||
|
styleInputFont.value = "";
|
||||||
|
if (fetched !== 1) return;
|
||||||
|
styleSelectFont.value = fonts.length-1;
|
||||||
|
changeFont(); // auto-change font if 1 font is fetched
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
styleFontSize.addEventListener("change", function() {
|
||||||
|
changeFontSize(+this.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
styleFontPlus.addEventListener("click", function() {
|
||||||
|
const size = Math.max(rn(getEl().attr("data-size") * 1.1, 2), 1);
|
||||||
|
changeFontSize(size);
|
||||||
|
});
|
||||||
|
|
||||||
|
styleFontMinus.addEventListener("click", function() {
|
||||||
|
const size = Math.max(rn(getEl().attr("data-size") * .9, 2), 1);
|
||||||
|
changeFontSize(size);
|
||||||
|
});
|
||||||
|
|
||||||
|
function changeFontSize(size) {
|
||||||
|
getEl().attr("data-size", size).attr("font-size", rn((size + (size / scale)) / 2, 2));
|
||||||
|
styleFontSize.value = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
styleRadiusInput.addEventListener("change", function() {
|
||||||
|
changeRadius(+this.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
styleRadiusPlus.addEventListener("click", function() {
|
||||||
|
const size = Math.max(rn(getEl().attr("size") * 1.1, 2), .2);
|
||||||
|
changeRadius(size);
|
||||||
|
});
|
||||||
|
|
||||||
|
styleRadiusMinus.addEventListener("click", function() {
|
||||||
|
const size = Math.max(rn(getEl().attr("size") * .9, 2), .2);
|
||||||
|
changeRadius(size);
|
||||||
|
});
|
||||||
|
|
||||||
|
function changeRadius(size) {
|
||||||
|
getEl().attr("size", size)
|
||||||
|
getEl().selectAll("circle").each(function() {this.setAttribute("r", size)});
|
||||||
|
styleRadiusInput.value = size;
|
||||||
|
const group = getEl().attr("id");
|
||||||
|
burgLabels.select("g#"+group).selectAll("text").each(function() {this.setAttribute("dy", `${size * -1.5}px`)});
|
||||||
|
changeIconSize(size * 2, group); // change also anchor icons
|
||||||
|
}
|
||||||
|
|
||||||
|
styleIconSizeInput.addEventListener("change", function() {
|
||||||
|
changeIconSize(+this.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
styleIconSizePlus.addEventListener("click", function() {
|
||||||
|
const size = Math.max(rn(getEl().attr("size") * 1.1, 2), .2);
|
||||||
|
changeIconSize(size);
|
||||||
|
});
|
||||||
|
|
||||||
|
styleIconSizeMinus.addEventListener("click", function() {
|
||||||
|
const size = Math.max(rn(getEl().attr("size") * .9, 2), .2);
|
||||||
|
changeIconSize(size);
|
||||||
|
});
|
||||||
|
|
||||||
|
function changeIconSize(size, group) {
|
||||||
|
const el = group ? anchors.select("#"+group) : getEl();
|
||||||
|
const oldSize = +el.attr("size");
|
||||||
|
const shift = (size - oldSize) / 2;
|
||||||
|
el.attr("size", size);
|
||||||
|
el.selectAll("use").each(function() {
|
||||||
|
const x = +this.getAttribute("x");
|
||||||
|
const y = +this.getAttribute("y");
|
||||||
|
this.setAttribute("x", x - shift);
|
||||||
|
this.setAttribute("y", y - shift);
|
||||||
|
this.setAttribute("width", size);
|
||||||
|
this.setAttribute("height", size);
|
||||||
|
});;
|
||||||
|
styleIconSizeInput.value = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// request to restore default style on button click
|
||||||
|
function askToRestoreDefaultStyle() {
|
||||||
|
alertMessage.innerHTML = "Are you sure you want to restore default style for all elements?";
|
||||||
|
$("#alert").dialog({resizable: false, title: "Restore default style",
|
||||||
|
buttons: {
|
||||||
|
Restore: function() {
|
||||||
|
applyDefaultStyle();
|
||||||
|
selectStyleElement();
|
||||||
|
$(this).dialog("close");
|
||||||
|
},
|
||||||
|
Cancel: function() {$(this).dialog("close");}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// request a URL to image to be used as a texture
|
||||||
|
function textureProvideURL() {
|
||||||
|
alertMessage.innerHTML = `Provide an image URL to be used as a texture:
|
||||||
|
<input id="textureURL" type="url" style="width: 254px" placeholder="http://www.example.com/image.jpg" oninput="fetchTextureURL(this.value)">
|
||||||
|
<div style="border: 1px solid darkgrey; height: 144px; margin-top: 2px"><canvas id="preview" width="256px" height="144px"></canvas></div>`;
|
||||||
|
$("#alert").dialog({resizable: false, title: "Load custom texture", width: 280,
|
||||||
|
buttons: {
|
||||||
|
Apply: function() {
|
||||||
|
const name = textureURL.value.split("/").pop();
|
||||||
|
if (!name || name === "") {tip("Please provide a valid URL", false, "error"); return;}
|
||||||
|
const opt = document.createElement("option");
|
||||||
|
opt.value = textureURL.value;
|
||||||
|
opt.text = name.slice(0, 20);
|
||||||
|
styleTextureInput.add(opt);
|
||||||
|
styleTextureInput.value = textureURL.value;
|
||||||
|
texture.select("image").attr('xlink:href', textureURL.value);
|
||||||
|
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
|
||||||
|
$(this).dialog("close");
|
||||||
|
},
|
||||||
|
Cancel: function() {$(this).dialog("close");}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchTextureURL(url) {
|
||||||
|
console.log("Provided URL is", url);
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = function () {
|
||||||
|
const canvas = document.getElementById("preview");
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
|
};
|
||||||
|
img.src = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style map filters handler
|
||||||
|
mapFilters.addEventListener("click", applyMapFilter);
|
||||||
|
function applyMapFilter() {
|
||||||
|
if (event.target.tagName !== "BUTTON") return;
|
||||||
|
const button = event.target;
|
||||||
|
svg.attr("filter", null);
|
||||||
|
if (button.classList.contains("pressed")) {button.classList.remove("pressed"); return;}
|
||||||
|
mapFilters.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
|
||||||
|
button.classList.add("pressed");
|
||||||
|
svg.attr("filter", "url(#filter-" + button.id + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option listeners
|
||||||
|
const optionsContent = document.getElementById("optionsContent");
|
||||||
|
optionsContent.addEventListener("input", function(event) {
|
||||||
|
const id = event.target.id, value = event.target.value;
|
||||||
|
if (id === "mapWidthInput" || id === "mapHeightInput") mapSizeInputChange();
|
||||||
|
else if (id === "densityInput" || id === "densityOutput") changeCellsDensity(value);
|
||||||
|
else if (id === "culturesInput") culturesOutput.value = value;
|
||||||
|
else if (id === "culturesOutput") culturesInput.value = value;
|
||||||
|
else if (id === "regionsInput" || id === "regionsOutput") changeStatesNumber(value);
|
||||||
|
else if (id === "powerInput") powerOutput.value = value;
|
||||||
|
else if (id === "powerOutput") powerInput.value = value;
|
||||||
|
else if (id === "neutralInput") neutralOutput.value = value;
|
||||||
|
else if (id === "neutralOutput") neutralInput.value = value;
|
||||||
|
else if (id === "manorsInput") manorsOutput.value = value;
|
||||||
|
else if (id === "manorsOutput") manorsInput.value = value;
|
||||||
|
else if (id === "uiSizeInput" || id === "uiSizeOutput") changeUIsize(value);
|
||||||
|
else if (id === "tooltipSizeInput" || id === "tooltipSizeOutput") changeTooltipSize(value);
|
||||||
|
else if (id === "transparencyInput") changeDialogsTransparency(value);
|
||||||
|
else if (id === "pngResolutionInput") pngResolutionOutput.value = value;
|
||||||
|
else if (id === "pngResolutionOutput") pngResolutionInput.value = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
optionsContent.addEventListener("change", function(event) {
|
||||||
|
if (event.target.dataset.stored) lock(event.target.dataset.stored);
|
||||||
|
const id = event.target.id, value = event.target.value;
|
||||||
|
if (id === "zoomExtentMin" || id === "zoomExtentMax") changeZoomExtent(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
optionsContent.addEventListener("click", function(event) {
|
||||||
|
const id = event.target.id;
|
||||||
|
if (id === "toggleFullscreen") toggleFullscreen();
|
||||||
|
else if (id === "optionsSeedGenerate") generateMapWithSeed();
|
||||||
|
else if (id === "optionsMapHistory") showSeedHistoryDialog();
|
||||||
|
else if (id === "zoomExtentDefault") restoreDefaultZoomExtent();
|
||||||
|
});
|
||||||
|
|
||||||
|
function mapSizeInputChange() {
|
||||||
|
changeMapSize();
|
||||||
|
autoResize = false;
|
||||||
|
localStorage.setItem("mapWidth", mapWidthInput.value);
|
||||||
|
localStorage.setItem("mapHeight", mapHeightInput.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// change svg size on manual size change or window resize, do not change graph size
|
||||||
|
function changeMapSize() {
|
||||||
|
svgWidth = +mapWidthInput.value;
|
||||||
|
svgHeight = +mapHeightInput.value;
|
||||||
|
svg.attr("width", svgWidth).attr("height", svgHeight);
|
||||||
|
const width = Math.max(svgWidth, graphWidth);
|
||||||
|
const height = Math.max(svgHeight, graphHeight);
|
||||||
|
zoom.translateExtent([[0, 0], [width, height]]);
|
||||||
|
fitScaleBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
// just apply map size that was already set, apply graph size!
|
||||||
|
function applyMapSize() {
|
||||||
|
svgWidth = graphWidth = +mapWidthInput.value;
|
||||||
|
svgHeight = graphHeight = +mapHeightInput.value;
|
||||||
|
svg.attr("width", svgWidth).attr("height", svgHeight);
|
||||||
|
zoom.translateExtent([[0, 0],[graphWidth, graphHeight]]).scaleExtent([1, 20]).scaleTo(svg, 1);
|
||||||
|
viewbox.attr("transform", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFullscreen() {
|
||||||
|
if (mapWidthInput.value != window.innerWidth || mapHeightInput.value != window.innerHeight) {
|
||||||
|
mapWidthInput.value = window.innerWidth;
|
||||||
|
mapHeightInput.value = window.innerHeight;
|
||||||
|
localStorage.removeItem("mapHeight");
|
||||||
|
localStorage.removeItem("mapWidth");
|
||||||
|
} else {
|
||||||
|
mapWidthInput.value = graphWidth;
|
||||||
|
mapHeightInput.value = graphHeight;
|
||||||
|
}
|
||||||
|
changeMapSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateMapWithSeed() {
|
||||||
|
if (optionsSeed.value == seed) {
|
||||||
|
tip("The current map already has this seed", false, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
regeneratePrompt();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSeedHistoryDialog() {
|
||||||
|
const alert = mapHistory.map(function(h, i) {
|
||||||
|
const created = new Date(h.created).toLocaleTimeString();
|
||||||
|
const button = `<i data-tip"Click to generate a map with this seed" onclick="restoreSeed(${i})" class="icon-history optionsSeedRestore"></i>`;
|
||||||
|
return `<div>${i+1}. Seed: ${h.seed} ${button}. Size: ${h.width}x${h.height}. Template: ${h.template}. Created: ${created}</div>`;
|
||||||
|
}).join("");
|
||||||
|
alertMessage.innerHTML = alert;
|
||||||
|
$("#alert").dialog({
|
||||||
|
resizable: false, title: "Seed history",
|
||||||
|
width: fitContent(), position: {my: "center", at: "center", of: "svg"}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate map with historycal seed
|
||||||
|
function restoreSeed(id) {
|
||||||
|
if (mapHistory[id].seed == seed) {
|
||||||
|
tip("The current map is already generated with this seed", null, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
optionsSeed.value = mapHistory[id].seed;
|
||||||
|
mapWidthInput.value = mapHistory[id].width;
|
||||||
|
mapHeightInput.value = mapHistory[id].height;
|
||||||
|
templateInput.value = mapHistory[id].template;
|
||||||
|
if (locked("template")) unlock("template");
|
||||||
|
regeneratePrompt();
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreDefaultZoomExtent() {
|
||||||
|
zoomExtentMin.value = 1;
|
||||||
|
zoomExtentMax.value = 20;
|
||||||
|
zoom.scaleExtent([1, 20]).scaleTo(svg, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeCellsDensity(value) {
|
||||||
|
densityInput.value = densityOutput.value = value;
|
||||||
|
if (value == 3) densityOutput.style.color = "red";
|
||||||
|
else if (value == 2) densityOutput.style.color = "yellow";
|
||||||
|
else if (value == 1) densityOutput.style.color = "green";
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeStatesNumber(value) {
|
||||||
|
regionsInput.value = regionsOutput.value = value;
|
||||||
|
burgLabels.select("#capitals").attr("data-size", Math.max(rn(6 - value / 20), 3));
|
||||||
|
labels.select("#countries").attr("data-size", Math.max(rn(18 - value / 6), 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeUIsize(value) {
|
||||||
|
uiSizeInput.value = uiSizeOutput.value = value;
|
||||||
|
document.getElementsByTagName("body")[0].style.fontSize = value * 11 + "px";
|
||||||
|
document.getElementById("options").style.width = (value - 1) * 300 / 2 + 300 + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeTooltipSize(value) {
|
||||||
|
tooltipSizeInput.value = tooltipSizeOutput.value = value;
|
||||||
|
tooltip.style.fontSize = `calc(${value}px + 0.5vw)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// change transparency for modal windows
|
||||||
|
function changeDialogsTransparency(value) {
|
||||||
|
transparencyInput.value = transparencyOutput.value = value;
|
||||||
|
const alpha = (100 - +value) / 100;
|
||||||
|
const optionsColor = "rgba(164, 139, 149, " + alpha + ")";
|
||||||
|
const dialogsColor = "rgba(255, 255, 255, " + alpha + ")";
|
||||||
|
const optionButtonsColor = "rgba(145, 110, 127, " + Math.min(alpha + .3, 1) + ")";
|
||||||
|
const optionLiColor = "rgba(153, 123, 137, " + Math.min(alpha + .3, 1) + ")";
|
||||||
|
document.getElementById("options").style.backgroundColor = optionsColor;
|
||||||
|
document.getElementById("dialogs").style.backgroundColor = dialogsColor;
|
||||||
|
document.querySelectorAll(".tabcontent button").forEach(el => el.style.backgroundColor = optionButtonsColor);
|
||||||
|
document.querySelectorAll(".tabcontent li").forEach(el => el.style.backgroundColor = optionLiColor);
|
||||||
|
document.querySelectorAll("button.options").forEach(el => el.style.backgroundColor = optionLiColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeZoomExtent(value) {
|
||||||
|
zoom.scaleExtent([+zoomExtentMin.value, +zoomExtentMax.value]);
|
||||||
|
zoom.scaleTo(svg, +value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// control sroted options
|
||||||
|
function applyStoredOptions() {
|
||||||
|
for(let i=0; i < localStorage.length; i++){
|
||||||
|
const stored = localStorage.key(i), value = localStorage.getItem(stored);
|
||||||
|
const input = document.getElementById(stored+"Input");
|
||||||
|
const output = document.getElementById(stored+"Output");
|
||||||
|
if (input) input.value = value;
|
||||||
|
if (output) output.value = value;
|
||||||
|
lock(stored);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!localStorage.getItem("mapWidth") || !localStorage.getItem("mapHeight")) {
|
||||||
|
mapWidthInput.value = window.innerWidth;
|
||||||
|
mapHeightInput.value = window.innerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorage.getItem("winds")) winds = localStorage.getItem("winds").split(",").map(w => +w);
|
||||||
|
|
||||||
|
changeDialogsTransparency(localStorage.getItem("transparency") || 30);
|
||||||
|
if (localStorage.getItem("uiSize")) changeUIsize(localStorage.getItem("uiSize"));
|
||||||
|
if (localStorage.getItem("tooltipSize")) changeTooltipSize(localStorage.getItem("tooltipSize"));
|
||||||
|
if (localStorage.getItem("regions")) changeStatesNumber(localStorage.getItem("regions"));
|
||||||
|
|
||||||
|
if (localStorage.getItem("equator")) {
|
||||||
|
const eqY = +equatorInput.value;
|
||||||
|
equidistanceOutput.min = equidistanceInput.min = Math.max(+mapHeightInput.value - eqY, eqY);
|
||||||
|
equidistanceOutput.max = equidistanceInput.max = equidistanceOutput.min * 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// randomize options if randomization is allowed in option
|
||||||
|
function randomizeOptions() {
|
||||||
|
Math.seedrandom(seed); // reset seed to initial one
|
||||||
|
if (!locked("regions")) regionsInput.value = regionsOutput.value = rand(12, 17);
|
||||||
|
if (!locked("manors")) manorsInput.value = manorsOutput.value = rn(0.5 + Math.random(), 1);
|
||||||
|
if (!locked("power")) powerInput.value = powerOutput.value = rand(0, 4);
|
||||||
|
if (!locked("neutral")) neutralInput.value = neutralOutput.value = rn(0.8 + Math.random(), 1);
|
||||||
|
if (!locked("cultures")) culturesInput.value = culturesOutput.value = rand(10, 15);
|
||||||
|
if (!locked("prec")) precInput.value = precOutput.value = gauss(100, 40, 0, 500);
|
||||||
|
const tMax = +temperatureEquatorOutput.max, tMin = +temperatureEquatorOutput.min; // temperature extremes
|
||||||
|
if (!locked("temperatureEquator")) temperatureEquatorOutput.value = temperatureEquatorInput.value = rand(tMax-6, tMax);
|
||||||
|
if (!locked("temperaturePole")) temperaturePoleOutput.value = temperaturePoleInput.value = rand(tMin, tMin+10);
|
||||||
|
if (!locked("equator") && !locked("equidistance")) randomizeWorldSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// define world size
|
||||||
|
function randomizeWorldSize() {
|
||||||
|
const eq = document.getElementById("equatorInput");
|
||||||
|
const eqDI = document.getElementById("equidistanceInput");
|
||||||
|
const eqDO = document.getElementById("equidistanceOutput");
|
||||||
|
|
||||||
|
const eqY = equatorOutput.value = eq.value = rand(+eq.min, +eq.max); // equator Y
|
||||||
|
eqDO.min = eqDI.min = Math.max(graphHeight - eqY, eqY);
|
||||||
|
eqDO.max = eqDI.max = eqDO.min * 10;
|
||||||
|
eqDO.value = eqDI.value = rand(rn(eqDO.min * 1.2), rn(eqDO.min * 4)); // distance from equator to poles
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove all saved data from LocalStorage and reload the page
|
||||||
|
function restoreDefaultOptions() {
|
||||||
|
localStorage.clear();
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// FONTS
|
||||||
|
|
||||||
|
// fetch default fonts if not done before
|
||||||
|
function loadDefaultFonts() {
|
||||||
|
if (!$('link[href="fonts.css"]').length) {
|
||||||
|
$("head").append('<link rel="stylesheet" type="text/css" href="fonts.css">');
|
||||||
|
const fontsToAdd = ["Amatic+SC:700", "IM+Fell+English", "Great+Vibes", "MedievalSharp", "Metamorphous",
|
||||||
|
"Nova+Script", "Uncial+Antiqua", "Underdog", "Caesar+Dressing", "Bitter", "Yellowtail", "Montez",
|
||||||
|
"Shadows+Into+Light", "Fredericka+the+Great", "Orbitron", "Dancing+Script:700",
|
||||||
|
"Architects+Daughter", "Kaushan+Script", "Gloria+Hallelujah", "Satisfy", "Comfortaa:700", "Cinzel"];
|
||||||
|
fontsToAdd.forEach(function(f) {if (fonts.indexOf(f) === -1) fonts.push(f);});
|
||||||
|
updateFontOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchFonts(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (url === "") {
|
||||||
|
tip("Use a direct link to any @font-face declaration or just font name to fetch from Google Fonts");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (url.indexOf("http") === -1) {
|
||||||
|
url = url.replace(url.charAt(0), url.charAt(0).toUpperCase()).split(" ").join("+");
|
||||||
|
url = "https://fonts.googleapis.com/css?family=" + url;
|
||||||
|
}
|
||||||
|
const fetched = addFonts(url).then(fetched => {
|
||||||
|
if (fetched === undefined) {
|
||||||
|
tip("Cannot fetch font for this value!", false, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fetched === 0) {
|
||||||
|
tip("Already in the fonts list!", false, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateFontOptions();
|
||||||
|
if (fetched === 1) {
|
||||||
|
tip("Font " + fonts[fonts.length - 1] + " is fetched");
|
||||||
|
} else if (fetched > 1) {
|
||||||
|
tip(fetched + " fonts are added to the list");
|
||||||
|
}
|
||||||
|
resolve(fetched);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addFonts(url) {
|
||||||
|
$("head").append('<link rel="stylesheet" type="text/css" href="' + url + '">');
|
||||||
|
return fetch(url)
|
||||||
|
.then(resp => resp.text())
|
||||||
|
.then(text => {
|
||||||
|
let s = document.createElement('style');
|
||||||
|
s.innerHTML = text;
|
||||||
|
document.head.appendChild(s);
|
||||||
|
let styleSheet = Array.prototype.filter.call(
|
||||||
|
document.styleSheets,
|
||||||
|
sS => sS.ownerNode === s)[0];
|
||||||
|
let FontRule = rule => {
|
||||||
|
let family = rule.style.getPropertyValue('font-family');
|
||||||
|
let font = family.replace(/['"]+/g, '').replace(/ /g, "+");
|
||||||
|
let weight = rule.style.getPropertyValue('font-weight');
|
||||||
|
if (weight !== "400") font += ":" + weight;
|
||||||
|
if (fonts.indexOf(font) == -1) {
|
||||||
|
fonts.push(font);
|
||||||
|
fetched++
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let fetched = 0;
|
||||||
|
for (let r of styleSheet.cssRules) {FontRule(r);}
|
||||||
|
document.head.removeChild(s);
|
||||||
|
return fetched;
|
||||||
|
})
|
||||||
|
.catch(function() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update font list for Label and Burg Editors
|
||||||
|
function updateFontOptions() {
|
||||||
|
styleSelectFont.innerHTML = "";
|
||||||
|
for (let i=0; i < fonts.length; i++) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = i;
|
||||||
|
const font = fonts[i].split(':')[0].replace(/\+/g, " ");
|
||||||
|
opt.style.fontFamily = opt.innerHTML = font;
|
||||||
|
styleSelectFont.add(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sticked menu Options listeners
|
||||||
|
document.getElementById("sticked").addEventListener("click", function(event) {
|
||||||
|
const id = event.target.id;
|
||||||
|
if (id === "newMapButton") regeneratePrompt();
|
||||||
|
else if (id === "saveButton") toggleSavePane();
|
||||||
|
else if (id === "loadMap") mapToLoad.click();
|
||||||
|
else if (id === "zoomReset") resetZoom(1000);
|
||||||
|
else if (id === "saveMap") saveMap();
|
||||||
|
else if (id === "saveSVG") saveAsImage("svg");
|
||||||
|
else if (id === "savePNG") saveAsImage("png");
|
||||||
|
if (id === "saveMap" || id === "saveSVG" || id === "savePNG") toggleSavePane();
|
||||||
|
});
|
||||||
|
|
||||||
|
function regeneratePrompt() {
|
||||||
|
if (customization) {tip("Please exit the customization mode first", false, "warning"); return;}
|
||||||
|
const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes
|
||||||
|
if (workingTime < 15) {regenerateMap(); return;}
|
||||||
|
|
||||||
|
alertMessage.innerHTML = `Are you sure you want to generate a new map?<br>
|
||||||
|
All unsaved changes made to the current map will be lost`;
|
||||||
|
$("#alert").dialog({resizable: false, title: "Generate new map",
|
||||||
|
buttons: {
|
||||||
|
Cancel: function() {$(this).dialog("close");},
|
||||||
|
Generate: regenerateMap
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSavePane() {
|
||||||
|
if (saveDropdown.style.display === "block") {saveDropdown.style.display = "none"; return;}
|
||||||
|
saveDropdown.style.display = "block";
|
||||||
|
|
||||||
|
// ask users to allow popups
|
||||||
|
if (!localStorage.getItem("dns_allow_popup_message")) {
|
||||||
|
alertMessage.innerHTML = `Generator uses pop-up window to download files.
|
||||||
|
<br>Please ensure your browser does not block popups.
|
||||||
|
<br>Please check browser settings and turn off adBlocker if it is enabled`;
|
||||||
|
|
||||||
|
$("#alert").dialog({title: "File saver", resizable: false, position: {my: "center", at: "center", of: "svg"},
|
||||||
|
buttons: {
|
||||||
|
OK: function() {
|
||||||
|
localStorage.setItem("dns_allow_popup_message", true);
|
||||||
|
$(this).dialog("close");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// load map
|
||||||
|
document.getElementById("mapToLoad").addEventListener("change", function() {
|
||||||
|
closeDialogs();
|
||||||
|
const fileToLoad = this.files[0];
|
||||||
|
this.value = "";
|
||||||
|
uploadFile(fileToLoad);
|
||||||
|
});
|
||||||
247
modules/ui/relief-editor.js
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
"use strict";
|
||||||
|
function editReliefIcon() {
|
||||||
|
if (customization) return;
|
||||||
|
closeDialogs(".stable");
|
||||||
|
if (!layerIsOn("toggleRelief")) toggleRelief();
|
||||||
|
|
||||||
|
terrain.selectAll("use").call(d3.drag().on("drag", dragReliefIcon)).classed("draggable", true);
|
||||||
|
elSelected = d3.select(d3.event.target);
|
||||||
|
|
||||||
|
restoreEditMode();
|
||||||
|
updateReliefIconSelected();
|
||||||
|
updateReliefSizeInput();
|
||||||
|
|
||||||
|
$("#reliefEditor").dialog({
|
||||||
|
title: "Edit Relief Icons", resizable: false,
|
||||||
|
position: {my: "center top+40", at: "top", of: d3.event, collision: "fit"},
|
||||||
|
close: closeReliefEditor
|
||||||
|
});
|
||||||
|
|
||||||
|
if (modules.editReliefIcon) return;
|
||||||
|
modules.editReliefIcon = true;
|
||||||
|
|
||||||
|
// add listeners
|
||||||
|
document.getElementById("reliefIndividual").addEventListener("click", enterIndividualMode);
|
||||||
|
document.getElementById("reliefBulkAdd").addEventListener("click", enterBulkAddMode);
|
||||||
|
document.getElementById("reliefBulkRemove").addEventListener("click", enterBulkRemoveMode);
|
||||||
|
|
||||||
|
document.getElementById("reliefSize").addEventListener("input", changeIconSize);
|
||||||
|
document.getElementById("reliefSizeNumber").addEventListener("input", changeIconSize);
|
||||||
|
reliefIconsDiv.querySelectorAll("button").forEach(el => el.addEventListener("click", changeIcon));
|
||||||
|
|
||||||
|
document.getElementById("reliefCopy").addEventListener("click", copyIcon);
|
||||||
|
document.getElementById("reliefMoveFront").addEventListener("click", () => elSelected.raise());
|
||||||
|
document.getElementById("reliefMoveBack").addEventListener("click", () => elSelected.lower());
|
||||||
|
document.getElementById("reliefRemove").addEventListener("click", removeIcon);
|
||||||
|
|
||||||
|
function dragReliefIcon() {
|
||||||
|
const dx = +this.getAttribute("x") - d3.event.x;
|
||||||
|
const dy = +this.getAttribute("y") - d3.event.y;
|
||||||
|
|
||||||
|
d3.event.on("drag", function() {
|
||||||
|
const x = d3.event.x, y = d3.event.y;
|
||||||
|
this.setAttribute("x", dx+x);
|
||||||
|
this.setAttribute("y", dy+y);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreEditMode() {
|
||||||
|
if (!reliefTools.querySelector("button.pressed")) enterIndividualMode(); else
|
||||||
|
if (reliefBulkAdd.classList.contains("pressed")) enterBulkAddMode(); else
|
||||||
|
if (reliefBulkRemove.classList.contains("pressed")) enterBulkRemoveMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateReliefIconSelected() {
|
||||||
|
const type = elSelected.attr("data-type");
|
||||||
|
reliefIconsDiv.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||||
|
reliefIconsDiv.querySelector("button[data-type='"+type+"']").classList.add("pressed");
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateReliefSizeInput() {
|
||||||
|
const size = +elSelected.attr("data-size");
|
||||||
|
reliefSize.value = reliefSizeNumber.value = rn(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterIndividualMode() {
|
||||||
|
reliefTools.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||||
|
reliefIndividual.classList.add("pressed");
|
||||||
|
|
||||||
|
reliefSizeDiv.style.display = "block";
|
||||||
|
reliefRadiusDiv.style.display = "none";
|
||||||
|
reliefSpacingDiv.style.display = "none";
|
||||||
|
reliefIconsSeletionAny.style.display = "none";
|
||||||
|
|
||||||
|
updateReliefSizeInput();
|
||||||
|
restoreDefaultEvents();
|
||||||
|
clearMainTip();
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterBulkAddMode() {
|
||||||
|
reliefTools.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||||
|
reliefBulkAdd.classList.add("pressed");
|
||||||
|
|
||||||
|
reliefSizeDiv.style.display = "block";
|
||||||
|
reliefRadiusDiv.style.display = "block";
|
||||||
|
reliefSpacingDiv.style.display = "block";
|
||||||
|
reliefIconsSeletionAny.style.display = "none";
|
||||||
|
|
||||||
|
const pressedType = reliefIconsDiv.querySelector("button.pressed");
|
||||||
|
if (pressedType.id === "reliefIconsSeletionAny") { // in "any" is pressed, select first type
|
||||||
|
reliefIconsSeletionAny.classList.remove("pressed");
|
||||||
|
reliefIconsDiv.querySelector("button").classList.add("pressed");
|
||||||
|
}
|
||||||
|
|
||||||
|
viewbox.style("cursor", "crosshair").call(d3.drag().on("start", dragToAdd)).on("touchmove mousemove", moveBrush);
|
||||||
|
tip("Drag to place relief icons within radius", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveBrush() {
|
||||||
|
showMainTip();
|
||||||
|
const point = d3.mouse(this);
|
||||||
|
const radius = +reliefRadius.value;
|
||||||
|
moveCircle(point[0], point[1], radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragToAdd() {
|
||||||
|
const pressed = reliefIconsDiv.querySelector("button.pressed");
|
||||||
|
if (!pressed) {tip("Please select an icon", false, error); return;}
|
||||||
|
|
||||||
|
const type = pressed.dataset.type;
|
||||||
|
const r = +reliefRadius.value;
|
||||||
|
const spacing = +reliefSpacing.value;
|
||||||
|
const size = +reliefSize.value;
|
||||||
|
|
||||||
|
// build a quadtree
|
||||||
|
const tree = d3.quadtree();
|
||||||
|
const positions = [];
|
||||||
|
terrain.selectAll("use").each(function() {
|
||||||
|
const x = +this.getAttribute("x") + this.getAttribute("width") / 2;
|
||||||
|
const y = +this.getAttribute("y") + this.getAttribute("height") / 2;
|
||||||
|
tree.add([x, y, x]);
|
||||||
|
const box = this.getBBox();
|
||||||
|
positions.push(box.y + box.height);
|
||||||
|
});
|
||||||
|
|
||||||
|
d3.event.on("drag", function() {
|
||||||
|
const p = d3.mouse(this);
|
||||||
|
moveCircle(p[0], p[1], r);
|
||||||
|
|
||||||
|
d3.range(Math.ceil(r/10)).forEach(function() {
|
||||||
|
const a = Math.PI * 2 * Math.random();
|
||||||
|
const rad = r * Math.random();
|
||||||
|
const cx = p[0] + rad * Math.cos(a);
|
||||||
|
const cy = p[1] + rad * Math.sin(a);
|
||||||
|
|
||||||
|
if (tree.find(cx, cy, spacing)) return; // too close to existing icon
|
||||||
|
if (pack.cells.h[findCell(cx, cy)] < 20) return; // on water cell
|
||||||
|
|
||||||
|
const h = rn(size / 2 * (Math.random() * .4 + .8), 2);
|
||||||
|
const x = rn(cx-h, 2);
|
||||||
|
const y = rn(cy-h, 2);
|
||||||
|
const z = y + h * 2;
|
||||||
|
|
||||||
|
let nth = 1;
|
||||||
|
while (positions[nth] && z > positions[nth]) {nth++;}
|
||||||
|
|
||||||
|
tree.add([cx, cy]);
|
||||||
|
positions.push(z);
|
||||||
|
terrain.insert("use", ":nth-child("+nth+")").attr("xlink:href", type).attr("data-type", type)
|
||||||
|
.attr("x", x).attr("y", y).attr("data-size", h*2).attr("width", h*2).attr("height", h*2);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterBulkRemoveMode() {
|
||||||
|
reliefTools.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||||
|
reliefBulkRemove.classList.add("pressed");
|
||||||
|
|
||||||
|
reliefSizeDiv.style.display = "none";
|
||||||
|
reliefRadiusDiv.style.display = "block";
|
||||||
|
reliefSpacingDiv.style.display = "none";
|
||||||
|
reliefIconsSeletionAny.style.display = "inline-block";
|
||||||
|
|
||||||
|
viewbox.style("cursor", "crosshair").call(d3.drag().on("start", dragToRemove)).on("touchmove mousemove", moveBrush);;
|
||||||
|
tip("Drag to remove relief icons in radius", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragToRemove() {
|
||||||
|
const pressed = reliefIconsDiv.querySelector("button.pressed");
|
||||||
|
if (!pressed) {tip("Please select an icon", false, error); return;}
|
||||||
|
|
||||||
|
const r = +reliefRadius.value;
|
||||||
|
const type = pressed.dataset.type;
|
||||||
|
const icons = type ? terrain.selectAll("use[data-type='"+type+"']") : terrain.selectAll("use");
|
||||||
|
const tree = d3.quadtree();
|
||||||
|
icons.each(function() {
|
||||||
|
const x = +this.getAttribute("x") + this.getAttribute("width") / 2;
|
||||||
|
const y = +this.getAttribute("y") + this.getAttribute("height") / 2;
|
||||||
|
tree.add([x, y, this]);
|
||||||
|
});
|
||||||
|
|
||||||
|
d3.event.on("drag", function() {
|
||||||
|
const p = d3.mouse(this);
|
||||||
|
moveCircle(p[0], p[1], r);
|
||||||
|
tree.findAll(p[0], p[1], r).forEach(f => f[2].remove());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeIconSize() {
|
||||||
|
const size = +reliefSize.value;
|
||||||
|
reliefSize.value = reliefSizeNumber.value = size;
|
||||||
|
if (!reliefIndividual.classList.contains("pressed")) return;
|
||||||
|
|
||||||
|
const shift = (size - +elSelected.attr("width")) / 2;
|
||||||
|
elSelected.attr("width", size).attr("height", size).attr("data-size", size);
|
||||||
|
const x = +elSelected.attr("x"), y = +elSelected.attr("y");
|
||||||
|
elSelected.attr("x", x-shift).attr("y", y-shift);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeIcon() {
|
||||||
|
if (this.classList.contains("pressed")) return;
|
||||||
|
|
||||||
|
reliefIconsDiv.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"))
|
||||||
|
this.classList.add("pressed");
|
||||||
|
|
||||||
|
if (reliefIndividual.classList.contains("pressed")) {
|
||||||
|
const type = this.dataset.type;
|
||||||
|
elSelected.attr("xlink:href", type).attr("data-type", type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyIcon() {
|
||||||
|
const parent = elSelected.node().parentNode;
|
||||||
|
const copy = elSelected.node().cloneNode(true);
|
||||||
|
|
||||||
|
let x = +elSelected.attr("x") - 3, y = +elSelected.attr("y") - 3;
|
||||||
|
while (parent.querySelector("[x='"+x+"']","[x='"+y+"']")) {
|
||||||
|
x -= 3; y -= 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
copy.setAttribute("x", x);
|
||||||
|
copy.setAttribute("y", y);
|
||||||
|
parent.insertBefore(copy, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeIcon() {
|
||||||
|
alertMessage.innerHTML = `Are you sure you want to remove the icon?`;
|
||||||
|
$("#alert").dialog({resizable: false, title: "Remove relief icon",
|
||||||
|
buttons: {
|
||||||
|
Remove: function() {
|
||||||
|
$(this).dialog("close");
|
||||||
|
elSelected.remove();
|
||||||
|
$("#reliefEditor").dialog("close");
|
||||||
|
},
|
||||||
|
Cancel: function() {$(this).dialog("close");}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeReliefEditor() {
|
||||||
|
terrain.selectAll("use").call(d3.drag().on("drag", null)).classed("draggable", false);
|
||||||
|
removeCircle();
|
||||||
|
unselect();
|
||||||
|
clearMainTip();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
278
modules/ui/rivers-editor.js
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
"use strict";
|
||||||
|
function editRiver() {
|
||||||
|
if (customization) return;
|
||||||
|
if (elSelected && d3.event.target.id === elSelected.attr("id")) return;
|
||||||
|
closeDialogs(".stable");
|
||||||
|
if (!layerIsOn("toggleRivers")) toggleRivers();
|
||||||
|
|
||||||
|
const node = d3.event.target;
|
||||||
|
elSelected = d3.select(node).on("click", addInterimControlPoint)
|
||||||
|
.call(d3.drag().on("start", dragRiver)).classed("draggable", true);
|
||||||
|
viewbox.on("touchmove mousemove", showEditorTips);
|
||||||
|
debug.append("g").attr("id", "controlPoints").attr("transform", elSelected.attr("transform"));
|
||||||
|
drawControlPoints(node);
|
||||||
|
updateValues(node);
|
||||||
|
|
||||||
|
$("#riverEditor").dialog({
|
||||||
|
title: "Edit River", resizable: false,
|
||||||
|
position: {my: "center top+20", at: "top", of: d3.event, collision: "fit"},
|
||||||
|
close: closeRiverEditor
|
||||||
|
});
|
||||||
|
|
||||||
|
if (modules.editRiver) return;
|
||||||
|
modules.editRiver = true;
|
||||||
|
|
||||||
|
// add listeners
|
||||||
|
document.getElementById("riverWidthShow").addEventListener("click", showRiverWidth);
|
||||||
|
document.getElementById("riverWidthHide").addEventListener("click", hideRiverWidth);
|
||||||
|
document.getElementById("riverWidthInput").addEventListener("input", changeWidth);
|
||||||
|
document.getElementById("riverIncrement").addEventListener("input", changeIncrement);
|
||||||
|
|
||||||
|
document.getElementById("riverResizeShow").addEventListener("click", showRiverSize);
|
||||||
|
document.getElementById("riverResizeHide").addEventListener("click", hideRiverSize);
|
||||||
|
document.getElementById("riverAngle").addEventListener("input", changeAngle);
|
||||||
|
document.getElementById("riverScale").addEventListener("input", changeScale);
|
||||||
|
document.getElementById("riverReset").addEventListener("click", resetTransformation);
|
||||||
|
|
||||||
|
document.getElementById("riverCopy").addEventListener("click", copyRiver);
|
||||||
|
document.getElementById("riverNew").addEventListener("click", toggleRiverCreationMode);
|
||||||
|
document.getElementById("riverLegend").addEventListener("click", editRiverLegend);
|
||||||
|
document.getElementById("riverRemove").addEventListener("click", removeRiver);
|
||||||
|
|
||||||
|
function showEditorTips() {
|
||||||
|
showMainTip();
|
||||||
|
if (d3.event.target.parentNode.id === elSelected.attr("id")) tip("Drag to move, click to add a control point"); else
|
||||||
|
if (d3.event.target.parentNode.id === "controlPoints") tip("Drag to move, click to delete the control point");
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateValues(node) {
|
||||||
|
const tr = parseTransform(node.getAttribute("transform"));
|
||||||
|
document.getElementById("riverAngle").value = tr[2];
|
||||||
|
document.getElementById("riverAngleValue").innerHTML = Math.abs(+tr[2]) + "°";
|
||||||
|
document.getElementById("riverScale").value = tr[5];
|
||||||
|
document.getElementById("riverWidthInput").value = node.dataset.width;
|
||||||
|
document.getElementById("riverIncrement").value = node.dataset.increment;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragRiver() {
|
||||||
|
const x = d3.event.x, y = d3.event.y;
|
||||||
|
const tr = parseTransform(elSelected.attr("transform"));
|
||||||
|
|
||||||
|
d3.event.on("drag", function() {
|
||||||
|
let xc = d3.event.x, yc = d3.event.y;
|
||||||
|
let transform = `translate(${(+tr[0]+xc-x)},${(+tr[1]+yc-y)}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
|
||||||
|
elSelected.attr("transform", transform);
|
||||||
|
debug.select("#controlPoints").attr("transform", transform);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawControlPoints(node) {
|
||||||
|
const l = node.getTotalLength() / 2;
|
||||||
|
const segments = Math.ceil(l / 5);
|
||||||
|
const increment = rn(l / segments * 1e5);
|
||||||
|
for (let i=increment*segments, c=i; i >= 0; i -= increment, c += increment) {
|
||||||
|
const p1 = node.getPointAtLength(i / 1e5);
|
||||||
|
const p2 = node.getPointAtLength(c / 1e5);
|
||||||
|
addControlPoint([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2]);
|
||||||
|
}
|
||||||
|
updateRiverLength(l);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addControlPoint(point) {
|
||||||
|
debug.select("#controlPoints").append("circle")
|
||||||
|
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .5)
|
||||||
|
.call(d3.drag().on("drag", dragControlPoint))
|
||||||
|
.on("click", clickControlPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragControlPoint() {
|
||||||
|
this.setAttribute("cx", d3.event.x);
|
||||||
|
this.setAttribute("cy", d3.event.y);
|
||||||
|
redrawRiver();
|
||||||
|
}
|
||||||
|
|
||||||
|
function redrawRiver() {
|
||||||
|
const points = [];
|
||||||
|
debug.select("#controlPoints").selectAll("circle").each(function() {
|
||||||
|
points.push([+this.getAttribute("cx"), +this.getAttribute("cy")]);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (points.length === 1) return;
|
||||||
|
if (points.length === 2) {elSelected.attr("d", `M${points[0][0]},${points[0][1]} L${points[1][0]},${points[1][1]}`); return;}
|
||||||
|
const d = Rivers.getPath(points, +riverWidthInput.value, +riverIncrement.value);
|
||||||
|
elSelected.attr("d", d);
|
||||||
|
updateRiverLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRiverLength(l = elSelected.node().getTotalLength() / 2) {
|
||||||
|
const tr = parseTransform(elSelected.attr("transform"));
|
||||||
|
riverLength.innerHTML = rn(l * tr[5] * distanceScale.value) + " " + distanceUnit.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickControlPoint() {
|
||||||
|
this.remove();
|
||||||
|
redrawRiver();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addInterimControlPoint() {
|
||||||
|
const point = d3.mouse(this);
|
||||||
|
|
||||||
|
const dists = [];
|
||||||
|
debug.select("#controlPoints").selectAll("circle").each(function() {
|
||||||
|
const x = +this.getAttribute("cx");
|
||||||
|
const y = +this.getAttribute("cy");
|
||||||
|
dists.push((point[0] - x) ** 2 + (point[1] - y) ** 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
let index = dists.length;
|
||||||
|
if (dists.length > 1) {
|
||||||
|
const sorted = dists.slice(0).sort((a, b) => a-b);
|
||||||
|
const closest = dists.indexOf(sorted[0]);
|
||||||
|
const next = dists.indexOf(sorted[1]);
|
||||||
|
if (closest <= next) index = closest+1; else index = next+1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const before = ":nth-child(" + (index + 1) + ")";
|
||||||
|
debug.select("#controlPoints").insert("circle", before)
|
||||||
|
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .5)
|
||||||
|
.call(d3.drag().on("drag", dragControlPoint))
|
||||||
|
.on("click", clickControlPoint);
|
||||||
|
|
||||||
|
redrawRiver();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showRiverWidth() {
|
||||||
|
document.querySelectorAll("#riverEditor > button").forEach(el => el.style.display = "none");
|
||||||
|
document.getElementById("riverWidthSection").style.display = "inline-block";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideRiverWidth() {
|
||||||
|
document.querySelectorAll("#riverEditor > button").forEach(el => el.style.display = "inline-block");
|
||||||
|
document.getElementById("riverWidthSection").style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeWidth() {
|
||||||
|
elSelected.attr("data-width", this.value);
|
||||||
|
redrawRiver();
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeIncrement() {
|
||||||
|
elSelected.attr("data-increment", this.value);
|
||||||
|
redrawRiver();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showRiverSize() {
|
||||||
|
document.querySelectorAll("#riverEditor > button").forEach(el => el.style.display = "none");
|
||||||
|
document.getElementById("riverResizeSection").style.display = "inline-block";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideRiverSize() {
|
||||||
|
document.querySelectorAll("#riverEditor > button").forEach(el => el.style.display = "inline-block");
|
||||||
|
document.getElementById("riverResizeSection").style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeAngle() {
|
||||||
|
const tr = parseTransform(elSelected.attr("transform"));
|
||||||
|
riverAngleValue.innerHTML = Math.abs(+this.value) + "°";
|
||||||
|
const c = elSelected.node().getBBox();
|
||||||
|
const angle = +this.value, scale = +tr[5];
|
||||||
|
const transform = `translate(${tr[0]},${tr[1]}) rotate(${angle} ${(c.x+c.width/2)*scale} ${(c.y+c.height/2)*scale}) scale(${scale})`;
|
||||||
|
elSelected.attr("transform", transform);
|
||||||
|
debug.select("#controlPoints").attr("transform", transform);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeScale() {
|
||||||
|
const tr = parseTransform(elSelected.attr("transform"));
|
||||||
|
const scaleOld = +tr[5],scale = +this.value;
|
||||||
|
const c = elSelected.node().getBBox();
|
||||||
|
const cx = c.x + c.width / 2, cy = c.y + c.height / 2;
|
||||||
|
const trX = +tr[0] + cx * (scaleOld - scale);
|
||||||
|
const trY = +tr[1] + cy * (scaleOld - scale);
|
||||||
|
const scX = +tr[3] * scale / scaleOld;
|
||||||
|
const scY = +tr[4] * scale / scaleOld;
|
||||||
|
const transform = `translate(${trX},${trY}) rotate(${tr[2]} ${scX} ${scY}) scale(${scale})`;
|
||||||
|
elSelected.attr("transform", transform);
|
||||||
|
debug.select("#controlPoints").attr("transform", transform);
|
||||||
|
updateRiverLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetTransformation() {
|
||||||
|
elSelected.attr("transform", null);
|
||||||
|
debug.select("#controlPoints").attr("transform", null);
|
||||||
|
riverAngle.value = 0;
|
||||||
|
riverAngleValue.innerHTML = "0°";
|
||||||
|
riverScale.value = 1;
|
||||||
|
updateRiverLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyRiver() {
|
||||||
|
const tr = parseTransform(elSelected.attr("transform"));
|
||||||
|
const d = elSelected.attr("d");
|
||||||
|
let x = 2, y = 2;
|
||||||
|
let transform = `translate(${tr[0]-x},${tr[1]-y}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
|
||||||
|
while (rivers.selectAll("[transform='" + transform + "'][d='" + d + "']").size() > 0) {
|
||||||
|
x += 2; y += 2;
|
||||||
|
transform = `translate(${tr[0]-x},${tr[1]-y}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
rivers.append("path").attr("d", d).attr("transform", transform).attr("id", getNextId("river"))
|
||||||
|
.attr("data-width", elSelected.attr("data-width")).attr("data-increment", elSelected.attr("data-increment"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRiverCreationMode() {
|
||||||
|
document.getElementById("riverNew").classList.toggle("pressed");
|
||||||
|
if (document.getElementById("riverNew").classList.contains("pressed")) {
|
||||||
|
tip("Click on map to add control points", true);
|
||||||
|
viewbox.on("click", addPointOnClick).style("cursor", "crosshair");
|
||||||
|
elSelected.on("click", null);
|
||||||
|
} else {
|
||||||
|
clearMainTip();
|
||||||
|
viewbox.on("click", clicked).style("cursor", "default");
|
||||||
|
elSelected.on("click", addInterimControlPoint).attr("data-new", null)
|
||||||
|
.call(d3.drag().on("start", dragRiver)).classed("draggable", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPointOnClick() {
|
||||||
|
if (!elSelected.attr("data-new")) {
|
||||||
|
debug.select("#controlPoints").selectAll("circle").remove();
|
||||||
|
const id = getNextId("river");
|
||||||
|
elSelected = d3.select(elSelected.node().parentNode).append("path").attr("id", id)
|
||||||
|
.attr("data-new", 1).attr("data-width", 2).attr("data-increment", 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add control point
|
||||||
|
const point = d3.mouse(this);
|
||||||
|
addControlPoint([point[0], point[1]]);
|
||||||
|
redrawRiver();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function editRiverLegend() {
|
||||||
|
const id = elSelected.attr("id");
|
||||||
|
editLegends(id, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRiver() {
|
||||||
|
alertMessage.innerHTML = "Are you sure you want to remove the river?";
|
||||||
|
$("#alert").dialog({resizable: false, title: "Remove river",
|
||||||
|
buttons: {
|
||||||
|
Remove: function() {
|
||||||
|
$(this).dialog("close");
|
||||||
|
elSelected.remove();
|
||||||
|
$("#riverEditor").dialog("close");
|
||||||
|
},
|
||||||
|
Cancel: function() {$(this).dialog("close");}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeRiverEditor() {
|
||||||
|
elSelected.attr("data-new", null).on("click", null);
|
||||||
|
clearMainTip();
|
||||||
|
riverNew.classList.remove("pressed");
|
||||||
|
debug.select("#controlPoints").remove();
|
||||||
|
unselect();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
284
modules/ui/routes-editor.js
Normal file
|
|
@ -0,0 +1,284 @@
|
||||||
|
"use strict";
|
||||||
|
function editRoute(onClick) {
|
||||||
|
if (customization) return;
|
||||||
|
if (!onClick && elSelected && d3.event.target.id === elSelected.attr("id")) return;
|
||||||
|
closeDialogs(".stable");
|
||||||
|
if (!layerIsOn("toggleRoutes")) toggleRoutes();
|
||||||
|
|
||||||
|
$("#routeEditor").dialog({
|
||||||
|
title: "Edit Route", resizable: false,
|
||||||
|
position: {my: "center top+20", at: "top", of: d3.event, collision: "fit"},
|
||||||
|
close: closeRoutesEditor
|
||||||
|
});
|
||||||
|
|
||||||
|
debug.append("g").attr("id", "controlPoints");
|
||||||
|
const node = onClick ? elSelected.node() : d3.event.target;
|
||||||
|
elSelected = d3.select(node).on("click", addInterimControlPoint);
|
||||||
|
drawControlPoints(node);
|
||||||
|
selectRouteGroup(node);
|
||||||
|
|
||||||
|
viewbox.on("touchmove mousemove", showEditorTips);
|
||||||
|
if (onClick) toggleRouteCreationMode();
|
||||||
|
|
||||||
|
if (modules.editRoute) return;
|
||||||
|
modules.editRoute = true;
|
||||||
|
|
||||||
|
// add listeners
|
||||||
|
document.getElementById("routeGroupsShow").addEventListener("click", showGroupSection);
|
||||||
|
document.getElementById("routeGroup").addEventListener("change", changeRouteGroup);
|
||||||
|
document.getElementById("routeGroupAdd").addEventListener("click", toggleNewGroupInput);
|
||||||
|
document.getElementById("routeGroupName").addEventListener("change", createNewGroup);
|
||||||
|
document.getElementById("routeGroupRemove").addEventListener("click", removeRouteGroup);
|
||||||
|
document.getElementById("routeGroupsHide").addEventListener("click", hideGroupSection);
|
||||||
|
document.getElementById("routeSplit").addEventListener("click", toggleRouteSplitMode);
|
||||||
|
document.getElementById("routeLegend").addEventListener("click", editRouteLegend);
|
||||||
|
document.getElementById("routeNew").addEventListener("click", toggleRouteCreationMode);
|
||||||
|
document.getElementById("routeRemove").addEventListener("click", removeRoute);
|
||||||
|
|
||||||
|
function showEditorTips() {
|
||||||
|
showMainTip();
|
||||||
|
if (routeNew.classList.contains("pressed")) return;
|
||||||
|
if (d3.event.target.id === elSelected.attr("id")) tip("Click to add a control point"); else
|
||||||
|
if (d3.event.target.parentNode.id === "controlPoints") tip("Drag to move, click to delete the control point");
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawControlPoints(node) {
|
||||||
|
const l = node.getTotalLength();
|
||||||
|
const increment = l / Math.ceil(l / 5);
|
||||||
|
for (let i=0; i <= l; i += increment) {addControlPoint(node.getPointAtLength(i));}
|
||||||
|
routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addControlPoint(point) {
|
||||||
|
debug.select("#controlPoints").append("circle")
|
||||||
|
.attr("cx", point.x).attr("cy", point.y).attr("r", .5)
|
||||||
|
.call(d3.drag().on("drag", dragControlPoint))
|
||||||
|
.on("click", clickControlPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addInterimControlPoint() {
|
||||||
|
const point = d3.mouse(this);
|
||||||
|
|
||||||
|
const dists = [];
|
||||||
|
debug.select("#controlPoints").selectAll("circle").each(function() {
|
||||||
|
const x = +this.getAttribute("cx");
|
||||||
|
const y = +this.getAttribute("cy");
|
||||||
|
dists.push((point[0] - x) ** 2 + (point[1] - y) ** 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
let index = dists.length;
|
||||||
|
if (dists.length > 1) {
|
||||||
|
const sorted = dists.slice(0).sort((a, b) => a-b);
|
||||||
|
const closest = dists.indexOf(sorted[0]);
|
||||||
|
const next = dists.indexOf(sorted[1]);
|
||||||
|
if (closest <= next) index = closest+1; else index = next+1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const before = ":nth-child(" + (index + 1) + ")";
|
||||||
|
debug.select("#controlPoints").insert("circle", before)
|
||||||
|
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .5)
|
||||||
|
.call(d3.drag().on("drag", dragControlPoint))
|
||||||
|
.on("click", clickControlPoint);
|
||||||
|
|
||||||
|
redrawRoute();
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragControlPoint() {
|
||||||
|
this.setAttribute("cx", d3.event.x);
|
||||||
|
this.setAttribute("cy", d3.event.y);
|
||||||
|
redrawRoute();
|
||||||
|
}
|
||||||
|
|
||||||
|
function redrawRoute() {
|
||||||
|
lineGen.curve(d3.curveCatmullRom.alpha(.1));
|
||||||
|
const points = [];
|
||||||
|
debug.select("#controlPoints").selectAll("circle").each(function() {
|
||||||
|
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
|
||||||
|
});
|
||||||
|
|
||||||
|
elSelected.attr("d", round(lineGen(points)));
|
||||||
|
const l = elSelected.node().getTotalLength();
|
||||||
|
routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showGroupSection() {
|
||||||
|
document.querySelectorAll("#routeEditor > button").forEach(el => el.style.display = "none");
|
||||||
|
document.getElementById("routeGroupsSelection").style.display = "inline-block";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideGroupSection() {
|
||||||
|
document.querySelectorAll("#routeEditor > button").forEach(el => el.style.display = "inline-block");
|
||||||
|
document.getElementById("routeGroupsSelection").style.display = "none";
|
||||||
|
document.getElementById("routeGroupName").style.display = "none";
|
||||||
|
document.getElementById("routeGroupName").value = "";
|
||||||
|
document.getElementById("routeGroup").style.display = "inline-block";
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectRouteGroup(node) {
|
||||||
|
const group = node.parentNode.id;
|
||||||
|
const select = document.getElementById("routeGroup");
|
||||||
|
select.options.length = 0; // remove all options
|
||||||
|
|
||||||
|
routes.selectAll("g").each(function() {
|
||||||
|
select.options.add(new Option(this.id, this.id, false, this.id === group));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeRouteGroup() {
|
||||||
|
document.getElementById(this.value).appendChild(elSelected.node());
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleNewGroupInput() {
|
||||||
|
if (routeGroupName.style.display === "none") {
|
||||||
|
routeGroupName.style.display = "inline-block";
|
||||||
|
routeGroupName.focus();
|
||||||
|
routeGroup.style.display = "none";
|
||||||
|
} else {
|
||||||
|
routeGroupName.style.display = "none";
|
||||||
|
routeGroup.style.display = "inline-block";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNewGroup() {
|
||||||
|
if (!this.value) {tip("Please provide a valid group name"); return;}
|
||||||
|
let group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
|
||||||
|
if (Number.isFinite(+group.charAt(0))) group = "g" + group;
|
||||||
|
|
||||||
|
if (document.getElementById(group)) {
|
||||||
|
tip("Element with this id already exists. Please provide a unique name", false, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// just rename if only 1 element left
|
||||||
|
const oldGroup = elSelected.node().parentNode;
|
||||||
|
const basic = ["roads", "trails", "searoutes"].includes(oldGroup.id);
|
||||||
|
if (!basic && oldGroup.childElementCount === 1) {
|
||||||
|
document.getElementById("routeGroup").selectedOptions[0].remove();
|
||||||
|
document.getElementById("routeGroup").options.add(new Option(group, group, false, true));
|
||||||
|
oldGroup.id = group;
|
||||||
|
toggleNewGroupInput();
|
||||||
|
document.getElementById("routeGroupName").value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newGroup = elSelected.node().parentNode.cloneNode(false);
|
||||||
|
document.getElementById("routes").appendChild(newGroup);
|
||||||
|
newGroup.id = group;
|
||||||
|
document.getElementById("routeGroup").options.add(new Option(group, group, false, true));
|
||||||
|
document.getElementById(group).appendChild(elSelected.node());
|
||||||
|
|
||||||
|
toggleNewGroupInput();
|
||||||
|
document.getElementById("routeGroupName").value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRouteGroup() {
|
||||||
|
const group = elSelected.node().parentNode.id;
|
||||||
|
const basic = ["roads", "trails", "searoutes"].includes(group);
|
||||||
|
const count = elSelected.node().parentNode.childElementCount;
|
||||||
|
alertMessage.innerHTML = `Are you sure you want to remove
|
||||||
|
${basic ? "all elements in the group" : "the entire route group"}?
|
||||||
|
<br><br>Routes to be removed: ${count}`;
|
||||||
|
$("#alert").dialog({resizable: false, title: "Remove route group",
|
||||||
|
buttons: {
|
||||||
|
Remove: function() {
|
||||||
|
$(this).dialog("close");
|
||||||
|
$("#routeEditor").dialog("close");
|
||||||
|
hideGroupSection();
|
||||||
|
if (basic) routes.select("#"+group).selectAll("path").remove();
|
||||||
|
else routes.select("#"+group).remove();
|
||||||
|
},
|
||||||
|
Cancel: function() {$(this).dialog("close");}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRouteSplitMode() {
|
||||||
|
document.getElementById("routeNew").classList.remove("pressed");
|
||||||
|
this.classList.toggle("pressed");
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickControlPoint() {
|
||||||
|
if (routeSplit.classList.contains("pressed")) splitRoute(this);
|
||||||
|
else {this.remove(); redrawRoute();}
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitRoute(clicked) {
|
||||||
|
lineGen.curve(d3.curveCatmullRom.alpha(.1));
|
||||||
|
const group = d3.select(elSelected.node().parentNode);
|
||||||
|
routeSplit.classList.remove("pressed");
|
||||||
|
|
||||||
|
const points1 = [], points2 = [];
|
||||||
|
let points = points1;
|
||||||
|
debug.select("#controlPoints").selectAll("circle").each(function() {
|
||||||
|
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
|
||||||
|
if (this === clicked) {
|
||||||
|
points = points2;
|
||||||
|
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
|
||||||
|
}
|
||||||
|
this.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
elSelected.attr("d", round(lineGen(points1)));
|
||||||
|
const id = getNextId("route");
|
||||||
|
group.append("path").attr("id", id).attr("d", lineGen(points2));
|
||||||
|
debug.select("#controlPoints").selectAll("circle").remove();
|
||||||
|
drawControlPoints(elSelected.node());
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRouteCreationMode() {
|
||||||
|
document.getElementById("routeSplit").classList.remove("pressed");
|
||||||
|
document.getElementById("routeNew").classList.toggle("pressed");
|
||||||
|
if (document.getElementById("routeNew").classList.contains("pressed")) {
|
||||||
|
tip("Click on map to add control points", true);
|
||||||
|
viewbox.on("click", addPointOnClick).style("cursor", "crosshair");
|
||||||
|
elSelected.on("click", null);
|
||||||
|
} else {
|
||||||
|
clearMainTip();
|
||||||
|
viewbox.on("click", clicked).style("cursor", "default");
|
||||||
|
elSelected.on("click", addInterimControlPoint).attr("data-new", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPointOnClick() {
|
||||||
|
// create new route
|
||||||
|
if (!elSelected.attr("data-new")) {
|
||||||
|
debug.select("#controlPoints").selectAll("circle").remove();
|
||||||
|
const parent = elSelected.node().parentNode;
|
||||||
|
const id = getNextId("route");
|
||||||
|
elSelected = d3.select(parent).append("path").attr("id", id).attr("data-new", 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add control point
|
||||||
|
const point = d3.mouse(this);
|
||||||
|
addControlPoint({x: point[0], y: point[1]});
|
||||||
|
redrawRoute();
|
||||||
|
}
|
||||||
|
|
||||||
|
function editRouteLegend() {
|
||||||
|
const id = elSelected.attr("id");
|
||||||
|
editLegends(id, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRoute() {
|
||||||
|
alertMessage.innerHTML = "Are you sure you want to remove the route?";
|
||||||
|
$("#alert").dialog({resizable: false, title: "Remove route",
|
||||||
|
buttons: {
|
||||||
|
Remove: function() {
|
||||||
|
$(this).dialog("close");
|
||||||
|
elSelected.remove();
|
||||||
|
$("#routeEditor").dialog("close");
|
||||||
|
},
|
||||||
|
Cancel: function() {$(this).dialog("close");}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeRoutesEditor() {
|
||||||
|
elSelected.attr("data-new", null).on("click", null);
|
||||||
|
clearMainTip();
|
||||||
|
routeSplit.classList.remove("pressed");
|
||||||
|
routeNew.classList.remove("pressed");
|
||||||
|
debug.select("#controlPoints").remove();
|
||||||
|
unselect();
|
||||||
|
}
|
||||||
|
}
|
||||||
514
modules/ui/states-editor.js
Normal file
|
|
@ -0,0 +1,514 @@
|
||||||
|
"use strict";
|
||||||
|
function editStates() {
|
||||||
|
if (customization) return;
|
||||||
|
closeDialogs("#statesEditor, .stable");
|
||||||
|
if (!layerIsOn("toggleStates")) toggleStates();
|
||||||
|
if (!layerIsOn("toggleBorders")) toggleBorders();
|
||||||
|
if (layerIsOn("toggleCultures")) toggleCultures();
|
||||||
|
if (layerIsOn("toggleBiomes")) toggleBiomes();
|
||||||
|
|
||||||
|
const body = document.getElementById("statesBodySection");
|
||||||
|
refreshStatesEditor();
|
||||||
|
|
||||||
|
if (modules.editStates) return;
|
||||||
|
modules.editStates = true;
|
||||||
|
|
||||||
|
$("#statesEditor").dialog({
|
||||||
|
title: "States Editor", width: fitContent(), close: closeStatesEditor,
|
||||||
|
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
|
||||||
|
});
|
||||||
|
|
||||||
|
// add listeners
|
||||||
|
document.getElementById("statesEditorRefresh").addEventListener("click", refreshStatesEditor);
|
||||||
|
document.getElementById("statesPercentage").addEventListener("click", togglePercentageMode);
|
||||||
|
document.getElementById("regenerateStateNames").addEventListener("click", regenerateNames);
|
||||||
|
document.getElementById("statesRegenerate").addEventListener("click", openRegenerationMenu);
|
||||||
|
document.getElementById("statesRegenerateBack").addEventListener("click", exitRegenerationMenu);
|
||||||
|
document.getElementById("statesRecalculate").addEventListener("click", recalculateStates);
|
||||||
|
document.getElementById("statesJustify").addEventListener("click", justifyStates);
|
||||||
|
document.getElementById("statesRandomize").addEventListener("click", randomizeStatesExpansion);
|
||||||
|
document.getElementById("statesNeutral").addEventListener("input", recalculateStates);
|
||||||
|
document.getElementById("statesNeutralNumber").addEventListener("click", recalculateStates);
|
||||||
|
document.getElementById("statesManually").addEventListener("click", enterStatesManualAssignent);
|
||||||
|
document.getElementById("statesManuallyApply").addEventListener("click", applyStatesManualAssignent);
|
||||||
|
document.getElementById("statesManuallyCancel").addEventListener("click", exitStatesManualAssignment);
|
||||||
|
document.getElementById("statesAdd").addEventListener("click", enterAddStateMode);
|
||||||
|
document.getElementById("statesExport").addEventListener("click", downloadStatesData);
|
||||||
|
|
||||||
|
function refreshStatesEditor() {
|
||||||
|
statesCollectStatistics();
|
||||||
|
statesEditorAddLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
function statesCollectStatistics() {
|
||||||
|
const cells = pack.cells, states = pack.states;
|
||||||
|
states.forEach(s => s.cells = s.area = s.burgs = s.rural = s.urban = 0);
|
||||||
|
|
||||||
|
for (const i of cells.i) {
|
||||||
|
if (cells.h[i] < 20) continue;
|
||||||
|
const s = cells.state[i];
|
||||||
|
states[s].cells += 1;
|
||||||
|
states[s].area += cells.area[i];
|
||||||
|
states[s].rural += cells.pop[i];
|
||||||
|
if (cells.burg[i]) {
|
||||||
|
states[s].urban += pack.burgs[cells.burg[i]].population;
|
||||||
|
states[s].burgs++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add line for each state
|
||||||
|
function statesEditorAddLines() {
|
||||||
|
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
|
||||||
|
const hidden = statesRegenerateButtons.style.display === "block" ? "visible" : "hidden"; // show/hide regenerate columns
|
||||||
|
let lines = "", totalArea = 0, totalPopulation = 0, totalBurgs = 0;
|
||||||
|
|
||||||
|
for (const s of pack.states) {
|
||||||
|
if (s.removed) continue;
|
||||||
|
const area = s.area * (distanceScale.value ** 2);
|
||||||
|
const rural = s.rural * populationRate.value;
|
||||||
|
const urban = s.urban * populationRate.value * urbanization.value;
|
||||||
|
const population = rural + urban;
|
||||||
|
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
|
||||||
|
totalArea += area;
|
||||||
|
totalPopulation += population;
|
||||||
|
totalBurgs += s.burgs;
|
||||||
|
|
||||||
|
if (!s.i) {
|
||||||
|
// Neutral line
|
||||||
|
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" data-cells=${s.cells} data-area=${area}
|
||||||
|
data-population=${population} data-burgs=${s.burgs} data-color="" data-capital="" data-culture="" data-type="" data-expansionism="">
|
||||||
|
<input class="stateColor placeholder" type="color">
|
||||||
|
<input data-tip="State name. Click and type to change" class="stateName italic" value="${s.name}" autocorrect="off" spellcheck="false">
|
||||||
|
<span class="icon-star-empty placeholder"></span>
|
||||||
|
<input class="stateCapital placeholder">
|
||||||
|
<select class="stateCulture placeholder">${getCultureOptions(0)}</select>
|
||||||
|
<select class="cultureType ${hidden} placeholder">${getTypeOptions(0)}</select>
|
||||||
|
<span class="icon-resize-full ${hidden} placeholder"></span>
|
||||||
|
<input class="statePower ${hidden} placeholder" type="number" value=0>
|
||||||
|
<span data-tip="Cells count" class="icon-check-empty"></span>
|
||||||
|
<div data-tip="Cells count" class="stateCells">${s.cells}</div>
|
||||||
|
<span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled"></span>
|
||||||
|
<div data-tip="Burgs count" class="stateBurgs">${s.burgs}</div>
|
||||||
|
<span data-tip="State area" style="padding-right: 4px" class="icon-map-o"></span>
|
||||||
|
<div data-tip="State area" class="biomeArea">${si(area) + unit}</div>
|
||||||
|
<span data-tip="${populationTip}" class="icon-male"></span>
|
||||||
|
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
|
||||||
|
</div>`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const capital = pack.burgs[s.capital].name;
|
||||||
|
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" data-capital="${capital}" data-color="${s.color}" data-cells=${s.cells}
|
||||||
|
data-area=${area} data-population=${population} data-burgs=${s.burgs} data-culture=${pack.cultures[s.culture].name} data-type=${s.type} data-expansionism=${s.expansionism}>
|
||||||
|
<input data-tip="State color. Click to change" class="stateColor" type="color" value="${s.color}">
|
||||||
|
<input data-tip="State name. Click and type to change" class="stateName" value="${s.name}" autocorrect="off" spellcheck="false">
|
||||||
|
<span data-tip="State capital. Click to zoom into view" class="icon-star-empty pointer"></span>
|
||||||
|
<input data-tip="Capital name. Click and type to rename" class="stateCapital" value="${capital}" autocorrect="off" spellcheck="false"/>
|
||||||
|
<select data-tip="Dominant culture. Click to change" class="stateCulture">${getCultureOptions(s.culture)}</select>
|
||||||
|
<select data-tip="State type. Click to change" class="cultureType ${hidden}">${getTypeOptions(s.type)}</select>
|
||||||
|
<span data-tip="State expansionism" class="icon-resize-full ${hidden}"></span>
|
||||||
|
<input data-tip="Expansionism (defines competitive size). Change to re-calculate states based on new value" class="statePower ${hidden}" type="number" min=0 max=99 step=.1 value=${s.expansionism}>
|
||||||
|
<span data-tip="Cells count" class="icon-check-empty"></span>
|
||||||
|
<div data-tip="Cells count" class="stateCells">${s.cells}</div>
|
||||||
|
<span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled"></span>
|
||||||
|
<div data-tip="Burgs count" class="stateBurgs">${s.burgs}</div>
|
||||||
|
<span data-tip="State area" style="padding-right: 4px" class="icon-map-o"></span>
|
||||||
|
<div data-tip="State area" class="biomeArea">${si(area) + unit}</div>
|
||||||
|
<span data-tip="${populationTip}" class="icon-male"></span>
|
||||||
|
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
|
||||||
|
<span data-tip="Remove state" class="icon-trash-empty"></span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
body.innerHTML = lines;
|
||||||
|
|
||||||
|
// update footer
|
||||||
|
statesFooterStates.innerHTML = pack.states.filter(s => s.i && !s.removed).length;
|
||||||
|
statesFooterCells.innerHTML = pack.cells.h.filter(h => h >= 20).length;
|
||||||
|
statesFooterBurgs.innerHTML = totalBurgs;
|
||||||
|
statesFooterArea.innerHTML = si(totalArea) + unit;
|
||||||
|
statesFooterPopulation.innerHTML = si(totalPopulation);
|
||||||
|
statesFooterArea.dataset.area = totalArea;
|
||||||
|
statesFooterPopulation.dataset.population = totalPopulation;
|
||||||
|
|
||||||
|
// add listeners
|
||||||
|
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev)));
|
||||||
|
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => stateHighlightOff(ev)));
|
||||||
|
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectStateOnLineClick));
|
||||||
|
body.querySelectorAll("div > input.stateColor").forEach(el => el.addEventListener("input", stateChangeColor));
|
||||||
|
body.querySelectorAll("div > input.stateName").forEach(el => el.addEventListener("input", stateChangeName));
|
||||||
|
body.querySelectorAll("div > input.stateCapital").forEach(el => el.addEventListener("input", stateChangeCapitalName));
|
||||||
|
body.querySelectorAll("div > span.icon-star-empty").forEach(el => el.addEventListener("click", stateCapitalZoomIn));
|
||||||
|
body.querySelectorAll("div > select.stateCulture").forEach(el => el.addEventListener("click", stateUpdateCulturesList));
|
||||||
|
body.querySelectorAll("div > select.cultureType").forEach(el => el.addEventListener("input", stateChangeType));
|
||||||
|
body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", stateChangeExpansionism));
|
||||||
|
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", stateRemove));
|
||||||
|
|
||||||
|
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
|
||||||
|
applySorting(statesHeader);
|
||||||
|
$("#statesEditor").dialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCultureOptions(culture) {
|
||||||
|
let options = "";
|
||||||
|
pack.cultures.slice(1).forEach(c => options += `<option ${c.i === culture ? "selected" : ""} value="${c.i}">${c.name}</option>`);
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTypeOptions(type) {
|
||||||
|
let options = "";
|
||||||
|
const types = ["Generic", "River", "Lake", "Naval", "Nomadic", "Hunting", "Highland"];
|
||||||
|
types.forEach(t => options += `<option ${type === t ? "selected" : ""} value="${t}">${t}</option>`);
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateHighlightOn(event) {
|
||||||
|
if (!layerIsOn("toggleStates")) return;
|
||||||
|
const state = +event.target.dataset.id;
|
||||||
|
if (customization || !state) return;
|
||||||
|
const path = regions.select("#state"+state).attr("d");
|
||||||
|
debug.append("path").attr("class", "highlighted").attr("d", path)
|
||||||
|
.attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1)
|
||||||
|
.attr("filter", "url(#blur1)").call(transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
function transition(path) {
|
||||||
|
const duration = (path.node().getTotalLength() + 5000) / 2;
|
||||||
|
path.transition().duration(duration).attrTween("stroke-dasharray", tweenDash);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tweenDash() {
|
||||||
|
const l = this.getTotalLength();
|
||||||
|
const i = d3.interpolateString("0," + l, l + "," + l);
|
||||||
|
return t => i(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePath(path) {
|
||||||
|
path.transition().duration(1000).attr("opacity", 0).remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateHighlightOff() {
|
||||||
|
debug.selectAll(".highlighted").each(function(el) {
|
||||||
|
d3.select(this).call(removePath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateChangeColor() {
|
||||||
|
const state = +this.parentNode.dataset.id;
|
||||||
|
pack.states[state].color = this.value;
|
||||||
|
regions.select("#state"+state).attr("fill", this.value);
|
||||||
|
regions.select("#state-gap"+state).attr("stroke", this.value);
|
||||||
|
regions.select("#state-border"+state).attr("stroke", d3.color(this.value).darker().hex());
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateChangeName() {
|
||||||
|
const state = +this.parentNode.dataset.id;
|
||||||
|
this.parentNode.dataset.name = this.value;
|
||||||
|
pack.states[state].name = this.value;
|
||||||
|
document.querySelector("#stateLabel"+state+" > textPath").textContent = this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateChangeCapitalName() {
|
||||||
|
const state = +this.parentNode.dataset.id;
|
||||||
|
this.parentNode.dataset.capital = this.value;
|
||||||
|
const capital = pack.states[state].capital;
|
||||||
|
if (!capital) return;
|
||||||
|
pack.burgs[capital].name = this.value;
|
||||||
|
document.querySelector("#burgLabel"+capital).textContent = this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateCapitalZoomIn() {
|
||||||
|
const state = +this.parentNode.dataset.id;
|
||||||
|
const capital = pack.states[state].capital;
|
||||||
|
const l = burgLabels.select("[data-id='" + capital + "']");
|
||||||
|
const x = +l.attr("x"), y = +l.attr("y");
|
||||||
|
zoomTo(x, y, 8, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateUpdateCulturesList() {
|
||||||
|
const state = +this.parentNode.dataset.id;
|
||||||
|
const v = +this.value;
|
||||||
|
this.parentNode.dataset.base = pack.states[state].culture = v;
|
||||||
|
this.options.length = 0;
|
||||||
|
pack.cultures.slice(1).forEach(c => this.options.add(new Option(c.name, c.i, false, c.i === v)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateChangeType() {
|
||||||
|
const state = +this.parentNode.dataset.id;
|
||||||
|
this.parentNode.dataset.type = this.value;
|
||||||
|
pack.states[state].type = this.value;
|
||||||
|
recalculateStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateChangeExpansionism() {
|
||||||
|
const state = +this.parentNode.dataset.id;
|
||||||
|
this.parentNode.dataset.expansionism = this.value;
|
||||||
|
pack.states[state].expansionism = +this.value;
|
||||||
|
recalculateStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateRemove() {
|
||||||
|
if (customization) return;
|
||||||
|
const state = +this.parentNode.dataset.id;
|
||||||
|
regions.select("#state"+state).remove();
|
||||||
|
regions.select("#state-gap"+state).remove();
|
||||||
|
regions.select("#state-border"+state).remove();
|
||||||
|
document.querySelector("#stateLabel"+state+" > textPath").remove();
|
||||||
|
pack.burgs.forEach(b => {if(b.state === state) b.state = 0;});
|
||||||
|
pack.cells.state.forEach((s, i) => {if(s === state) pack.cells.state[i] = 0;});
|
||||||
|
pack.states[state].removed = true;
|
||||||
|
|
||||||
|
const capital = pack.states[state].capital;
|
||||||
|
pack.burgs[capital].capital = false;
|
||||||
|
pack.burgs[capital].state = 0;
|
||||||
|
moveBurgToGroup(capital, "towns");
|
||||||
|
|
||||||
|
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
|
||||||
|
refreshStatesEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePercentageMode() {
|
||||||
|
if (body.dataset.type === "absolute") {
|
||||||
|
body.dataset.type = "percentage";
|
||||||
|
const totalCells = +statesFooterCells.innerHTML;
|
||||||
|
const totalBurgs = +statesFooterBurgs.innerHTML;
|
||||||
|
const totalArea = +statesFooterArea.dataset.area;
|
||||||
|
const totalPopulation = +statesFooterPopulation.dataset.population;
|
||||||
|
|
||||||
|
body.querySelectorAll(":scope > div").forEach(function(el) {
|
||||||
|
el.querySelector(".stateCells").innerHTML = rn(+el.dataset.cells / totalCells * 100) + "%";
|
||||||
|
el.querySelector(".stateBurgs").innerHTML = rn(+el.dataset.burgs / totalBurgs * 100) + "%";
|
||||||
|
el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100) + "%";
|
||||||
|
el.querySelector(".culturePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100) + "%";
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
body.dataset.type = "absolute";
|
||||||
|
statesEditorAddLines();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function regenerateNames() {
|
||||||
|
body.querySelectorAll(":scope > div").forEach(function(el) {
|
||||||
|
const state = +el.dataset.id;
|
||||||
|
if (!state) return;
|
||||||
|
const culture = pack.states[state].culture;
|
||||||
|
const name = Names.getState(Names.getCulture(culture, 4, 7, ""), culture);
|
||||||
|
el.querySelector(".stateName").value = name;
|
||||||
|
pack.states[state].name = el.dataset.name = name;
|
||||||
|
labels.select("#stateLabel"+state+" > textPath").text(name);
|
||||||
|
});
|
||||||
|
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRegenerationMenu() {
|
||||||
|
statesBottom.querySelectorAll(":scope > button").forEach(el => el.style.display = "none");
|
||||||
|
statesRegenerateButtons.style.display = "block";
|
||||||
|
statesEditor.querySelectorAll(".hidden").forEach(el => {el.classList.remove("hidden"); el.classList.add("visible");});
|
||||||
|
$("#statesEditor").dialog({position: {my: "right top", at: "right top", of: $("#statesEditor").parent(), collision: "fit"}});
|
||||||
|
}
|
||||||
|
|
||||||
|
function recalculateStates() {
|
||||||
|
BurgsAndStates.expandStates();
|
||||||
|
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
|
||||||
|
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
|
||||||
|
refreshStatesEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
function justifyStates() {
|
||||||
|
BurgsAndStates.normalizeStates();
|
||||||
|
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
|
||||||
|
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
|
||||||
|
refreshStatesEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomizeStatesExpansion() {
|
||||||
|
pack.states.slice(1).forEach(s => {
|
||||||
|
const expansionism = rn(Math.random() * powerInput.value / 2 + 1, 1);
|
||||||
|
s.expansionism = expansionism;
|
||||||
|
body.querySelector("div.states[data-id='"+s.i+"'] > input.statePower").value = expansionism;
|
||||||
|
});
|
||||||
|
recalculateStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitRegenerationMenu() {
|
||||||
|
statesBottom.querySelectorAll(":scope > button").forEach(el => el.style.display = "inline-block");
|
||||||
|
statesRegenerateButtons.style.display = "none";
|
||||||
|
statesEditor.querySelectorAll(".visible").forEach(el => {el.classList.remove("visible"); el.classList.add("hidden");});
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterStatesManualAssignent() {
|
||||||
|
if (!layerIsOn("toggleStates")) toggleStates();
|
||||||
|
customization = 2;
|
||||||
|
regions.append("g").attr("id", "temp");
|
||||||
|
document.querySelectorAll("#statesBottom > button").forEach(el => el.style.display = "none");
|
||||||
|
document.getElementById("statesManuallyButtons").style.display = "inline-block";
|
||||||
|
document.getElementById("statesHalo").style.display = "none";
|
||||||
|
|
||||||
|
tip("Click on state to select, drag the circle to change state", true);
|
||||||
|
viewbox.style("cursor", "crosshair").call(d3.drag()
|
||||||
|
.on("drag", dragStateBrush))
|
||||||
|
.on("click", selectStateOnMapClick)
|
||||||
|
.on("touchmove mousemove", moveStateBrush);
|
||||||
|
|
||||||
|
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
|
||||||
|
body.querySelector("div").classList.add("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectStateOnLineClick(i) {
|
||||||
|
if (customization !== 2) return;
|
||||||
|
body.querySelector("div.selected").classList.remove("selected");
|
||||||
|
this.classList.add("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectStateOnMapClick() {
|
||||||
|
const point = d3.mouse(this);
|
||||||
|
const i = findCell(point[0], point[1]);
|
||||||
|
if (pack.cells.h[i] < 20) return;
|
||||||
|
|
||||||
|
const assigned = regions.select("#temp").select("polygon[data-cell='"+i+"']");
|
||||||
|
const state = assigned.size() ? +assigned.attr("data-state") : pack.cells.state[i];
|
||||||
|
|
||||||
|
body.querySelector("div.selected").classList.remove("selected");
|
||||||
|
body.querySelector("div[data-id='"+state+"']").classList.add("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragStateBrush() {
|
||||||
|
const p = d3.mouse(this);
|
||||||
|
const r = +statesManuallyBrush.value;
|
||||||
|
moveCircle(p[0], p[1], r);
|
||||||
|
|
||||||
|
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
|
||||||
|
const selection = found.filter(isLand);
|
||||||
|
if (selection) changeStateForSelection(selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// change state within selection
|
||||||
|
function changeStateForSelection(selection) {
|
||||||
|
const temp = regions.select("#temp");
|
||||||
|
const selected = body.querySelector("div.selected");
|
||||||
|
|
||||||
|
const stateNew = +selected.dataset.id;
|
||||||
|
const color = pack.states[stateNew].color || "#ffffff";
|
||||||
|
|
||||||
|
selection.forEach(function(i) {
|
||||||
|
const exists = temp.select("polygon[data-cell='"+i+"']");
|
||||||
|
const stateOld = exists.size() ? +exists.attr("data-state") : pack.cells.state[i];
|
||||||
|
if (stateNew === stateOld) return;
|
||||||
|
if (i === pack.states[stateOld].center) return;
|
||||||
|
|
||||||
|
// change of append new element
|
||||||
|
if (exists.size()) exists.attr("data-state", stateNew).attr("fill", color).attr("stroke", color);
|
||||||
|
else temp.append("polygon").attr("data-cell", i).attr("data-state", stateNew).attr("points", getPackPolygon(i)).attr("fill", color).attr("stroke", color);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveStateBrush() {
|
||||||
|
showMainTip();
|
||||||
|
const point = d3.mouse(this);
|
||||||
|
const radius = +statesManuallyBrush.value;
|
||||||
|
moveCircle(point[0], point[1], radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyStatesManualAssignent() {
|
||||||
|
const cells = pack.cells;
|
||||||
|
const changed = regions.select("#temp").selectAll("polygon");
|
||||||
|
changed.each(function() {
|
||||||
|
const i = +this.dataset.cell;
|
||||||
|
const c = +this.dataset.state;
|
||||||
|
cells.state[i] = c;
|
||||||
|
if (cells.burg[i]) pack.burgs[cells.burg[i]].state = c;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (changed.size()) {
|
||||||
|
refreshStatesEditor();
|
||||||
|
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
|
||||||
|
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
|
||||||
|
}
|
||||||
|
exitStatesManualAssignment();
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitStatesManualAssignment() {
|
||||||
|
customization = 0;
|
||||||
|
regions.select("#temp").remove();
|
||||||
|
removeCircle();
|
||||||
|
document.querySelectorAll("#statesBottom > button").forEach(el => el.style.display = "inline-block");
|
||||||
|
document.getElementById("statesManuallyButtons").style.display = "none";
|
||||||
|
document.getElementById("statesHalo").style.display = "block";
|
||||||
|
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
|
||||||
|
restoreDefaultEvents();
|
||||||
|
clearMainTip();
|
||||||
|
const selected = body.querySelector("div.selected");
|
||||||
|
if (selected) selected.classList.remove("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterAddStateMode() {
|
||||||
|
if (this.classList.contains("pressed")) {exitAddStateMode(); return;};
|
||||||
|
customization = 3;
|
||||||
|
this.classList.add("pressed");
|
||||||
|
tip("Click on the map to create a new capital or promote an existing burg", true);
|
||||||
|
viewbox.style("cursor", "crosshair").on("click", addState);
|
||||||
|
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addState() {
|
||||||
|
const point = d3.mouse(this);
|
||||||
|
const center = findCell(point[0], point[1]);
|
||||||
|
if (pack.cells.h[center] < 20) {tip("You cannot place state into the water. Please click on a land cell", false, "error"); return;}
|
||||||
|
let burg = pack.cells.burg[center];
|
||||||
|
if (burg && pack.burgs[burg].capital) {tip("Existing capital cannot be selected as a new state capital! Select other cell", false, "error"); return;}
|
||||||
|
if (!burg) burg = addBurg(point); // add new burg
|
||||||
|
|
||||||
|
// turn burg into a capital
|
||||||
|
pack.burgs[burg].capital = true;
|
||||||
|
pack.burgs[burg].state = pack.states.length;
|
||||||
|
moveBurgToGroup(burg, "cities");
|
||||||
|
|
||||||
|
exitAddStateMode();
|
||||||
|
const culture = pack.cells.culture[center];
|
||||||
|
const basename = center%5 === 0 ? pack.burgs[burg].name : Names.getCulture(culture);
|
||||||
|
const name = Names.getState(basename, culture);
|
||||||
|
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
|
||||||
|
pack.states.push({i:pack.states.length, name, color, expansionism:.5, capital:burg, type:"Generic", center, culture});
|
||||||
|
recalculateStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitAddStateMode() {
|
||||||
|
customization = 0;
|
||||||
|
restoreDefaultEvents();
|
||||||
|
clearMainTip();
|
||||||
|
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
|
||||||
|
if (statesAdd.classList.contains("pressed")) statesAdd.classList.remove("pressed");
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadStatesData() {
|
||||||
|
const unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value;
|
||||||
|
let data = "Id,State,Color,Capital,Culture,Type,Expansionism,Cells,Burgs,Area "+unit+",Population\n"; // headers
|
||||||
|
|
||||||
|
body.querySelectorAll(":scope > div").forEach(function(el) {
|
||||||
|
data += el.dataset.id + ",";
|
||||||
|
data += el.dataset.name + ",";
|
||||||
|
data += el.dataset.color + ",";
|
||||||
|
data += el.dataset.capital + ",";
|
||||||
|
data += el.dataset.culture + ",";
|
||||||
|
data += el.dataset.type + ",";
|
||||||
|
data += el.dataset.expansionism + ",";
|
||||||
|
data += el.dataset.cells + ",";
|
||||||
|
data += el.dataset.burgs + ",";
|
||||||
|
data += el.dataset.area + ",";
|
||||||
|
data += el.dataset.population + "\n";
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataBlob = new Blob([data], {type: "text/plain"});
|
||||||
|
const url = window.URL.createObjectURL(dataBlob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.download = "states_data" + Date.now() + ".csv";
|
||||||
|
link.href = url;
|
||||||
|
link.click();
|
||||||
|
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeStatesEditor() {
|
||||||
|
if (customization === 2) exitStatesManualAssignment();
|
||||||
|
if (customization === 3) exitAddStateMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
222
modules/ui/tools.js
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
// module to control the Tools options (click to edit, to re-geenerate, tp add)
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
toolsContent.addEventListener("click", function() {
|
||||||
|
if (customization) {tip("Please exit the customization mode first", false, "warning"); return;}
|
||||||
|
if (event.target.tagName !== "BUTTON") return;
|
||||||
|
const button = event.target.id;
|
||||||
|
|
||||||
|
// Click to open Editor buttons
|
||||||
|
if (button === "editHeightmapButton") editHeightmap(); else
|
||||||
|
if (button === "editBiomesButton") editBiomes(); else
|
||||||
|
if (button === "editStatesButton") editStates(); else
|
||||||
|
if (button === "editCulturesButton") editCultures(); else
|
||||||
|
if (button === "editNamesBaseButton") editNamesbase(); else
|
||||||
|
if (button === "editBurgsButton") editBurgs(); else
|
||||||
|
if (button === "editUnitsButton") editUnits();
|
||||||
|
|
||||||
|
// Click to Regenerate buttons
|
||||||
|
if (button === "regenerateStateLabels") {BurgsAndStates.drawStateLabels(); if (!layerIsOn("toggleLabels")) toggleLabels();} else
|
||||||
|
if (button === "regenerateReliefIcons") {ReliefIcons(); if (!layerIsOn("toggleRelief")) toggleRelief();} else
|
||||||
|
if (button === "regenerateRoutes") {Routes.regenerate(); if (!layerIsOn("toggleRoutes")) toggleRoutes();} else
|
||||||
|
if (button === "regenerateRivers") {Rivers.generate(); if (!layerIsOn("toggleRivers")) toggleRivers();} else
|
||||||
|
if (button === "regeneratePopulation") recalculatePopulation();
|
||||||
|
|
||||||
|
// Click to Add buttons
|
||||||
|
if (button === "addLabel") toggleAddLabel(); else
|
||||||
|
if (button === "addBurgTool") toggleAddBurg(); else
|
||||||
|
if (button === "addRiver") toggleAddRiver(); else
|
||||||
|
if (button === "addRoute") toggleAddRoute(); else
|
||||||
|
if (button === "addMarker") toggleAddMarker();
|
||||||
|
});
|
||||||
|
|
||||||
|
function recalculatePopulation() {
|
||||||
|
rankCells();
|
||||||
|
pack.burgs.forEach(b => {
|
||||||
|
if (!b.i || b.removed) return;
|
||||||
|
const i = b.cell;
|
||||||
|
|
||||||
|
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i]) / 3 + b.i / 1000 + i % 100 / 1000, .1), 3);
|
||||||
|
if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
|
||||||
|
if (b.port) b.population = rn(b.population * 1.3, 3); // increase port population
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function unpressClickToAddButton() {
|
||||||
|
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||||
|
restoreDefaultEvents();
|
||||||
|
clearMainTip();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAddLabel() {
|
||||||
|
const pressed = document.getElementById("addLabel").classList.contains("pressed");
|
||||||
|
if (pressed) {unpressClickToAddButton(); return;}
|
||||||
|
|
||||||
|
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||||
|
addLabel.classList.add('pressed');
|
||||||
|
closeDialogs(".stable");
|
||||||
|
viewbox.style("cursor", "crosshair").on("click", addLabelOnClick);
|
||||||
|
tip("Click on map to place label. Hold Shift to add multiple", true);
|
||||||
|
if (!layerIsOn("toggleLabels")) toggleLabels();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLabelOnClick() {
|
||||||
|
const point = d3.mouse(this);
|
||||||
|
|
||||||
|
// get culture in clicked point to generate a name
|
||||||
|
const cell = findCell(point[0], point[1]);
|
||||||
|
const culture = pack.cells.culture[cell];
|
||||||
|
const name = Names.getCulture(culture);
|
||||||
|
const id = getNextId("label");
|
||||||
|
|
||||||
|
labels.select("#addedLabels").append("text").attr("id", id)
|
||||||
|
.append("textPath").attr("xlink:href", "#textPath_"+id).text(name)
|
||||||
|
.attr("startOffset", "50%").attr("font-size", "100%");
|
||||||
|
|
||||||
|
defs.select("#textPaths").append("path").attr("id", "textPath_"+id)
|
||||||
|
.attr("d", `M${point[0]-60},${point[1]} h${120}`);
|
||||||
|
|
||||||
|
if (d3.event.shiftKey === false) unpressClickToAddButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAddBurg() {
|
||||||
|
unpressClickToAddButton();
|
||||||
|
document.getElementById("addBurgTool").classList.add("pressed");
|
||||||
|
editBurgs();
|
||||||
|
document.getElementById("addNewBurg").click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAddRiver() {
|
||||||
|
const pressed = document.getElementById("addRiver").classList.contains("pressed");
|
||||||
|
if (pressed) {unpressClickToAddButton(); return;}
|
||||||
|
|
||||||
|
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||||
|
addRiver.classList.add('pressed');
|
||||||
|
closeDialogs(".stable");
|
||||||
|
viewbox.style("cursor", "crosshair").on("click", addRiverOnClick);
|
||||||
|
tip("Click on map to place new river or extend an existing one. Hold Shift to place multiple rivers", true);
|
||||||
|
if (!layerIsOn("toggleRivers")) toggleRivers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRiverOnClick() {
|
||||||
|
const cells = pack.cells;
|
||||||
|
const point = d3.mouse(this);
|
||||||
|
let i = findCell(point[0], point[1]);
|
||||||
|
if (cells.r[i] || cells.h[i] < 20 || cells.b[i]) return;
|
||||||
|
|
||||||
|
const dataRiver = []; // to store river points
|
||||||
|
const river = +getNextId("river").slice(5); // river id
|
||||||
|
cells.fl[i] = grid.cells.prec[cells.g[i]]; // initial flux
|
||||||
|
let render = true;
|
||||||
|
|
||||||
|
while (i) {
|
||||||
|
cells.r[i] = river;
|
||||||
|
const x = cells.p[i][0], y = cells.p[i][1];
|
||||||
|
dataRiver.push({x, y, cell:i});
|
||||||
|
|
||||||
|
const min = cells.c[i][d3.scan(cells.c[i], (a, b) => cells.h[a] - cells.h[b])]; // downhill cell
|
||||||
|
|
||||||
|
if (cells.h[i] <= cells.h[min]) {
|
||||||
|
tip(`Clicked cell is depressed! To resolve edit the heightmap and allow system to change heights`, false, "error");
|
||||||
|
render = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tx = cells.p[min][0], ty = cells.p[min][1];
|
||||||
|
|
||||||
|
if (cells.h[min] < 20) {
|
||||||
|
const px = (x + tx) / 2;
|
||||||
|
const py = (y + ty) / 2;
|
||||||
|
dataRiver.push({x: px, y: py, cell:i});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cells.r[min]) {
|
||||||
|
cells.fl[min] += cells.fl[i];
|
||||||
|
i = min;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = cells.r[min];
|
||||||
|
const riverCellsUpper = cells.i.filter(i => cells.r[i] === r && cells.h[i] > cells.h[min]);
|
||||||
|
|
||||||
|
// new river is not perspective
|
||||||
|
if (dataRiver.length <= riverCellsUpper.length) {
|
||||||
|
cells.conf[min] += cells.fl[i];
|
||||||
|
dataRiver.push({x: tx, y: ty, cell: min});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// new river is more perspective
|
||||||
|
rivers.select("#river"+r).remove();
|
||||||
|
riverCellsUpper.forEach(i => cells.r[i] = 0);
|
||||||
|
if (riverCellsUpper.length > 1) {
|
||||||
|
// redraw upper part of the old river
|
||||||
|
}
|
||||||
|
|
||||||
|
cells.conf[min] = cells.fl[min];
|
||||||
|
cells.fl[min] = cells.fl[i] + grid.cells.prec[cells.g[min]];
|
||||||
|
i = min;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!render) return;
|
||||||
|
const points = Rivers.addMeandring(dataRiver, Math.random() * .5 + .1);
|
||||||
|
const width = Math.random() * .5 + .9;
|
||||||
|
const increment = Math.random() * .4 + .8;
|
||||||
|
const d = Rivers.getPath(points, width, increment);
|
||||||
|
rivers.append("path").attr("d", d).attr("id", "river"+river).attr("data-width", width).attr("data-increment", increment);
|
||||||
|
|
||||||
|
if (d3.event.shiftKey === false) unpressClickToAddButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAddRoute() {
|
||||||
|
const pressed = document.getElementById("addRoute").classList.contains("pressed");
|
||||||
|
if (pressed) {unpressClickToAddButton(); return;}
|
||||||
|
|
||||||
|
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||||
|
addRoute.classList.add('pressed');
|
||||||
|
closeDialogs(".stable");
|
||||||
|
viewbox.style("cursor", "crosshair").on("click", addRouteOnClick);
|
||||||
|
tip("Click on map to add a first control point", true);
|
||||||
|
if (!layerIsOn("toggleRoutes")) toggleRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRouteOnClick() {
|
||||||
|
unpressClickToAddButton();
|
||||||
|
const point = d3.mouse(this);
|
||||||
|
const id = getNextId("route");
|
||||||
|
elSelected = routes.select("g").append("path").attr("id", id).attr("data-new", 1).attr("d", `M${point[0]},${point[1]}`);
|
||||||
|
editRoute(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAddMarker() {
|
||||||
|
const pressed = document.getElementById("addMarker").classList.contains("pressed");
|
||||||
|
if (pressed) {unpressClickToAddButton(); return;}
|
||||||
|
|
||||||
|
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||||
|
addMarker.classList.add('pressed');
|
||||||
|
closeDialogs(".stable");
|
||||||
|
viewbox.style("cursor", "crosshair").on("click", addMarkerOnClick);
|
||||||
|
tip("Click on map to add a marker. Hold Shift to add multiple", true);
|
||||||
|
if (!layerIsOn("toggleMarkers")) toggleMarkers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMarkerOnClick() {
|
||||||
|
const point = d3.mouse(this);
|
||||||
|
const x = rn(point[0], 2), y = rn(point[1], 2);
|
||||||
|
const id = getNextId("markerElement");
|
||||||
|
|
||||||
|
const selected = markerSelectGroup.value;
|
||||||
|
const valid = selected && d3.select("#defs-markers").select("#"+selected).size();
|
||||||
|
const symbol = valid ? "#"+selected : "#marker0";
|
||||||
|
const added = markers.select("[data-id='" + symbol + "']").size();
|
||||||
|
let desired = valid && added ? markers.select("[data-id='" + symbol + "']").attr("data-size") : 1;
|
||||||
|
if (isNaN(desired)) desired = 1;
|
||||||
|
const size = desired * 5 + 25 / scale;
|
||||||
|
|
||||||
|
markers.append("use").attr("id", id).attr("xlink:href", symbol).attr("data-id", symbol)
|
||||||
|
.attr("data-x", x).attr("data-y", y).attr("x", x - size / 2).attr("y", y - size)
|
||||||
|
.attr("data-size", desired).attr("width", size).attr("height", size);
|
||||||
|
|
||||||
|
if (d3.event.shiftKey === false) unpressClickToAddButton();
|
||||||
|
}
|
||||||
167
modules/ui/units-editor.js
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
"use strict";
|
||||||
|
function editUnits() {
|
||||||
|
closeDialogs("#unitsEditor, .stable");
|
||||||
|
$("#unitsEditor").dialog();
|
||||||
|
|
||||||
|
if (modules.editUnits) return;
|
||||||
|
modules.editUnits = true;
|
||||||
|
|
||||||
|
$("#unitsEditor").dialog({
|
||||||
|
title: "Units Editor",
|
||||||
|
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
|
||||||
|
});
|
||||||
|
|
||||||
|
// add listeners
|
||||||
|
document.getElementById("distanceUnit").addEventListener("change", changeDistanceUnit);
|
||||||
|
document.getElementById("distanceScaleSlider").addEventListener("input", changeDistanceScale);
|
||||||
|
document.getElementById("distanceScale").addEventListener("change", changeDistanceScale);
|
||||||
|
document.getElementById("distanceScale").addEventListener("mouseenter", hideDistanceUnitOutput);
|
||||||
|
document.getElementById("distanceScale").addEventListener("mouseleave", showDistanceUnitOutput);
|
||||||
|
document.getElementById("heightUnit").addEventListener("change", changeHeightUnit);
|
||||||
|
document.getElementById("heightExponent").addEventListener("input", changeHeightExponent);
|
||||||
|
document.getElementById("heightExponentSlider").addEventListener("input", changeHeightExponent);
|
||||||
|
document.getElementById("temperatureScale").addEventListener("change", () => {if (layerIsOn("toggleTemp")) drawTemp()});
|
||||||
|
document.getElementById("barSizeSlider").addEventListener("input", changeScaleBarSize);
|
||||||
|
document.getElementById("barSize").addEventListener("input", changeScaleBarSize);
|
||||||
|
document.getElementById("barLabel").addEventListener("input", drawScaleBar);
|
||||||
|
document.getElementById("barPosX").addEventListener("input", fitScaleBar);
|
||||||
|
document.getElementById("barPosY").addEventListener("input", fitScaleBar);
|
||||||
|
document.getElementById("barBackOpacity").addEventListener("input", function() {scaleBar.select("rect").attr("opacity", this.value)});
|
||||||
|
document.getElementById("barBackColor").addEventListener("input", function() {scaleBar.select("rect").attr("fill", this.value)});
|
||||||
|
document.getElementById("populationRateSlider").addEventListener("input", changePopulationRate);
|
||||||
|
document.getElementById("populationRate").addEventListener("change", changePopulationRate);
|
||||||
|
document.getElementById("urbanizationSlider").addEventListener("input", changeUrbanizationRate);
|
||||||
|
document.getElementById("urbanization").addEventListener("change", changeUrbanizationRate);
|
||||||
|
|
||||||
|
document.getElementById("addLinearRuler").addEventListener("click", addAdditionalRuler);
|
||||||
|
document.getElementById("addOpisometer").addEventListener("click", toggleOpisometerMode);
|
||||||
|
document.getElementById("addPlanimeter").addEventListener("click", togglePlanimeterMode);
|
||||||
|
document.getElementById("removeRulers").addEventListener("click", removeAllRulers);
|
||||||
|
|
||||||
|
function changeDistanceUnit() {
|
||||||
|
if (this.value === "custom_name") {
|
||||||
|
const custom = prompt("Provide a custom name for distance unit");
|
||||||
|
if (custom) this.options.add(new Option(custom, custom, false, true));
|
||||||
|
else {this.value = document.getElementById("distanceUnitOutput").innerHTML; return;};
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("distanceUnitOutput").innerHTML = this.value;
|
||||||
|
drawScaleBar();
|
||||||
|
calculateFriendlyGridSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeDistanceScale() {
|
||||||
|
const scale = +this.value;
|
||||||
|
if (!scale || isNaN(scale) || scale < 0) {
|
||||||
|
tip("Distance scale should be a positive number", false, "error");
|
||||||
|
this.value = document.getElementById("distanceScale").dataset.value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("distanceScaleSlider").value = scale;
|
||||||
|
document.getElementById("distanceScale").value = scale;
|
||||||
|
document.getElementById("distanceScale").dataset.value = scale;
|
||||||
|
drawScaleBar();
|
||||||
|
calculateFriendlyGridSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideDistanceUnitOutput() {document.getElementById("distanceUnitOutput").style.opacity = .2;}
|
||||||
|
function showDistanceUnitOutput() {document.getElementById("distanceUnitOutput").style.opacity = 1;}
|
||||||
|
|
||||||
|
function changeHeightUnit() {
|
||||||
|
if (this.value !== "custom_name") return;
|
||||||
|
const custom = prompt("Provide a custom name for height unit");
|
||||||
|
if (custom) this.options.add(new Option(custom, custom, false, true));
|
||||||
|
else this.value = "ft";
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeHeightExponent() {
|
||||||
|
document.getElementById("heightExponent").value = this.value;
|
||||||
|
document.getElementById("heightExponentSlider").value = this.value;
|
||||||
|
calculateTemperatures();
|
||||||
|
if (layerIsOn("toggleTemp")) drawTemp();
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeScaleBarSize() {
|
||||||
|
document.getElementById("barSize").value = this.value;
|
||||||
|
document.getElementById("barSizeSlider").value = this.value;
|
||||||
|
drawScaleBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePopulationRate() {
|
||||||
|
const rate = +this.value;
|
||||||
|
if (!rate || isNaN(rate) || rate <= 0) {
|
||||||
|
tip("Population rate should be a positive number", false, "error");
|
||||||
|
this.value = document.getElementById("populationRate").dataset.value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("populationRateSlider").value = rate;
|
||||||
|
document.getElementById("populationRate").value = rate;
|
||||||
|
document.getElementById("populationRate").dataset.value = rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeUrbanizationRate() {
|
||||||
|
const rate = +this.value;
|
||||||
|
if (!rate || isNaN(rate) || rate < 0) {
|
||||||
|
tip("Urbanization rate should be a number", false, "error");
|
||||||
|
this.value = document.getElementById("urbanization").dataset.value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("urbanizationSlider").value = rate;
|
||||||
|
document.getElementById("urbanization").value = rate;
|
||||||
|
document.getElementById("urbanization").dataset.value = rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAdditionalRuler() {
|
||||||
|
if (!layerIsOn("toggleRulers")) toggleRulers();
|
||||||
|
const y = rn(Math.random() * graphHeight * .5 + graphHeight * .25);
|
||||||
|
addRuler(graphWidth * .2, y, graphWidth * .8, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleOpisometerMode() {
|
||||||
|
if (this.classList.contains("pressed")) {
|
||||||
|
restoreDefaultEvents();
|
||||||
|
clearMainTip();
|
||||||
|
this.classList.remove("pressed");
|
||||||
|
} else {
|
||||||
|
if (!layerIsOn("toggleRulers")) toggleRulers();
|
||||||
|
tip("Draw a curve to measure its length", true);
|
||||||
|
unitsBottom.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
|
||||||
|
this.classList.add("pressed");
|
||||||
|
viewbox.style("cursor", "crosshair").call(d3.drag().on("start", drawOpisometer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePlanimeterMode() {
|
||||||
|
if (this.classList.contains("pressed")) {
|
||||||
|
restoreDefaultEvents();
|
||||||
|
clearMainTip();
|
||||||
|
this.classList.remove("pressed");
|
||||||
|
} else {
|
||||||
|
if (!layerIsOn("toggleRulers")) toggleRulers();
|
||||||
|
tip("Draw a line to measure its inner area", true);
|
||||||
|
unitsBottom.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
|
||||||
|
this.classList.add("pressed");
|
||||||
|
viewbox.style("cursor", "crosshair").call(d3.drag().on("start", drawPlanimeter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAllRulers() {
|
||||||
|
if (!ruler.selectAll("g").size()) return;
|
||||||
|
alertMessage.innerHTML = `Are you sure you want to remove all placed rulers?`;
|
||||||
|
$("#alert").dialog({resizable: false, title: "Remove all rulers",
|
||||||
|
buttons: {
|
||||||
|
Remove: function() {
|
||||||
|
$(this).dialog("close");
|
||||||
|
ruler.selectAll("g").remove();
|
||||||
|
},
|
||||||
|
Cancel: function() {$(this).dialog("close");}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
114
modules/ui/world-configurator.js
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
function editWorld() {
|
||||||
|
if (customization) return;
|
||||||
|
$("#worldConfigurator").dialog({title: "Configure World", width: 440});
|
||||||
|
|
||||||
|
const globe = d3.select("#globe");
|
||||||
|
const clr = d3.scaleSequential(d3.interpolateSpectral);
|
||||||
|
const tMax = +temperatureEquatorOutput.max, tMin = +temperatureEquatorOutput.min; // temperature extremes
|
||||||
|
const projection = d3.geoOrthographic().translate([100, 100]).scale(100);
|
||||||
|
const path = d3.geoPath(projection);
|
||||||
|
|
||||||
|
updateGlobeTemperature();
|
||||||
|
updateGlobePosition();
|
||||||
|
|
||||||
|
if (modules.editWorld) return;
|
||||||
|
modules.editWorld = true;
|
||||||
|
|
||||||
|
document.getElementById("worldControls").addEventListener("input", (e) => updateWorld(e.target));
|
||||||
|
globe.select("#globeWindArrows").on("click", changeWind);
|
||||||
|
globe.select("#restoreWind").on("click", restoreDefaultWinds);
|
||||||
|
globe.select("#globeGraticule").attr("d", round(path(d3.geoGraticule()()))); // globe graticule
|
||||||
|
updateWindDirections();
|
||||||
|
|
||||||
|
function updateWorld(el) {
|
||||||
|
if (el) {
|
||||||
|
document.getElementById(el.dataset.stored+"Input").value = el.value;
|
||||||
|
document.getElementById(el.dataset.stored+"Output").value = el.value;
|
||||||
|
if (el.dataset.stored) lock(el.dataset.stored);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateGlobeTemperature();
|
||||||
|
updateGlobePosition();
|
||||||
|
calculateTemperatures();
|
||||||
|
generatePrecipitation();
|
||||||
|
elevateLakes();
|
||||||
|
Rivers.generate();
|
||||||
|
defineBiomes();
|
||||||
|
|
||||||
|
if (layerIsOn("toggleTemp")) drawTemp();
|
||||||
|
if (layerIsOn("togglePrec")) drawPrec();
|
||||||
|
if (layerIsOn("toggleBiomes")) drawBiomes();
|
||||||
|
if (layerIsOn("toggleCoordinates")) drawCoordinates();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateGlobePosition() {
|
||||||
|
const eqY = +document.getElementById("equatorOutput").value;
|
||||||
|
const equidistance = document.getElementById("equidistanceOutput");
|
||||||
|
equidistance.min = equidistanceInput.min = Math.max(graphHeight - eqY, eqY);
|
||||||
|
equidistance.max = equidistanceInput.max = equidistance.min * 10;
|
||||||
|
const eqD = +equidistance.value;
|
||||||
|
calculateMapCoordinates();
|
||||||
|
const mc = mapCoordinates; // shortcut
|
||||||
|
|
||||||
|
const scale = +distanceScale.value, unit = distanceUnit.value;
|
||||||
|
document.getElementById("mapSize").innerHTML = `${graphWidth}x${graphHeight}`;
|
||||||
|
document.getElementById("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
|
||||||
|
document.getElementById("meridianLength").innerHTML = rn(eqD * 2);
|
||||||
|
document.getElementById("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * scale)} ${unit}`;
|
||||||
|
document.getElementById("meridianLengthEarth").innerHTML = toKilometer(eqD * 2 * scale);
|
||||||
|
document.getElementById("mapCoordinates").innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`;
|
||||||
|
|
||||||
|
function toKilometer(v) {
|
||||||
|
let kilometers; // value converted to kilometers
|
||||||
|
if (unit === "km") kilometers = v;
|
||||||
|
else if (unit === "mi") kilometers = v * 1.60934;
|
||||||
|
else if (unit === "lg") kilometers = v * 5.556;
|
||||||
|
else if (unit === "vr") kilometers = v * 1.0668;
|
||||||
|
else return ""; // do not show as distanceUnit is custom
|
||||||
|
return " = " + rn(kilometers / 200) + "%🌏"; // % + Earth icon
|
||||||
|
}
|
||||||
|
|
||||||
|
function lat(lat) {return lat > 0 ? Math.abs(rn(lat)) + "°N" : Math.abs(rn(lat)) + "°S";} // parse latitude value
|
||||||
|
const area = d3.geoGraticule().extent([[mc.lonW, mc.latN], [mc.lonE, mc.latS]]);
|
||||||
|
globe.select("#globeArea").attr("d", round(path(area.outline()))); // map area
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateGlobeTemperature() {
|
||||||
|
const tEq = +document.getElementById("temperatureEquatorOutput").value;
|
||||||
|
document.getElementById("temperatureEquatorF").innerHTML = rn(tEq * 9/5 + 32);
|
||||||
|
const tPole = +document.getElementById("temperaturePoleOutput").value;
|
||||||
|
document.getElementById("temperaturePoleF").innerHTML = rn(tPole * 9/5 + 32);
|
||||||
|
globe.selectAll(".tempGradient90").attr("stop-color", clr(1 - (tPole - tMin) / (tMax - tMin)));
|
||||||
|
globe.selectAll(".tempGradient60").attr("stop-color", clr(1 - (tEq - (tEq - tPole) * 2/3 - tMin) / (tMax - tMin)));
|
||||||
|
globe.selectAll(".tempGradient30").attr("stop-color", clr(1 - (tEq - (tEq - tPole) * 1/3 - tMin) / (tMax - tMin)));
|
||||||
|
globe.select(".tempGradient0").attr("stop-color", clr(1 - (tEq - tMin) / (tMax - tMin)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWindDirections() {
|
||||||
|
globe.select("#globeWindArrows").selectAll("path").each(function(d, i) {
|
||||||
|
const tr = parseTransform(this.getAttribute("transform"));
|
||||||
|
this.setAttribute("transform", `rotate(${winds[i]} ${tr[1]} ${tr[2]})`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeWind() {
|
||||||
|
const arrow = d3.event.target.nextElementSibling;
|
||||||
|
const tier = +arrow.dataset.tier;
|
||||||
|
winds[tier] = (winds[tier] + 45) % 360;
|
||||||
|
const tr = parseTransform(arrow.getAttribute("transform"));
|
||||||
|
arrow.setAttribute("transform", `rotate(${winds[tier]} ${tr[1]} ${tr[2]})`);
|
||||||
|
localStorage.setItem("winds", winds);
|
||||||
|
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => (90-c) / 30 | 0);
|
||||||
|
if (mapTiers.includes(tier)) updateWorld();
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreDefaultWinds() {
|
||||||
|
const defaultWinds = [225, 45, 225, 315, 135, 315];
|
||||||
|
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => (90-c) / 30 | 0);
|
||||||
|
const update = mapTiers.some(t => winds[t] != defaultWinds[t]);
|
||||||
|
winds = defaultWinds;
|
||||||
|
updateWindDirections();
|
||||||
|
if (update) updateWorld();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
436
modules/utils.js
Normal file
|
|
@ -0,0 +1,436 @@
|
||||||
|
// FMG helper functions
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// add boundary points to pseudo-clip voronoi cells
|
||||||
|
function getBoundaryPoints(width, height, spacing) {
|
||||||
|
const offset = rn(-1 * spacing);
|
||||||
|
const bSpacing = spacing * 2;
|
||||||
|
const w = width - offset * 2;
|
||||||
|
const h = height - offset * 2;
|
||||||
|
const numberX = Math.ceil(w / bSpacing) - 1;
|
||||||
|
const numberY = Math.ceil(h / bSpacing) - 1;
|
||||||
|
let points = [];
|
||||||
|
for (let i = 0.5; i < numberX; i++) {
|
||||||
|
let x = Math.ceil(w * i / numberX + offset);
|
||||||
|
points.push([x, offset], [x, h + offset]);
|
||||||
|
}
|
||||||
|
for (let i = 0.5; i < numberY; i++) {
|
||||||
|
let y = Math.ceil(h * i / numberY + offset);
|
||||||
|
points.push([offset, y], [w + offset, y]);
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get points on a regular square grid and jitter them a bit
|
||||||
|
function getJitteredGrid(width, height, spacing) {
|
||||||
|
const radius = spacing / 2; // square radius
|
||||||
|
const jittering = radius * 0.9; // max deviation
|
||||||
|
const jitter = function() {return Math.random() * 2 * jittering - jittering;};
|
||||||
|
let points = [];
|
||||||
|
for (let y = radius; y < height; y += spacing) {
|
||||||
|
for (let x = radius; x < width; x += spacing) {
|
||||||
|
let xj = rn(x + jitter(), 2);
|
||||||
|
let yj = rn(y + jitter(), 2);
|
||||||
|
points.push([xj, yj]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
// return cell index on a regular square grid
|
||||||
|
function findGridCell(x, y) {
|
||||||
|
return Math.floor(Math.min(y / grid.spacing, grid.cellsY -1)) * grid.cellsX + Math.floor(Math.min(x / grid.spacing, grid.cellsX-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// return array of cell indexes in radius on a regular square grid
|
||||||
|
function findGridAll(x, y, radius) {
|
||||||
|
const c = grid.cells.c;
|
||||||
|
let found = [findGridCell(x, y)];
|
||||||
|
let r = Math.floor(radius / grid.spacing);
|
||||||
|
if (r > 0) found = found.concat(c[found[0]]);
|
||||||
|
if (r > 1) {
|
||||||
|
let frontier = c[found[0]];
|
||||||
|
while (r > 1) {
|
||||||
|
let cycle = frontier.slice();
|
||||||
|
frontier = [];
|
||||||
|
cycle.forEach(function(s) {
|
||||||
|
|
||||||
|
c[s].forEach(function(e) {
|
||||||
|
if (found.indexOf(e) !== -1) return;
|
||||||
|
found.push(e);
|
||||||
|
frontier.push(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
r--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return found;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// return closest pack points quadtree datum
|
||||||
|
function find(x, y, radius = Infinity) {
|
||||||
|
return pack.cells.q.find(x, y, radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return closest cell index
|
||||||
|
function findCell(x, y, radius = Infinity) {
|
||||||
|
const found = pack.cells.q.find(x, y, radius);
|
||||||
|
return found ? found[2] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// return array of cell indexes in radius
|
||||||
|
function findAll(x, y, radius) {
|
||||||
|
const found = pack.cells.q.findAll(x, y, radius);
|
||||||
|
return found.map(r => r[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get polygon points for packed cells knowing cell id
|
||||||
|
function getPackPolygon(i) {
|
||||||
|
return pack.cells.v[i].map(v => pack.vertices.p[v]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get polygon points for initial cells knowing cell id
|
||||||
|
function getGridPolygon(i) {
|
||||||
|
return grid.cells.v[i].map(v => grid.vertices.p[v]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// mbostock's poissonDiscSampler
|
||||||
|
function* poissonDiscSampler(x0, y0, x1, y1, r, k = 3) {
|
||||||
|
if (!(x1 >= x0) || !(y1 >= y0) || !(r > 0)) throw new Error;
|
||||||
|
|
||||||
|
const width = x1 - x0;
|
||||||
|
const height = y1 - y0;
|
||||||
|
const r2 = r * r;
|
||||||
|
const r2_3 = 3 * r2;
|
||||||
|
const cellSize = r * Math.SQRT1_2;
|
||||||
|
const gridWidth = Math.ceil(width / cellSize);
|
||||||
|
const gridHeight = Math.ceil(height / cellSize);
|
||||||
|
const grid = new Array(gridWidth * gridHeight);
|
||||||
|
const queue = [];
|
||||||
|
|
||||||
|
function far(x, y) {
|
||||||
|
const i = x / cellSize | 0;
|
||||||
|
const j = y / cellSize | 0;
|
||||||
|
const i0 = Math.max(i - 2, 0);
|
||||||
|
const j0 = Math.max(j - 2, 0);
|
||||||
|
const i1 = Math.min(i + 3, gridWidth);
|
||||||
|
const j1 = Math.min(j + 3, gridHeight);
|
||||||
|
for (let j = j0; j < j1; ++j) {
|
||||||
|
const o = j * gridWidth;
|
||||||
|
for (let i = i0; i < i1; ++i) {
|
||||||
|
const s = grid[o + i];
|
||||||
|
if (s) {
|
||||||
|
const dx = s[0] - x;
|
||||||
|
const dy = s[1] - y;
|
||||||
|
if (dx * dx + dy * dy < r2) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sample(x, y) {
|
||||||
|
queue.push(grid[gridWidth * (y / cellSize | 0) + (x / cellSize | 0)] = [x, y]);
|
||||||
|
return [x + x0, y + y0];
|
||||||
|
}
|
||||||
|
|
||||||
|
yield sample(width / 2, height / 2);
|
||||||
|
|
||||||
|
pick: while (queue.length) {
|
||||||
|
const i = Math.random() * queue.length | 0;
|
||||||
|
const parent = queue[i];
|
||||||
|
|
||||||
|
for (let j = 0; j < k; ++j) {
|
||||||
|
const a = 2 * Math.PI * Math.random();
|
||||||
|
const r = Math.sqrt(Math.random() * r2_3 + r2);
|
||||||
|
const x = parent[0] + r * Math.cos(a);
|
||||||
|
const y = parent[1] + r * Math.sin(a);
|
||||||
|
if (0 <= x && x < width && 0 <= y && y < height && far(x, y)) {
|
||||||
|
yield sample(x, y);
|
||||||
|
continue pick;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = queue.pop();
|
||||||
|
if (i < queue.length) queue[i] = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter land cells
|
||||||
|
function isLand(i) {
|
||||||
|
return pack.cells.h[i] >= 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter water cells
|
||||||
|
function isWater(i) {
|
||||||
|
return pack.cells.h[i] < 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort cells by height: highest go first
|
||||||
|
function highest(a, b) {
|
||||||
|
return pack.cells.h[b] - pack.cells.h[a];
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert RGB color string to HEX without #
|
||||||
|
function toHEX(rgb){
|
||||||
|
if (rgb.charAt(0) === "#") {return rgb;}
|
||||||
|
rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
|
||||||
|
return (rgb && rgb.length === 4) ? "#" +
|
||||||
|
("0" + parseInt(rgb[1],10).toString(16)).slice(-2) +
|
||||||
|
("0" + parseInt(rgb[2],10).toString(16)).slice(-2) +
|
||||||
|
("0" + parseInt(rgb[3],10).toString(16)).slice(-2) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// return array of standard shuffled colors
|
||||||
|
function getColors(number) {
|
||||||
|
const c12 = d3.scaleOrdinal(d3.schemeSet3);
|
||||||
|
const cRB = d3.scaleSequential(d3.interpolateRainbow);
|
||||||
|
const colors = d3.shuffle(d3.range(number).map(i => i < 12 ? c12(i) : d3.color(cRB((i-12)/(number-12))).hex()));
|
||||||
|
//debug.selectAll("circle").data(colors).enter().append("circle").attr("r", 15).attr("cx", (d,i) => 60 + i * 40).attr("cy", 20).attr("fill", d => d);
|
||||||
|
return colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// conver temperature from °C to other scales
|
||||||
|
function convertTemperature(c) {
|
||||||
|
switch(temperatureScale.value) {
|
||||||
|
case "°C": return c + "°C";
|
||||||
|
case "°F": return rn(c * 9 / 5 + 32) + "°F";
|
||||||
|
case "K": return rn(c + 273.15) + "K";
|
||||||
|
case "°R": return rn((c + 273.15) * 9 / 5) + "°R";
|
||||||
|
case "°De": return rn((100 - c) * 3 / 2) + "°De";
|
||||||
|
case "°N": return rn(c * 33 / 100) + "°N";
|
||||||
|
case "°Ré": return rn(c * 4 / 5) + "°Ré";
|
||||||
|
case "°Rø": return rn(c * 21 / 40 + 7.5) + "°Rø";
|
||||||
|
default: return c + "°C";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// random number in a range
|
||||||
|
function rand(min, max) {
|
||||||
|
if (min === undefined && !max === undefined) return Math.random();
|
||||||
|
if (max === undefined) {max = min; min = 0;}
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gauss(expected = 100, deviation = 30, min = 0, max = 300, round = 0) {
|
||||||
|
return rn(Math.max(Math.min(d3.randomNormal(expected, deviation)(), max), min), round);
|
||||||
|
}
|
||||||
|
|
||||||
|
// round value to d decimals
|
||||||
|
function rn(v, d = 0) {
|
||||||
|
const m = Math.pow(10, d);
|
||||||
|
return Math.round(v * m) / m;
|
||||||
|
}
|
||||||
|
|
||||||
|
// round string to d decimals
|
||||||
|
function round(s, d = 1) {
|
||||||
|
return s.replace(/[\d\.-][\d\.e-]*/g, function(n) {return rn(n, d);})
|
||||||
|
}
|
||||||
|
|
||||||
|
// corvent number to short string with SI postfix
|
||||||
|
function si(n) {
|
||||||
|
if (n >= 1e9) {return rn(n / 1e9, 1) + "B";}
|
||||||
|
if (n >= 1e8) {return rn(n / 1e6) + "M";}
|
||||||
|
if (n >= 1e6) {return rn(n / 1e6, 1) + "M";}
|
||||||
|
if (n >= 1e4) {return rn(n / 1e3) + "K";}
|
||||||
|
if (n >= 1e3) {return rn(n / 1e3, 1) + "K";}
|
||||||
|
return rn(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
// getInteger number from user input data
|
||||||
|
function getInteger(value) {
|
||||||
|
const metric = value.slice(-1);
|
||||||
|
if (metric === "K") return parseInt(value.slice(0, -1) * 1e3);
|
||||||
|
if (metric === "M") return parseInt(value.slice(0, -1) * 1e6);
|
||||||
|
if (metric === "B") return parseInt(value.slice(0, -1) * 1e9);
|
||||||
|
return parseInt(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove parent element (usually if child is clicked)
|
||||||
|
function removeParent() {
|
||||||
|
this.parentNode.parentNode.removeChild(this.parentNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return string with 1st char capitalized
|
||||||
|
function capitalize(string) {
|
||||||
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// transform string to array [translateX,translateY,rotateDeg,rotateX,rotateY,scale]
|
||||||
|
function parseTransform(string) {
|
||||||
|
if (!string) {return [0,0,0,0,0,1];}
|
||||||
|
const a = string.replace(/[a-z()]/g, "").replace(/[ ]/g, ",").split(",");
|
||||||
|
return [a[0] || 0, a[1] || 0, a[2] || 0, a[3] || 0, a[4] || 0, a[5] || 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// findAll d3.quandtree search from https://bl.ocks.org/lwthatcher/b41479725e0ff2277c7ac90df2de2b5e
|
||||||
|
void function addFindAll() {
|
||||||
|
const Quad = function(node, x0, y0, x1, y1) {
|
||||||
|
this.node = node;
|
||||||
|
this.x0 = x0;
|
||||||
|
this.y0 = y0;
|
||||||
|
this.x1 = x1;
|
||||||
|
this.y1 = y1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tree_filter = function(x, y, radius) {
|
||||||
|
var t = {x, y, x0: this._x0, y0: this._y0, x3: this._x1, y3: this._y1, quads: [], node: this._root};
|
||||||
|
if (t.node) {t.quads.push(new Quad(t.node, t.x0, t.y0, t.x3, t.y3))};
|
||||||
|
radiusSearchInit(t, radius);
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
while (t.q = t.quads.pop()) {
|
||||||
|
i++;
|
||||||
|
|
||||||
|
// Stop searching if this quadrant can’t contain a closer node.
|
||||||
|
if (!(t.node = t.q.node)
|
||||||
|
|| (t.x1 = t.q.x0) > t.x3
|
||||||
|
|| (t.y1 = t.q.y0) > t.y3
|
||||||
|
|| (t.x2 = t.q.x1) < t.x0
|
||||||
|
|| (t.y2 = t.q.y1) < t.y0) continue;
|
||||||
|
|
||||||
|
// Bisect the current quadrant.
|
||||||
|
if (t.node.length) {
|
||||||
|
t.node.explored = true;
|
||||||
|
var xm = (t.x1 + t.x2) / 2,
|
||||||
|
ym = (t.y1 + t.y2) / 2;
|
||||||
|
|
||||||
|
t.quads.push(
|
||||||
|
new Quad(t.node[3], xm, ym, t.x2, t.y2),
|
||||||
|
new Quad(t.node[2], t.x1, ym, xm, t.y2),
|
||||||
|
new Quad(t.node[1], xm, t.y1, t.x2, ym),
|
||||||
|
new Quad(t.node[0], t.x1, t.y1, xm, ym)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Visit the closest quadrant first.
|
||||||
|
if (t.i = (y >= ym) << 1 | (x >= xm)) {
|
||||||
|
t.q = t.quads[t.quads.length - 1];
|
||||||
|
t.quads[t.quads.length - 1] = t.quads[t.quads.length - 1 - t.i];
|
||||||
|
t.quads[t.quads.length - 1 - t.i] = t.q;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visit this point. (Visiting coincident points isn’t necessary!)
|
||||||
|
else {
|
||||||
|
var dx = x - +this._x.call(null, t.node.data),
|
||||||
|
dy = y - +this._y.call(null, t.node.data),
|
||||||
|
d2 = dx * dx + dy * dy;
|
||||||
|
radiusSearchVisit(t, d2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t.result;
|
||||||
|
}
|
||||||
|
d3.quadtree.prototype.findAll = tree_filter;
|
||||||
|
|
||||||
|
var radiusSearchInit = function(t, radius) {
|
||||||
|
t.result = [];
|
||||||
|
t.x0 = t.x - radius, t.y0 = t.y - radius;
|
||||||
|
t.x3 = t.x + radius, t.y3 = t.y + radius;
|
||||||
|
t.radius = radius * radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
var radiusSearchVisit = function(t, d2) {
|
||||||
|
t.node.data.scanned = true;
|
||||||
|
if (d2 < t.radius) {
|
||||||
|
do {t.result.push(t.node.data); t.node.data.selected = true;} while (t.node = t.node.next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// normalization function
|
||||||
|
function normalize(val, min, max) {
|
||||||
|
return Math.min(Math.max((val - min) / (max - min), 0), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return a random integer from min to max biased towards one end based on exponent distribution (the bigger ex the higher bias towards min)
|
||||||
|
// from https://gamedev.stackexchange.com/a/116875
|
||||||
|
function biased(min, max, ex) {
|
||||||
|
return Math.round(min + (max - min) * Math.pow(Math.random(), ex));
|
||||||
|
}
|
||||||
|
|
||||||
|
// return array of values common for both array a and array b
|
||||||
|
function intersect(a, b) {
|
||||||
|
const setB = new Set(b);
|
||||||
|
return [...new Set(a)].filter(a => setB.has(a));
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if char is vowel
|
||||||
|
function vowel(c) {
|
||||||
|
return "aeiouy".includes(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the last element of array
|
||||||
|
function last(array) {
|
||||||
|
return array[array.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// return value in range [0, 100] (height range)
|
||||||
|
function lim(v) {
|
||||||
|
return Math.max(Math.min(v, 100), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get number from string in format "1-3" or "2" or "0.5"
|
||||||
|
function getNumberInRange(r) {
|
||||||
|
if (typeof r !== "string") {console.error("The value should be a string", r); return 0;}
|
||||||
|
if (!isNaN(+r)) return +r;
|
||||||
|
const sign = r[0] === "-" ? -1 : 1;
|
||||||
|
if (isNaN(+r[0])) r = r.slice(1);
|
||||||
|
const range = r.includes("-") ? r.split("-") : null;
|
||||||
|
if (!range) {console.error("Cannot parse the number. Check the format", r); return 0;}
|
||||||
|
const count = rand(range[0] * sign, +range[1]);
|
||||||
|
if (isNaN(count) || count < 0) {console.error("Cannot parse number. Check the format", r); return 0;}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function analizeNamesbase() {
|
||||||
|
const result = [];
|
||||||
|
nameBases.forEach((b,i) => {
|
||||||
|
const d = nameBase[i];
|
||||||
|
const size = d.length;
|
||||||
|
const ar = d.map(n => n.length);
|
||||||
|
const min = d3.min(ar);
|
||||||
|
const max = d3.max(ar);
|
||||||
|
const mean = rn(d3.mean(ar), 1);
|
||||||
|
const median = d3.median(ar);
|
||||||
|
const lengths = new Uint8Array(max);
|
||||||
|
ar.forEach(l => lengths[l]++);
|
||||||
|
const common = d3.scan(lengths, (a,b) => b-a);
|
||||||
|
const string = d.join("");
|
||||||
|
const doubleArray = [];
|
||||||
|
let double = "";
|
||||||
|
for (let i=0; i<string.length; i++) {
|
||||||
|
if (!doubleArray[string[i]]) doubleArray[string[i]] = 0;
|
||||||
|
if (string[i] === string[i-1]) doubleArray[string[i]]++;
|
||||||
|
}
|
||||||
|
for (const l in doubleArray) {if(doubleArray[l] > size/35) double += l;}
|
||||||
|
const multi = rn(d3.mean(d.map(n => (n.match(/ /g)||[]).length)),2);
|
||||||
|
result.push({name:b.name, size, min, max, mean, median, common, double, multi});
|
||||||
|
});
|
||||||
|
console.table(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// polyfill for composedPath
|
||||||
|
function getComposedPath(node) {
|
||||||
|
let parent;
|
||||||
|
if (node.parentNode) parent = node.parentNode;
|
||||||
|
else if (node.host) parent = node.host;
|
||||||
|
else if (node.defaultView) parent = node.defaultView;
|
||||||
|
if (parent !== undefined) return [node].concat(getComposedPath(parent));
|
||||||
|
return [node];
|
||||||
|
};
|
||||||
|
|
||||||
|
// get next unused id
|
||||||
|
function getNextId(core, i = 1) {
|
||||||
|
while (document.getElementById(core+i)) i++;
|
||||||
|
return core + i;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAbsolutePath(href) {
|
||||||
|
if (!href) return "";
|
||||||
|
var link = document.createElement("a");
|
||||||
|
link.href = href;
|
||||||
|
return link.href;
|
||||||
|
}
|
||||||
82
modules/voronoi.js
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
(function (global, factory) {
|
||||||
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||||
|
typeof define === 'function' && define.amd ? define(factory) :
|
||||||
|
(global.Voronoi = factory());
|
||||||
|
}(this, (function () { 'use strict';
|
||||||
|
|
||||||
|
var Voronoi = function Voronoi(delaunay, points, pointsN) {
|
||||||
|
const cells = {v: [], c: [], b: []}; // voronoi cells: v = cell vertices, c = adjacent cells, b = near-border cell
|
||||||
|
const vertices = {p: [], v: [], c: []}; // cells vertices: p = vertex coordinates, v = neighboring vertices, c = adjacent cells
|
||||||
|
|
||||||
|
for (let e=0; e < delaunay.triangles.length; e++) {
|
||||||
|
|
||||||
|
const p = delaunay.triangles[nextHalfedge(e)];
|
||||||
|
if (p < pointsN && !cells.c[p]) {
|
||||||
|
const edges = edgesAroundPoint(e);
|
||||||
|
cells.v[p] = edges.map(e => triangleOfEdge(e)); // cell: adjacent vertex
|
||||||
|
cells.c[p] = edges.map(e => delaunay.triangles[e]).filter(c => c < pointsN); // cell: adjacent valid cells
|
||||||
|
cells.b[p] = edges.length > cells.c[p].length ? 1 : 0; // cell: is border
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = triangleOfEdge(e);
|
||||||
|
if (!vertices.p[t]) {
|
||||||
|
vertices.p[t] = triangleCenter(t); // vertex: coordinates
|
||||||
|
vertices.v[t] = trianglesAdjacentToTriangle(t); // vertex: adjacent vertices
|
||||||
|
vertices.c[t] = pointsOfTriangle(t); // vertex: adjacent cells
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointsOfTriangle(t) {
|
||||||
|
return edgesOfTriangle(t).map(e => delaunay.triangles[e]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function trianglesAdjacentToTriangle(t) {
|
||||||
|
let triangles = [];
|
||||||
|
for (let e of edgesOfTriangle(t)) {
|
||||||
|
let opposite = delaunay.halfedges[e];
|
||||||
|
triangles.push(triangleOfEdge(opposite));
|
||||||
|
}
|
||||||
|
return triangles;
|
||||||
|
}
|
||||||
|
|
||||||
|
function edgesAroundPoint(start) {
|
||||||
|
let result = [], incoming = start;
|
||||||
|
do {
|
||||||
|
result.push(incoming);
|
||||||
|
const outgoing = nextHalfedge(incoming);
|
||||||
|
incoming = delaunay.halfedges[outgoing];
|
||||||
|
} while (incoming !== -1 && incoming !== start && result.length < 20);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function triangleCenter(t) {
|
||||||
|
let vertices = pointsOfTriangle(t).map(p => points[p]);
|
||||||
|
return circumcenter(vertices[0], vertices[1], vertices[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {cells, vertices}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function edgesOfTriangle(t) {return [3*t, 3*t+1, 3*t+2];}
|
||||||
|
|
||||||
|
function triangleOfEdge(e) {return Math.floor(e/3);}
|
||||||
|
|
||||||
|
function nextHalfedge(e) {return (e % 3 === 2) ? e-2 : e+1;}
|
||||||
|
|
||||||
|
function prevHalfedge(e) {return (e % 3 === 0) ? e+2 : e-1;}
|
||||||
|
|
||||||
|
function circumcenter(a, b, c) {
|
||||||
|
let ad = a[0]*a[0] + a[1]*a[1],
|
||||||
|
bd = b[0]*b[0] + b[1]*b[1],
|
||||||
|
cd = c[0]*c[0] + c[1]*c[1];
|
||||||
|
let D = 2 * (a[0] * (b[1] - c[1]) + b[0] * (c[1] - a[1]) + c[0] * (a[1] - b[1]));
|
||||||
|
return [
|
||||||
|
Math.floor(1/D * (ad * (b[1] - c[1]) + bd * (c[1] - a[1]) + cd * (a[1] - b[1]))),
|
||||||
|
Math.floor(1/D * (ad * (c[0] - b[0]) + bd * (a[0] - c[0]) + cd * (b[0] - a[0])))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Voronoi;
|
||||||
|
|
||||||
|
})));
|
||||||