p&&(c[1]=-1);1d||1l;l++)d[4*(h+a.width)+l]=d[4*(h+2*a.width)+l],d[4*(h+a.width*(a.height-2))+l]=d[4*(h+a.width*(a.height-3))+l];for(h=2;hl;l++)d[4*(h*a.width+1)+l]=d[4*(h*a.width+2)+l],d[4*((h+1)*a.width-2)+l]=d[4*((h+1)*a.width-
-3)+l];for(l=0;4>l;l++)d[4*(a.width+1)+l]=d[4*(2*a.width+2)+l],d[4*(2*a.width-2)+l]=d[4*(3*a.width-3)+l],d[4*(a.width*(a.height-2)+1)+l]=d[4*(a.width*(a.height-3)+2)+l],d[4*(a.width*(a.height-1)-2)+l]=d[4*(a.width*(a.height-2)-3)+l];for(h=1;hl;l++)d[4*h+l]=d[4*(h+a.width)+l],d[4*(h+a.width*(a.height-1))+l]=d[4*(h+a.width*(a.height-2))+l];for(h=1;hl;l++)d[h*a.width*4+l]=d[4*(h*a.width+1)+l],d[4*((h+1)*a.width-1)+l]=d[4*((h+1)*a.width-2)+l];for(l=0;4>
-l;l++)d[l]=d[4*(a.width+1)+l],d[4*(a.width-1)+l]=d[4*(2*a.width-2)+l],d[a.width*(a.height-1)*4+l]=d[4*(a.width*(a.height-2)+1)+l],d[4*(a.width*a.height-1)+l]=d[4*(a.width*(a.height-1)-2)+l];c.putImageData(e,0,0);y++;6==y&&(na=this.width,P.appendChild(R),g())};for(h=0;6>h;h++)c=new Image,c.crossOrigin=Z.crossOrigin?Z.crossOrigin:"anonymous",c.side=h,c.onload=p,c.src="multires"==G?encodeURI(ja.replace("%s",z[h])+"."+s.extension):encodeURI(s[h].src)}else{if(!a)throw console.log("Error: no WebGL support detected!"),
-{type:"no webgl"};s.fullpath=s.basePath?s.basePath+s.path:s.path;s.invTileResolution=1/s.tileResolution;e=Ca();ua=[];for(h=0;6>h;h++)ua[h]=e.slice(12*h,12*h+12),e=Ca();if("equirectangular"==G){if(h=Math.max(s.width,s.height),e=a.getParameter(a.MAX_TEXTURE_SIZE),h>e)throw console.log("Error: The image is too big; it's "+h+"px wide, but this device's maximum supported width is "+e+"px."),{type:"webgl size error",width:h,maxWidth:e};}else if("cubemap"==G&&(h=s[0].width,e=a.getParameter(a.MAX_CUBE_MAP_TEXTURE_SIZE),
-h>e))throw console.log("Error: The cube face image is too big; it's "+h+"px wide, but this device's maximum supported width is "+e+"px."),{type:"webgl size error",width:h,maxWidth:e};t===m||t.horizonPitch===m&&t.horizonRoll===m||(fa=[t.horizonPitch==m?0:t.horizonPitch,t.horizonRoll==m?0:t.horizonRoll]);h=a.TEXTURE_2D;a.viewport(0,0,a.drawingBufferWidth,a.drawingBufferHeight);V=a.createShader(a.VERTEX_SHADER);e=r;"multires"==G&&(e=v);a.shaderSource(V,e);a.compileShader(V);N=a.createShader(a.FRAGMENT_SHADER);
-e=Na;"cubemap"==G?(h=a.TEXTURE_CUBE_MAP,e=Oa):"multires"==G&&(e=oa);a.shaderSource(N,e);a.compileShader(N);d=a.createProgram();a.attachShader(d,V);a.attachShader(d,N);a.linkProgram(d);a.getShaderParameter(V,a.COMPILE_STATUS)||console.log(a.getShaderInfoLog(V));a.getShaderParameter(N,a.COMPILE_STATUS)||console.log(a.getShaderInfoLog(N));a.getProgramParameter(d,a.LINK_STATUS)||console.log(a.getProgramInfoLog(d));a.useProgram(d);d.drawInProgress=!1;d.texCoordLocation=a.getAttribLocation(d,"a_texCoord");
-a.enableVertexAttribArray(d.texCoordLocation);"multires"!=G?(ka||(ka=a.createBuffer()),a.bindBuffer(a.ARRAY_BUFFER,ka),a.bufferData(a.ARRAY_BUFFER,new Float32Array([-1,1,1,1,1,-1,-1,1,1,-1,-1,-1]),a.STATIC_DRAW),a.vertexAttribPointer(d.texCoordLocation,2,a.FLOAT,!1,0,0),d.aspectRatio=a.getUniformLocation(d,"u_aspectRatio"),a.uniform1f(d.aspectRatio,a.drawingBufferWidth/a.drawingBufferHeight),d.psi=a.getUniformLocation(d,"u_psi"),d.theta=a.getUniformLocation(d,"u_theta"),d.f=a.getUniformLocation(d,
-"u_f"),d.h=a.getUniformLocation(d,"u_h"),d.v=a.getUniformLocation(d,"u_v"),d.vo=a.getUniformLocation(d,"u_vo"),d.rot=a.getUniformLocation(d,"u_rot"),a.uniform1f(d.h,ja/(2*Math.PI)),a.uniform1f(d.v,p/Math.PI),a.uniform1f(d.vo,c/Math.PI*2),"equirectangular"==G&&(d.backgroundColor=a.getUniformLocation(d,"u_backgroundColor"),a.uniform4fv(d.backgroundColor,(t.backgroundColor?t.backgroundColor:[0,0,0]).concat([1]))),d.texture=a.createTexture(),a.bindTexture(h,d.texture),"cubemap"==G?(a.texImage2D(a.TEXTURE_CUBE_MAP_POSITIVE_X,
-0,a.RGB,a.RGB,a.UNSIGNED_BYTE,s[1]),a.texImage2D(a.TEXTURE_CUBE_MAP_NEGATIVE_X,0,a.RGB,a.RGB,a.UNSIGNED_BYTE,s[3]),a.texImage2D(a.TEXTURE_CUBE_MAP_POSITIVE_Y,0,a.RGB,a.RGB,a.UNSIGNED_BYTE,s[4]),a.texImage2D(a.TEXTURE_CUBE_MAP_NEGATIVE_Y,0,a.RGB,a.RGB,a.UNSIGNED_BYTE,s[5]),a.texImage2D(a.TEXTURE_CUBE_MAP_POSITIVE_Z,0,a.RGB,a.RGB,a.UNSIGNED_BYTE,s[0]),a.texImage2D(a.TEXTURE_CUBE_MAP_NEGATIVE_Z,0,a.RGB,a.RGB,a.UNSIGNED_BYTE,s[2])):a.texImage2D(h,0,a.RGB,a.RGB,a.UNSIGNED_BYTE,s),a.texParameteri(h,a.TEXTURE_WRAP_S,
-a.CLAMP_TO_EDGE),a.texParameteri(h,a.TEXTURE_WRAP_T,a.CLAMP_TO_EDGE),a.texParameteri(h,a.TEXTURE_MIN_FILTER,a.LINEAR),a.texParameteri(h,a.TEXTURE_MAG_FILTER,a.LINEAR)):(d.vertPosLocation=a.getAttribLocation(d,"a_vertCoord"),a.enableVertexAttribArray(d.vertPosLocation),F||(F=a.createBuffer()),ba||(ba=a.createBuffer()),Da||(Da=a.createBuffer()),a.bindBuffer(a.ARRAY_BUFFER,ba),a.bufferData(a.ARRAY_BUFFER,new Float32Array([0,0,1,0,1,1,0,1]),a.STATIC_DRAW),a.bindBuffer(a.ELEMENT_ARRAY_BUFFER,Da),a.bufferData(a.ELEMENT_ARRAY_BUFFER,
-new Uint16Array([0,1,2,0,2,3]),a.STATIC_DRAW),d.perspUniform=a.getUniformLocation(d,"u_perspMatrix"),d.cubeUniform=a.getUniformLocation(d,"u_cubeMatrix"),d.level=-1,d.currentNodes=[],d.nodeCache=[],d.nodeCacheTimestamp=0);ja=a.getError();if(0!==ja)throw console.log("Error: Something went wrong with WebGL!",ja),{type:"webgl error"};g()}};this.destroy=function(){P!==m&&(A!==m&&P.contains(A)&&P.removeChild(A),R!==m&&P.contains(R)&&P.removeChild(R));if(a){var d=a.getExtension("WEBGL_lose_context");d&&
-d.loseContext()}};this.resize=function(){var h=J.devicePixelRatio||1;A.width=A.clientWidth*h;A.height=A.clientHeight*h;a&&(1286==a.getError()&&ta(),a.viewport(0,0,a.drawingBufferWidth,a.drawingBufferHeight),"multires"!=G&&a.uniform1f(d.aspectRatio,A.clientWidth/A.clientHeight))};this.resize();this.setPose=function(a,d){fa=[a,d]};this.render=function(h,e,f,r){var p;p=0;r===m&&(r={});r.roll&&(p=r.roll);if(fa!==m){var c=fa[0],g=fa[1],t=h,z=e,y=Math.cos(g)*Math.sin(h)*Math.sin(c)+Math.cos(h)*(Math.cos(c)*
-Math.cos(e)+Math.sin(g)*Math.sin(c)*Math.sin(e)),v=-Math.sin(h)*Math.sin(g)+Math.cos(h)*Math.cos(g)*Math.sin(e);h=Math.cos(g)*Math.cos(c)*Math.sin(h)+Math.cos(h)*(-Math.cos(e)*Math.sin(c)+Math.cos(c)*Math.sin(g)*Math.sin(e));h=Math.asin(Math.max(Math.min(h,1),-1));e=Math.atan2(v,y);c=[Math.cos(t)*(Math.sin(g)*Math.sin(c)*Math.cos(z)-Math.cos(c)*Math.sin(z)),Math.cos(t)*Math.cos(g)*Math.cos(z),Math.cos(t)*(Math.cos(c)*Math.sin(g)*Math.cos(z)+Math.sin(z)*Math.sin(c))];g=[-Math.cos(h)*Math.sin(e),Math.cos(h)*
-Math.cos(e)];g=Math.acos(Math.max(Math.min((c[0]*g[0]+c[1]*g[1])/(Math.sqrt(c[0]*c[0]+c[1]*c[1]+c[2]*c[2])*Math.sqrt(g[0]*g[0]+g[1]*g[1])),1),-1));0>c[2]&&(g=2*Math.PI-g);p+=g}if(a||"multires"!=G&&"cubemap"!=G){if("multires"!=G)f=2*Math.atan(Math.tan(0.5*f)/(a.drawingBufferWidth/a.drawingBufferHeight)),f=1/Math.tan(0.5*f),a.uniform1f(d.psi,e),a.uniform1f(d.theta,h),a.uniform1f(d.rot,p),a.uniform1f(d.f,f),!0===va&&"equirectangular"==G&&(a.bindTexture(a.TEXTURE_2D,d.texture),a.texImage2D(a.TEXTURE_2D,
-0,a.RGB,a.RGB,a.UNSIGNED_BYTE,s)),a.drawArrays(a.TRIANGLES,0,6);else{c=a.drawingBufferWidth/a.drawingBufferHeight;g=2*Math.atan(Math.tan(f/2)*a.drawingBufferHeight/a.drawingBufferWidth);g=1/Math.tan(g/2);c=[g/c,0,0,0,0,g,0,0,0,0,100.1/-99.9,20/-99.9,0,0,-1,0];for(g=1;gs.tileResolution*Math.pow(2,g-1)*Math.tan(f/2)*0.707;)g++;d.level=g;g=[1,0,0,0,1,0,0,0,1];g=ra(g,-p,"z");g=ra(g,-h,"x");g=ra(g,e,"y");g=[g[0],g[1],g[2],0,g[3],g[4],g[5],0,g[6],g[7],g[8],0,0,0,0,1];a.uniformMatrix4fv(d.perspUniform,
-!1,new Float32Array(sa(c)));a.uniformMatrix4fv(d.cubeUniform,!1,new Float32Array(sa(g)));c=[c[0]*g[0],c[0]*g[1],c[0]*g[2],0,c[5]*g[4],c[5]*g[5],c[5]*g[6],0,c[10]*g[8],c[10]*g[9],c[10]*g[10],c[11],-g[8],-g[9],-g[10],0];d.nodeCache.sort(bb);if(200d.currentNodes.length+50)for(g=d.nodeCache.splice(200,d.nodeCache.length-200),p=0;pp;p++)t=new X(ua[p],g[p],1,0,0,s.fullpath),
-Y(c,t,h,e,f);d.currentNodes.sort(W);for(p=0;pp;p++)f=R.querySelector(".pnlm-"+e[p]+"face").style,f.webkitTransform=h+r[e[p]],f.transform=h+r[e[p]]};this.isLoading=function(){if(a&&"multires"==G)for(var f=0;f u_h || coord.y < -u_v + u_vo || coord.y > u_v + u_vo)\ngl_FragColor = u_backgroundColor;\nelse\ngl_FragColor = texture2D(u_image, vec2((coord.x + u_h) / (u_h * 2.0), (-coord.y + u_v + u_vo) / (u_v * 2.0)));\n}",
-oa="varying mediump vec2 v_texCoord;uniform sampler2D u_sampler;void main(void) {gl_FragColor = texture2D(u_sampler, v_texCoord);}";return{renderer:function(f,m,r,v){return new Ba(f,m,r,v)}}}(window,document);window.requestAnimationFrame||(window.requestAnimationFrame=function(){return window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(J,f){window.setTimeout(J,1E3/60)}}());
-window.pannellum=function(J,f,m){function Ba(r,v){function Oa(u){J.removeEventListener("deviceorientation",Oa);u&&null!==u.alpha&&null!==u.beta&&null!==u.gamma?(w.container.appendChild(w.orientation),pa=!0,Ya&&Ka()):pa=!1}function Na(){var u=f.createElement("div");u.innerHTML="\x3c!--[if lte IE 9]>u;u++)O.push(new Image),O[u].crossOrigin=b.crossOrigin;n.load.lbox.style.display=
-"block";n.load.lbar.style.display="none"}else if("multires"==b.type)u=JSON.parse(JSON.stringify(b.multiRes)),b.basePath&&b.multiRes.basePath&&!/^(?:[a-z]+:)?\/\//i.test(b.multiRes.basePath)?u.basePath=b.basePath+b.multiRes.basePath:b.multiRes.basePath?u.basePath=b.multiRes.basePath:b.basePath&&(u.basePath=b.basePath),O=u;else if(!0===b.dynamic)O=b.panorama;else{if(b.panorama===m){W(b.strings.noPanoramaError);return}O=new Image}if("cubemap"==b.type)for(var ca=6,c=function(){ca--;0===ca&&P()},d=function(a){var u=
-f.createElement("a");u.href=a.target.src;u.innerHTML=u.href;W(b.strings.fileAccessError.replace("%s",u.outerHTML))},u=0;uc||65536")+12),e=function(a){var u;0<=d.indexOf(a+'="')?(u=d.substring(d.indexOf(a+'="')+a.length+2),u=u.substring(0,u.indexOf('"'))):
-0<=d.indexOf(a+">")&&(u=d.substring(d.indexOf(a+">")+a.length+1),u=u.substring(0,u.indexOf("<")));return u!==m?Number(u):null},ca=e("GPano:FullPanoWidthPixels"),c=e("GPano:CroppedAreaImageWidthPixels"),f=e("GPano:FullPanoHeightPixels"),g=e("GPano:CroppedAreaImageHeightPixels"),h=e("GPano:CroppedAreaTopPixels"),l=e("GPano:PoseHeadingDegrees"),p=e("GPano:PosePitchDegrees"),e=e("GPano:PoseRollDegrees");null!==ca&&null!==c&&null!==f&&null!==g&&null!==h&&(0>da.indexOf("haov")&&(b.haov=c/ca*360),0>da.indexOf("vaov")&&
-(b.vaov=g/f*180),0>da.indexOf("vOffset")&&(b.vOffset=-180*((h+g/2)/f-0.5)),null!==l&&0>da.indexOf("northOffset")&&(b.northOffset=l,!1!==b.compass&&(b.compass=!0)),null!==p&&null!==e&&(0>da.indexOf("horizonPitch")&&(b.horizonPitch=p),0>da.indexOf("horizonRoll")&&(b.horizonRoll=e)))}O.src=J.URL.createObjectURL(a)});k.readAsBinaryString!==m?k.readAsBinaryString(a):k.readAsText(a)}function W(a){a===m&&(a=b.strings.genericWebGLError);n.errorMsg.innerHTML=""+a+"
";w.load.style.display="none";n.load.box.style.display=
-"none";n.errorMsg.style.display="table";Pa=!0;M.style.display="none";ga("error",a)}function X(a){var b=Y(a);ha.style.left=b.x+"px";ha.style.top=b.y+"px";clearTimeout(X.t1);clearTimeout(X.t2);ha.style.display="block";ha.style.opacity=1;X.t1=setTimeout(function(){ha.style.opacity=0},2E3);X.t2=setTimeout(function(){ha.style.display="none"},2500);a.preventDefault()}function Y(a){var b=r.getBoundingClientRect(),c={};c.x=a.clientX-b.left;c.y=a.clientY-b.top;return c}function Ca(a){a.preventDefault();r.focus();
-if(K&&b.draggable){var k=Y(a);if(b.hotSpotDebug){var c=sa(a);console.log("Pitch: "+c[0]+", Yaw: "+c[1]+", Center Pitch: "+b.pitch+", Center Yaw: "+b.yaw+", HFOV: "+b.hfov)}Ja();l();b.roll=0;x.hfov=0;la=!0;S=Date.now();xa=k.x;ya=k.y;Qa=b.yaw;Ra=b.pitch;C.classList.add("pnlm-grabbing");C.classList.remove("pnlm-grab");ga("mousedown",a);F()}}function ra(a){b.minHfov===b.hfov?za.setHfov(wa,1E3):(a=sa(a),za.lookAt(a[0],a[1],b.minHfov,1E3))}function sa(a){var k=Y(a);a=B.getCanvas();var c=a.clientWidth,d=
-a.clientHeight;a=k.x/c*2-1;var d=(1-k.y/d*2)*d/c,e=1/Math.tan(b.hfov*Math.PI/360),f=Math.sin(b.pitch*Math.PI/180),g=Math.cos(b.pitch*Math.PI/180),k=e*g-d*f,c=Math.sqrt(a*a+k*k),d=180*Math.atan((d*g+e*f)/c)/Math.PI;a=180*Math.atan2(a/c,k/c)/Math.PI+b.yaw;-180>a&&(a+=360);180a.wheelDelta?1:-1):a.wheelDelta?(U(b.hfov-0.05*a.wheelDelta),x.hfov=0>a.wheelDelta?1:-1):a.detail&&(U(b.hfov+1.5*a.detail),x.hfov=0Za.indexOf(k)||(a.preventDefault(),
-27==k?Ga&&z():s(k,!0))}function ua(){for(var a=0;10>a;a++)q[a]=!1}function fa(a){var b=a.which||a.keycode;0>Za.indexOf(b)||(a.preventDefault(),s(b,!1))}function s(a,b){var c=!1;switch(a){case 109:case 189:case 17:case 173:q[0]!=b&&(c=!0);q[0]=b;break;case 107:case 187:case 16:case 61:q[1]!=b&&(c=!0);q[1]=b;break;case 38:q[2]!=b&&(c=!0);q[2]=b;break;case 87:q[6]!=b&&(c=!0);q[6]=b;break;case 40:q[3]!=b&&(c=!0);q[3]=b;break;case 83:q[7]!=b&&(c=!0);q[7]=b;break;case 37:q[4]!=b&&(c=!0);q[4]=b;break;case 65:q[8]!=
-b&&(c=!0);q[8]=b;break;case 39:q[5]!=b&&(c=!0);q[5]=b;break;case 68:q[9]!=b&&(c=!0),q[9]=b}c&&b&&(ia="undefined"!==typeof performance&&performance.now()?performance.now():Date.now(),F())}function G(){if(K){var a=!1,k=b.pitch,c=b.yaw,d=b.hfov,e;e="undefined"!==typeof performance&&performance.now()?performance.now():Date.now();ia===m&&(ia=e);var f=(e-ia)*b.hfov/1700,f=Math.min(f,1);q[0]&&!0===b.keyboardZoom&&(U(b.hfov+(0.8*x.hfov+0.5)*f),a=!0);q[1]&&!0===b.keyboardZoom&&(U(b.hfov+(0.8*x.hfov-0.2)*f),
-a=!0);if(q[2]||q[6])b.pitch+=(0.8*x.pitch+0.2)*f,a=!0;if(q[3]||q[7])b.pitch+=(0.8*x.pitch-0.2)*f,a=!0;if(q[4]||q[8])b.yaw+=(0.8*x.yaw-0.2)*f,a=!0;if(q[5]||q[9])b.yaw+=(0.8*x.yaw+0.2)*f,a=!0;a&&(S=Date.now());Date.now();if(b.autoRotate){if(0.001=b.autoRotateStopDelay&&(b.autoRotateStopDelay=!1,$=b.autoRotate,
-b.autoRotate=0))}L.pitch&&(va("pitch"),k=b.pitch);L.yaw&&(va("yaw"),c=b.yaw);L.hfov&&(va("hfov"),d=b.hfov);0k.startPosition&&c>=k.endPosition||k.endPositionb.autoRotateInactivityDelay&&!b.autoRotate&&(b.autoRotate=$,za.lookAt(Ea,m,wa,3E3)),
-requestAnimationFrame(ba);else if(B&&(B.isLoading()||!0===b.dynamic&&$a))requestAnimationFrame(ba);else{Ta=!1;ia=m;var a=b.autoRotateInactivityDelay-(Date.now()-S);0b.yaw&&(b.yaw+=360);a=b.yaw;var k=b.maxYaw-b.minYaw,d=-180,e=180;360>k&&(d=b.minYaw+b.hfov/2,e=b.maxYaw-b.hfov/2,kaa?aa+=1:10===aa?(ab=c[2]/Math.PI*180+b.yaw,aa=!0,requestAnimationFrame(ba)):(b.pitch=c[0]/Math.PI*180,b.roll=-c[1]/Math.PI*180,b.yaw=-c[2]/Math.PI*180+ab)}function h(){try{var a={};b.horizonPitch!==m&&(a.horizonPitch=b.horizonPitch*Math.PI/180);b.horizonRoll!==m&&(a.horizonRoll=b.horizonRoll*Math.PI/180);b.backgroundColor!==m&&(a.backgroundColor=
-b.backgroundColor);B.init(O,b.type,b.dynamic,b.haov*Math.PI/180,b.vaov*Math.PI/180,b.vOffset*Math.PI/180,e,a);!0!==b.dynamic&&(O=m)}catch(c){if("webgl error"==c.type||"no webgl"==c.type)W();else if("webgl size error"==c.type)W(b.strings.textureSizeError.replace("%s",c.width).replace("%s",c.maxWidth));else throw W(b.strings.unknownError),c;}}function e(){if(b.sceneFadeDuration&&B.fadeImg!==m){B.fadeImg.style.opacity=0;var a=B.fadeImg;delete B.fadeImg;setTimeout(function(){M.removeChild(a);ga("scenechangefadedone")},
-b.sceneFadeDuration)}Ha.style.display=b.compass?"inline":"none";ja();n.load.box.style.display="none";qa!==m&&(M.removeChild(qa),qa=m);K=!0;ga("load");F()}function Ia(a){a.pitch=Number(a.pitch)||0;a.yaw=Number(a.yaw)||0;var c=f.createElement("div");c.className="pnlm-hotspot-base";c.className=a.cssClass?c.className+(" "+a.cssClass):c.className+(" pnlm-hotspot pnlm-sprite pnlm-"+D(a.type));var d=f.createElement("span");a.text&&(d.innerHTML=D(a.text));var e;if(a.video){e=f.createElement("video");var g=
-a.video;b.basePath&&!oa(g)&&(g=b.basePath+g);e.src=encodeURI(g);e.controls=!0;e.style.width=a.width+"px";M.appendChild(c);d.appendChild(e)}else if(a.image){g=a.image;b.basePath&&!oa(g)&&(g=b.basePath+g);e=f.createElement("a");e.href=encodeURI(a.URL?a.URL:g);e.target="_blank";d.appendChild(e);var h=f.createElement("img");h.src=encodeURI(g);h.style.width=a.width+"px";h.style.paddingTop="5px";M.appendChild(c);e.appendChild(h);d.style.maxWidth="initial"}else a.URL?(e=f.createElement("a"),e.href=encodeURI(a.URL),
-e.target="_blank",M.appendChild(e),c.style.cursor="pointer",d.style.cursor="pointer",e.appendChild(c)):(a.sceneId&&(c.onclick=c.ontouchend=function(){c.clicked||(c.clicked=!0,I(a.sceneId,a.targetPitch,a.targetYaw,a.targetHfov));return!1},c.style.cursor="pointer",d.style.cursor="pointer"),M.appendChild(c));if(a.createTooltipFunc)a.createTooltipFunc(c,a.createTooltipArgs);else if(a.text||a.video||a.image)c.classList.add("pnlm-tooltip"),c.appendChild(d),d.style.width=d.scrollWidth-20+"px",d.style.marginLeft=
--(d.scrollWidth-c.offsetWidth)/2+"px",d.style.marginTop=-d.scrollHeight-12+"px";a.clickHandlerFunc&&(c.addEventListener("click",function(b){a.clickHandlerFunc(b,a.clickHandlerArgs)},"false"),c.style.cursor="pointer",d.style.cursor="pointer");a.div=c}function ja(){Va||(b.hotSpots?(b.hotSpots=b.hotSpots.sort(function(a,b){return a.pitch=a.yaw&&-90=h||(90=a.yaw)&&0>=h)a.div.style.visibility="hidden";else{var l=Math.sin((-a.yaw+b.yaw)*Math.PI/180),p=Math.tan(b.hfov*Math.PI/360);a.div.style.visibility="visible";var m=B.getCanvas(),n=m.clientWidth,
-m=m.clientHeight,c=[-n/p*l*d/h/2,-n/p*(c*f-d*g*e)/h/2],d=Math.sin(b.roll*Math.PI/180),e=Math.cos(b.roll*Math.PI/180),c=[c[0]*e-c[1]*d,c[0]*d+c[1]*e];c[0]+=(n-a.div.offsetWidth)/2;c[1]+=(m-a.div.offsetHeight)/2;n="translate("+c[0]+"px, "+c[1]+"px) translateZ(9999px) rotate("+b.roll+"deg)";a.div.style.webkitTransform=n;a.div.style.MozTransform=n;a.div.style.transform=n}}function g(a){b={};var c,d,e="haov vaov vOffset northOffset horizonPitch horizonRoll".split(" ");da=[];for(c in Wa)Wa.hasOwnProperty(c)&&
-(b[c]=Wa[c]);for(c in v.default)if(v.default.hasOwnProperty(c))if("strings"==c)for(d in v.default.strings)v.default.strings.hasOwnProperty(d)&&(b.strings[d]=D(v.default.strings[d]));else b[c]=v.default[c],0<=e.indexOf(c)&&da.push(c);if(null!==a&&""!==a&&v.scenes&&v.scenes[a]){var f=v.scenes[a];for(c in f)if(f.hasOwnProperty(c))if("strings"==c)for(d in f.strings)f.strings.hasOwnProperty(d)&&(b.strings[d]=D(f.strings[d]));else b[c]=f[c],0<=e.indexOf(c)&&da.push(c);b.scene=a}for(c in v)if(v.hasOwnProperty(c))if("strings"==
-c)for(d in v.strings)v.strings.hasOwnProperty(d)&&(b.strings[d]=D(v.strings[d]));else b[c]=v[c],0<=e.indexOf(c)&&da.push(c)}function t(a){if((a=a?a:!1)&&"preview"in b){var c=b.preview;b.basePath&&!oa(c)&&(c=b.basePath+c);qa=f.createElement("div");qa.className="pnlm-preview-img";qa.style.backgroundImage="url('"+encodeURI(c)+"')";M.appendChild(qa)}var c=b.title,d=b.author;a&&("previewTitle"in b&&(b.title=b.previewTitle),"previewAuthor"in b&&(b.author=b.previewAuthor));b.hasOwnProperty("title")||(n.title.innerHTML=
-"");b.hasOwnProperty("author")||(n.author.innerHTML="");b.hasOwnProperty("title")||b.hasOwnProperty("author")||(n.container.style.display="none");w.load.innerHTML=""+b.strings.loadButtonLabel+"
";n.load.boxp.innerHTML=b.strings.loadingLabel;for(var e in b)if(b.hasOwnProperty(e))switch(e){case "title":n.title.innerHTML=D(b[e]);n.container.style.display="inline";break;case "author":n.author.innerHTML=b.strings.bylineLabel.replace("%s",D(b[e]));n.container.style.display="inline";break;case "fallback":n.errorMsg.innerHTML=
-'Your browser does not support WebGL.
Click here to view this panorama in an alternative viewer.
';break;case "hfov":U(Number(b[e]));break;case "autoLoad":!0===b[e]&&B===m&&(n.load.box.style.display="inline",w.load.style.display="none",Na());break;case "showZoomCtrl":w.zoom.style.display=b[e]&&!1!=b.showControls?"block":"none";break;case "showFullscreenCtrl":w.fullscreen.style.display=b[e]&&!1!=b.showControls&&("fullscreen"in f||"mozFullScreen"in
-f||"webkitIsFullScreen"in f||"msFullscreenElement"in f)?"block":"none";break;case "hotSpotDebug":Xa.style.display=b[e]?"block":"none";break;case "showControls":b[e]||(w.orientation.style.display="none",w.zoom.style.display="none",w.fullscreen.style.display="none");break;case "orientationOnByDefault":b[e]&&(pa===m?Ya=!0:!0===pa&&Ka())}a&&(c?b.title=c:delete b.title,d?b.author=d:delete b.author)}function z(){if(K&&!Pa)if(Ga)f.exitFullscreen?f.exitFullscreen():f.mozCancelFullScreen?f.mozCancelFullScreen():
-f.webkitCancelFullScreen?f.webkitCancelFullScreen():f.msExitFullscreen&&f.msExitFullscreen();else try{r.requestFullscreen?r.requestFullscreen():r.mozRequestFullScreen?r.mozRequestFullScreen():r.msRequestFullscreen?r.msRequestFullscreen():r.webkitRequestFullScreen()}catch(a){}}function y(){f.fullscreen||f.mozFullScreen||f.webkitIsFullScreen||f.msFullscreenElement?(w.fullscreen.classList.add("pnlm-fullscreen-toggle-button-active"),Ga=!0):(w.fullscreen.classList.remove("pnlm-fullscreen-toggle-button-active"),
-Ga=!1);B.resize();U(b.hfov);F()}function E(a){var c=b.minHfov;"multires"==b.type&&B&&(c=Math.min(c,B.getCanvas().width/(b.multiRes.cubeResolution/90*0.9)));return c>b.maxHfov?(console.log("HFOV bounds do not make sense (minHfov > maxHfov)."),b.hfov):ab.maxHfov?b.maxHfov:a}function U(a){b.hfov=E(a)}function Ja(){L={};$=b.autoRotate?b.autoRotate:$;b.autoRotate=!1}function Q(){Pa&&(n.load.box.style.display="none",n.errorMsg.style.display="none",Pa=!1,ga("errorcleared"));K=!1;w.load.style.display=
-"none";n.load.box.style.display="inline";Na()}function I(a,c,d,e,f){K=!1;L={};var h,l;if(b.sceneFadeDuration&&!f&&(h=B.render(b.pitch*Math.PI/180,b.yaw*Math.PI/180,b.hfov*Math.PI/180,{returnImage:!0}),h!==m)){f=new Image;f.className="pnlm-fade-img";f.style.transition="opacity "+b.sceneFadeDuration/1E3+"s";f.style.width="100%";f.style.height="100%";f.onload=function(){I(a,c,d,e,!0)};f.src=h;M.appendChild(f);B.fadeImg=f;return}f="same"===c?b.pitch:c;h="same"===d?b.yaw:"sameAzimuth"===d?b.yaw+(b.northOffset||
-0)-(v.scenes[a].northOffset||0):d;l="same"===e?b.hfov:e;p();g(a);x.yaw=x.pitch=x.hfov=0;t();f!==m&&(b.pitch=f);h!==m&&(b.yaw=h);l!==m&&(b.hfov=l);ga("scenechange",a);Q()}function l(){J.removeEventListener("deviceorientation",Ma);w.orientation.classList.remove("pnlm-orientation-button-active");aa=!1}function Ka(){aa=1;J.addEventListener("deviceorientation",Ma);w.orientation.classList.add("pnlm-orientation-button-active")}function D(a){return v.escapeHTML?String(a).split(/&/g).join("&").split('"').join(""").split("'").join("'").split("<").join("<").split(">").join(">").split("/").join("/").split("\n").join("
"):
-String(a).split("\n").join("
")}function ga(a){if(a in T)for(var b=T[a].length;0a?2*a*a:-1+(4-2*a)*a},draggable:!0,disableKeyboardCtrl:!1,crossOrigin:"anonymous",strings:{loadButtonLabel:"Click to
Load
Panorama",loadingLabel:"Loading...",bylineLabel:"by %s",noPanoramaError:"No panorama image was specified.",
-fileAccessError:"The file %s could not be accessed.",malformedURLError:"There is something wrong with the panorama URL.",iOS8WebGLError:"Due to iOS 8's broken WebGL implementation, only progressive encoded JPEGs work for your device (this panorama uses standard encoding).",genericWebGLError:"Your browser does not have the necessary WebGL support to display this panorama.",textureSizeError:"This panorama is too big for your device! It's %spx wide, but your device only supports images up to %spx wide. Try another device. (If you're the author, try scaling down the image.)",
-unknownError:"Unknown error. Check developer console."}},Za=[16,17,27,37,38,39,40,61,65,68,83,87,107,109,173,187,189];r="string"===typeof r?f.getElementById(r):r;r.classList.add("pnlm-container");r.tabIndex=0;var C=f.createElement("div");C.className="pnlm-ui";r.appendChild(C);var M=f.createElement("div");M.className="pnlm-render-container";r.appendChild(M);var H=f.createElement("div");H.className="pnlm-dragfix";C.appendChild(H);var ha=f.createElement("span");ha.className="pnlm-about-msg";ha.innerHTML=
-'Pannellum 2.4.1';C.appendChild(ha);H.addEventListener("contextmenu",X);var n={},Xa=f.createElement("div");Xa.className="pnlm-sprite pnlm-hot-spot-debug-indicator";C.appendChild(Xa);n.container=f.createElement("div");n.container.className="pnlm-panorama-info";n.title=f.createElement("div");n.title.className="pnlm-title-box";n.container.appendChild(n.title);n.author=f.createElement("div");n.author.className="pnlm-author-box";n.container.appendChild(n.author);
-C.appendChild(n.container);n.load={};n.load.box=f.createElement("div");n.load.box.className="pnlm-load-box";n.load.boxp=f.createElement("p");n.load.box.appendChild(n.load.boxp);n.load.lbox=f.createElement("div");n.load.lbox.className="pnlm-lbox";n.load.lbox.innerHTML='';n.load.box.appendChild(n.load.lbox);n.load.lbar=f.createElement("div");n.load.lbar.className="pnlm-lbar";n.load.lbarFill=f.createElement("div");n.load.lbarFill.className="pnlm-lbar-fill";n.load.lbar.appendChild(n.load.lbarFill);
-n.load.box.appendChild(n.load.lbar);n.load.msg=f.createElement("p");n.load.msg.className="pnlm-lmsg";n.load.box.appendChild(n.load.msg);C.appendChild(n.load.box);n.errorMsg=f.createElement("div");n.errorMsg.className="pnlm-error-msg pnlm-info-box";C.appendChild(n.errorMsg);var w={};w.container=f.createElement("div");w.container.className="pnlm-controls-container";C.appendChild(w.container);w.load=f.createElement("div");w.load.className="pnlm-load-button";w.load.addEventListener("click",function(){t();
-Q()});C.appendChild(w.load);w.zoom=f.createElement("div");w.zoom.className="pnlm-zoom-controls pnlm-controls";w.zoomIn=f.createElement("div");w.zoomIn.className="pnlm-zoom-in pnlm-sprite pnlm-control";w.zoomIn.addEventListener("click",function(){K&&(U(b.hfov-5),F())});w.zoom.appendChild(w.zoomIn);w.zoomOut=f.createElement("div");w.zoomOut.className="pnlm-zoom-out pnlm-sprite pnlm-control";w.zoomOut.addEventListener("click",function(){K&&(U(b.hfov+5),F())});w.zoom.appendChild(w.zoomOut);w.container.appendChild(w.zoom);
-w.fullscreen=f.createElement("div");w.fullscreen.addEventListener("click",z);w.fullscreen.className="pnlm-fullscreen-toggle-button pnlm-sprite pnlm-fullscreen-toggle-button-inactive pnlm-controls pnlm-control";(f.fullscreenEnabled||f.mozFullScreenEnabled||f.webkitFullscreenEnabled||f.msFullscreenEnabled)&&w.container.appendChild(w.fullscreen);w.orientation=f.createElement("div");w.orientation.addEventListener("click",function(a){aa?l():Ka()});w.orientation.addEventListener("mousedown",function(a){a.stopPropagation()});
-w.orientation.addEventListener("touchstart",function(a){a.stopPropagation()});w.orientation.addEventListener("pointerdown",function(a){a.stopPropagation()});w.orientation.className="pnlm-orientation-button pnlm-orientation-button-inactive pnlm-sprite pnlm-controls pnlm-control";var pa,Ya=!1;J.DeviceOrientationEvent?J.addEventListener("deviceorientation",Oa):pa=!1;var Ha=f.createElement("div");Ha.className="pnlm-compass pnlm-controls pnlm-control";C.appendChild(Ha);v.firstScene?g(v.firstScene):v.default&&
-v.default.firstScene?g(v.default.firstScene):g(null);t(!0);var ma=[],Aa=[];Z.prototype.multiply=function(a){return new Z(this.w*a.w-this.x*a.x-this.y*a.y-this.z*a.z,this.x*a.w+this.w*a.x+this.y*a.z-this.z*a.y,this.y*a.w+this.w*a.y+this.z*a.x-this.x*a.z,this.z*a.w+this.w*a.z+this.x*a.y-this.y*a.x)};Z.prototype.toEulerAngles=function(){var a=Math.atan2(2*(this.w*this.x+this.y*this.z),1-2*(this.x*this.x+this.y*this.y)),b=Math.asin(2*(this.w*this.y-this.z*this.x)),c=Math.atan2(2*(this.w*this.z+this.x*
-this.y),1-2*(this.y*this.y+this.z*this.z));return[a,b,c]};this.isLoaded=function(){return Boolean(K)};this.getPitch=function(){return b.pitch};this.setPitch=function(a,c,d,e){(c=c==m?1E3:Number(c))?L.pitch={startTime:Date.now(),startPosition:b.pitch,endPosition:a,duration:c,callback:d,callbackArgs:e}:b.pitch=a;F();return this};this.getPitchBounds=function(){return[b.minPitch,b.maxPitch]};this.setPitchBounds=function(a){b.minPitch=Math.max(-90,Math.min(a[0],90));b.maxPitch=Math.max(-90,Math.min(a[1],
-90));return this};this.getYaw=function(){return b.yaw};this.setYaw=function(a,c,d,e){c=c==m?1E3:Number(c);a=(a+180)%360-180;c?(180= _margin && i <= rows.length - _margin &&
- j >= _margin && j <= cols.length - _margin) {
- tiles.unshift([x, y, z0]); // tiles in view at beginning
- } else {
- tiles.push([x, y, z0]); // tiles in margin at the end
- }
- }
- }
-
- tiles.translate = origin;
- tiles.scale = k;
-
- return tiles;
- }
-
- tile.scaleExtent = function(val) {
- if (!arguments.length) return _scaleExtent;
- _scaleExtent = val;
- return tile;
- };
-
- tile.size = function(val) {
- if (!arguments.length) return _size;
- _size = val;
- return tile;
- };
-
- tile.scale = function(val) {
- if (!arguments.length) return _scale;
- _scale = val;
- return tile;
- };
-
- tile.translate = function(val) {
- if (!arguments.length) return _translate;
- _translate = val;
- return tile;
- };
-
- tile.zoomDelta = function(val) {
- if (!arguments.length) return _zoomDelta;
- _zoomDelta = +val;
- return tile;
- };
-
- // number to extend the rows/columns beyond those covering the viewport
- tile.margin = function(val) {
- if (!arguments.length) return _margin;
- _margin = +val;
- return tile;
- };
-
- return tile;
-}
diff --git a/modules/lib/index.js b/modules/lib/index.js
index b6ddad54a..bee98416c 100644
--- a/modules/lib/index.js
+++ b/modules/lib/index.js
@@ -1,3 +1,2 @@
export { d3combobox } from './d3.combobox';
-export { d3geoTile } from './d3.geo.tile';
export { d3keybinding } from './d3.keybinding';
diff --git a/modules/modes/add_area.js b/modules/modes/add_area.js
index 4e0af4318..7b4d6f5d9 100644
--- a/modules/modes/add_area.js
+++ b/modules/modes/add_area.js
@@ -20,11 +20,12 @@ export function modeAddArea(context) {
};
var behavior = behaviorAddWay(context)
- .tail(t('modes.add_area.tail'))
- .on('start', start)
- .on('startFromWay', startFromWay)
- .on('startFromNode', startFromNode),
- defaultTags = { area: 'yes' };
+ .tail(t('modes.add_area.tail'))
+ .on('start', start)
+ .on('startFromWay', startFromWay)
+ .on('startFromNode', startFromNode);
+
+ var defaultTags = { area: 'yes' };
function actionClose(wayId) {
@@ -35,9 +36,9 @@ export function modeAddArea(context) {
function start(loc) {
- var startGraph = context.graph(),
- node = osmNode({ loc: loc }),
- way = osmWay({ tags: defaultTags });
+ var startGraph = context.graph();
+ var node = osmNode({ loc: loc });
+ var way = osmWay({ tags: defaultTags });
context.perform(
actionAddEntity(node),
@@ -51,9 +52,9 @@ export function modeAddArea(context) {
function startFromWay(loc, edge) {
- var startGraph = context.graph(),
- node = osmNode({ loc: loc }),
- way = osmWay({ tags: defaultTags });
+ var startGraph = context.graph();
+ var node = osmNode({ loc: loc });
+ var way = osmWay({ tags: defaultTags });
context.perform(
actionAddEntity(node),
@@ -68,8 +69,8 @@ export function modeAddArea(context) {
function startFromNode(node) {
- var startGraph = context.graph(),
- way = osmWay({ tags: defaultTags });
+ var startGraph = context.graph();
+ var way = osmWay({ tags: defaultTags });
context.perform(
actionAddEntity(way),
diff --git a/modules/modes/add_line.js b/modules/modes/add_line.js
index d18deff14..bd4e6c482 100644
--- a/modules/modes/add_line.js
+++ b/modules/modes/add_line.js
@@ -27,9 +27,9 @@ export function modeAddLine(context) {
function start(loc) {
- var startGraph = context.graph(),
- node = osmNode({ loc: loc }),
- way = osmWay();
+ var startGraph = context.graph();
+ var node = osmNode({ loc: loc });
+ var way = osmWay();
context.perform(
actionAddEntity(node),
@@ -42,9 +42,9 @@ export function modeAddLine(context) {
function startFromWay(loc, edge) {
- var startGraph = context.graph(),
- node = osmNode({ loc: loc }),
- way = osmWay();
+ var startGraph = context.graph();
+ var node = osmNode({ loc: loc });
+ var way = osmWay();
context.perform(
actionAddEntity(node),
@@ -58,8 +58,8 @@ export function modeAddLine(context) {
function startFromNode(node) {
- var startGraph = context.graph(),
- way = osmWay();
+ var startGraph = context.graph();
+ var way = osmWay();
context.perform(
actionAddEntity(way),
diff --git a/modules/modes/add_note.js b/modules/modes/add_note.js
new file mode 100644
index 000000000..8f7f065a0
--- /dev/null
+++ b/modules/modes/add_note.js
@@ -0,0 +1,56 @@
+import { t } from '../util/locale';
+import { behaviorDraw } from '../behavior';
+import { modeBrowse, modeSelectNote } from './index';
+import { osmNote } from '../osm';
+import { services } from '../services';
+
+
+export function modeAddNote(context) {
+ var mode = {
+ id: 'add-note',
+ button: 'note',
+ title: t('modes.add_note.title'),
+ description: t('modes.add_note.description'),
+ key: '4'
+ };
+
+ var behavior = behaviorDraw(context)
+ .tail(t('modes.add_note.tail'))
+ .on('click', add)
+ .on('cancel', cancel)
+ .on('finish', cancel);
+
+
+ function add(loc) {
+ var osm = services.osm;
+ if (!osm) return;
+
+ var note = osmNote({ loc: loc, status: 'open', comments: [] });
+ osm.replaceNote(note);
+
+ // force a reraw (there is no history change that would otherwise do this)
+ context.pan([0,0]);
+
+ context
+ .selectedNoteID(note.id)
+ .enter(modeSelectNote(context, note.id).newFeature(true));
+ }
+
+
+ function cancel() {
+ context.enter(modeBrowse(context));
+ }
+
+
+ mode.enter = function() {
+ context.install(behavior);
+ };
+
+
+ mode.exit = function() {
+ context.uninstall(behavior);
+ };
+
+
+ return mode;
+}
diff --git a/modules/modes/browse.js b/modules/modes/browse.js
index 5ba0e7fee..acd9cc6f3 100644
--- a/modules/modes/browse.js
+++ b/modules/modes/browse.js
@@ -8,6 +8,7 @@ import {
} from '../behavior';
import { modeDragNode } from './drag_node';
+import { modeDragNote } from './drag_note';
export function modeBrowse(context) {
@@ -23,14 +24,13 @@ export function modeBrowse(context) {
behaviorHover(context).on('hover', context.ui().sidebar.hover),
behaviorSelect(context),
behaviorLasso(context),
- modeDragNode(context).behavior
+ modeDragNode(context).behavior,
+ modeDragNote(context).behavior
];
mode.enter = function() {
- behaviors.forEach(function(behavior) {
- context.install(behavior);
- });
+ behaviors.forEach(context.install);
// Get focus on the body.
if (document.activeElement && document.activeElement.blur) {
@@ -47,9 +47,7 @@ export function modeBrowse(context) {
mode.exit = function() {
context.ui().sidebar.hover.cancel();
- behaviors.forEach(function(behavior) {
- context.uninstall(behavior);
- });
+ behaviors.forEach(context.uninstall);
if (sidebar) {
context.ui().sidebar.hide();
diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js
index f726b9dbb..0327ece06 100644
--- a/modules/modes/drag_node.js
+++ b/modules/modes/drag_node.js
@@ -446,9 +446,6 @@ export function modeDragNode(context) {
context.history()
.on('undone.drag-node', null);
- context.map()
- .on('drawn.drag-node', null);
-
_activeEntity = null;
context.surface()
diff --git a/modules/modes/drag_note.js b/modules/modes/drag_note.js
new file mode 100644
index 000000000..7f834cb10
--- /dev/null
+++ b/modules/modes/drag_note.js
@@ -0,0 +1,126 @@
+import {
+ event as d3_event,
+ select as d3_select
+} from 'd3-selection';
+
+import { services } from '../services';
+import { actionNoop } from '../actions';
+import { behaviorEdit, behaviorDrag } from '../behavior';
+import { geoVecSubtract, geoViewportEdge } from '../geo';
+import { modeSelectNote } from './index';
+
+
+export function modeDragNote(context) {
+ var mode = {
+ id: 'drag-note',
+ button: 'browse'
+ };
+
+ var edit = behaviorEdit(context);
+
+ var _nudgeInterval;
+ var _lastLoc;
+
+
+ function startNudge(note, nudge) {
+ if (_nudgeInterval) window.clearInterval(_nudgeInterval);
+ _nudgeInterval = window.setInterval(function() {
+ context.pan(nudge);
+ doMove(note, nudge);
+ }, 50);
+ }
+
+
+ function stopNudge() {
+ if (_nudgeInterval) {
+ window.clearInterval(_nudgeInterval);
+ _nudgeInterval = null;
+ }
+ }
+
+
+ function origin(note) {
+ return context.projection(note.loc);
+ }
+
+
+ function start(note) {
+ context.surface().selectAll('.note-' + note.id)
+ .classed('active', true);
+
+ context.perform(actionNoop());
+ context.enter(mode);
+ context.selectedNoteID(note.id);
+ }
+
+
+ function move(note) {
+ d3_event.sourceEvent.stopPropagation();
+ _lastLoc = context.projection.invert(d3_event.point);
+
+ doMove(note);
+ var nudge = geoViewportEdge(d3_event.point, context.map().dimensions());
+ if (nudge) {
+ startNudge(note, nudge);
+ } else {
+ stopNudge();
+ }
+ }
+
+
+ function doMove(note, nudge) {
+ nudge = nudge || [0, 0];
+
+ var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc);
+ var currMouse = geoVecSubtract(currPoint, nudge);
+ var loc = context.projection.invert(currMouse);
+
+ note = note.move(loc);
+
+ var osm = services.osm;
+ if (osm) {
+ osm.replaceNote(note); // update note cache
+ }
+
+ context.replace(actionNoop()); // trigger redraw
+ }
+
+
+ function end(note) {
+ context.replace(actionNoop()); // trigger redraw
+
+ context
+ .selectedNoteID(note.id)
+ .enter(modeSelectNote(context, note.id));
+ }
+
+
+ var drag = behaviorDrag()
+ .selector('.layer-notes .new')
+ .surface(d3_select('#map').node())
+ .origin(origin)
+ .on('start', start)
+ .on('move', move)
+ .on('end', end);
+
+
+ mode.enter = function() {
+ context.install(edit);
+ };
+
+
+ mode.exit = function() {
+ context.ui().sidebar.hover.cancel();
+ context.uninstall(edit);
+
+ context.surface()
+ .selectAll('.active')
+ .classed('active', false);
+
+ stopNudge();
+ };
+
+ mode.behavior = drag;
+
+ return mode;
+}
diff --git a/modules/modes/draw_line.js b/modules/modes/draw_line.js
index 83c2a9ae0..a9f5b0e49 100644
--- a/modules/modes/draw_line.js
+++ b/modules/modes/draw_line.js
@@ -2,7 +2,7 @@ import { t } from '../util/locale';
import { behaviorDrawWay } from '../behavior';
-export function modeDrawLine(context, wayId, startGraph, affix) {
+export function modeDrawLine(context, wayID, startGraph, affix) {
var mode = {
button: 'line',
id: 'draw-line'
@@ -12,16 +12,16 @@ export function modeDrawLine(context, wayId, startGraph, affix) {
mode.enter = function() {
- var way = context.entity(wayId);
+ var way = context.entity(wayID);
var index = (affix === 'prefix') ? 0 : undefined;
- var headId = (affix === 'prefix') ? way.first() : way.last();
+ var headID = (affix === 'prefix') ? way.first() : way.last();
- behavior = behaviorDrawWay(context, wayId, index, mode, startGraph)
+ behavior = behaviorDrawWay(context, wayID, index, mode, startGraph)
.tail(t('modes.draw_line.tail'));
var addNode = behavior.addNode;
behavior.addNode = function(node, d) {
- if (node.id === headId) {
+ if (node.id === headID) {
behavior.finish();
} else {
addNode(node, d);
@@ -38,7 +38,7 @@ export function modeDrawLine(context, wayId, startGraph, affix) {
mode.selectedIDs = function() {
- return [wayId];
+ return [wayID];
};
diff --git a/modules/modes/index.js b/modules/modes/index.js
index ffcdf9868..af440c4c2 100644
--- a/modules/modes/index.js
+++ b/modules/modes/index.js
@@ -1,12 +1,15 @@
export { modeAddArea } from './add_area';
export { modeAddLine } from './add_line';
export { modeAddPoint } from './add_point';
+export { modeAddNote } from './add_note';
export { modeBrowse } from './browse';
export { modeDragNode } from './drag_node';
+export { modeDragNote } from './drag_note';
export { modeDrawArea } from './draw_area';
export { modeDrawLine } from './draw_line';
export { modeMove } from './move';
export { modeRotate } from './rotate';
export { modeSave } from './save';
export { modeSelect } from './select';
+export { modeSelectData } from './select_data';
export { modeSelectNote } from './select_note';
diff --git a/modules/modes/rotate.js b/modules/modes/rotate.js
index 4addaed88..12cac0fdd 100644
--- a/modules/modes/rotate.js
+++ b/modules/modes/rotate.js
@@ -120,9 +120,7 @@ export function modeRotate(context, entityIDs) {
mode.enter = function() {
- behaviors.forEach(function(behavior) {
- context.install(behavior);
- });
+ behaviors.forEach(context.install);
context.surface()
.on('mousemove.rotate', doRotate)
@@ -141,9 +139,7 @@ export function modeRotate(context, entityIDs) {
mode.exit = function() {
- behaviors.forEach(function(behavior) {
- context.uninstall(behavior);
- });
+ behaviors.forEach(context.uninstall);
context.surface()
.on('mousemove.rotate', null)
diff --git a/modules/modes/select.js b/modules/modes/select.js
index 924314de9..1d19eccc3 100644
--- a/modules/modes/select.js
+++ b/modules/modes/select.js
@@ -36,6 +36,7 @@ import {
import { modeBrowse } from './browse';
import { modeDragNode } from './drag_node';
+import { modeDragNote } from './drag_note';
import * as Operations from '../operations/index';
import { uiEditMenu, uiSelectionList } from '../ui';
import { uiCmd } from '../ui/cmd';
@@ -63,7 +64,8 @@ export function modeSelect(context, selectedIDs) {
behaviorHover(context),
behaviorSelect(context),
behaviorLasso(context),
- modeDragNode(context).restoreSelectedIDs(selectedIDs).behavior
+ modeDragNode(context).restoreSelectedIDs(selectedIDs).behavior,
+ modeDragNote(context).behavior
];
var inspector;
var editMenu;
@@ -449,9 +451,7 @@ export function modeSelect(context, selectedIDs) {
}
});
- behaviors.forEach(function(behavior) {
- context.install(behavior);
- });
+ behaviors.forEach(context.install);
keybinding
.on(['[', 'pgup'], previousVertex)
@@ -520,10 +520,7 @@ export function modeSelect(context, selectedIDs) {
if (timeout) window.clearTimeout(timeout);
if (inspector) wrap.call(inspector.close);
- behaviors.forEach(function(behavior) {
- context.uninstall(behavior);
- });
-
+ behaviors.forEach(context.uninstall);
keybinding.off();
closeMenu();
editMenu = undefined;
diff --git a/modules/modes/select_data.js b/modules/modes/select_data.js
new file mode 100644
index 000000000..1e433660d
--- /dev/null
+++ b/modules/modes/select_data.js
@@ -0,0 +1,97 @@
+import {
+ event as d3_event,
+ select as d3_select
+} from 'd3-selection';
+
+import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js';
+
+import {
+ behaviorBreathe,
+ behaviorHover,
+ behaviorLasso,
+ behaviorSelect
+} from '../behavior';
+
+import {
+ modeDragNode,
+ modeDragNote
+} from '../modes';
+
+import { modeBrowse } from './browse';
+import { uiDataEditor } from '../ui';
+
+
+export function modeSelectData(context, selectedDatum) {
+ var mode = {
+ id: 'select-data',
+ button: 'browse'
+ };
+
+ var keybinding = d3_keybinding('select-data');
+ var dataEditor = uiDataEditor(context);
+
+ var behaviors = [
+ behaviorBreathe(context),
+ behaviorHover(context),
+ behaviorSelect(context),
+ behaviorLasso(context),
+ modeDragNode(context).behavior,
+ modeDragNote(context).behavior
+ ];
+
+
+ // class the data as selected, or return to browse mode if the data is gone
+ function selectData(drawn) {
+ var selection = context.surface().selectAll('.layer-mapdata .data' + selectedDatum.__featurehash__);
+
+ if (selection.empty()) {
+ // Return to browse mode if selected DOM elements have
+ // disappeared because the user moved them out of view..
+ var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent;
+ if (drawn && source && (source.type === 'mousemove' || source.type === 'touchmove')) {
+ context.enter(modeBrowse(context));
+ }
+ } else {
+ selection.classed('selected', true);
+ }
+ }
+
+
+ function esc() {
+ context.enter(modeBrowse(context));
+ }
+
+
+ mode.enter = function() {
+ behaviors.forEach(context.install);
+ keybinding.on('⎋', esc, true);
+ d3_select(document).call(keybinding);
+
+ selectData();
+
+ context.ui().sidebar
+ .show(dataEditor.datum(selectedDatum));
+
+ context.map()
+ .on('drawn.select-data', selectData);
+ };
+
+
+ mode.exit = function() {
+ behaviors.forEach(context.uninstall);
+ keybinding.off();
+
+ context.surface()
+ .selectAll('.layer-mapdata .selected')
+ .classed('selected hover', false);
+
+ context.map()
+ .on('drawn.select-data', null);
+
+ context.ui().sidebar
+ .hide();
+ };
+
+
+ return mode;
+}
diff --git a/modules/modes/select_note.js b/modules/modes/select_note.js
index 8da727c8e..36246a207 100644
--- a/modules/modes/select_note.js
+++ b/modules/modes/select_note.js
@@ -6,11 +6,17 @@ import {
import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js';
import {
+ behaviorBreathe,
behaviorHover,
behaviorLasso,
behaviorSelect
} from '../behavior';
+import {
+ modeDragNode,
+ modeDragNote
+} from '../modes';
+
import { services } from '../services';
import { modeBrowse } from './browse';
import { uiNoteEditor } from '../ui';
@@ -18,7 +24,7 @@ import { uiNoteEditor } from '../ui';
export function modeSelectNote(context, selectedNoteID) {
var mode = {
- id: 'select_note',
+ id: 'select-note',
button: 'browse'
};
@@ -34,11 +40,16 @@ export function modeSelectNote(context, selectedNoteID) {
});
var behaviors = [
+ behaviorBreathe(context),
behaviorHover(context),
behaviorSelect(context),
behaviorLasso(context),
+ modeDragNode(context).behavior,
+ modeDragNote(context).behavior
];
+ var newFeature = false;
+
function checkSelectedID() {
if (!osm) return;
@@ -50,72 +61,73 @@ export function modeSelectNote(context, selectedNoteID) {
}
- mode.enter = function() {
+ // class the note as selected, or return to browse mode if the note is gone
+ function selectNote(drawn) {
+ if (!checkSelectedID()) return;
- // class the note as selected, or return to browse mode if the note is gone
- function selectNote(drawn) {
- if (!checkSelectedID()) return;
+ var selection = context.surface().selectAll('.layer-notes .note-' + selectedNoteID);
- var selection = context.surface()
- .selectAll('.note-' + selectedNoteID);
-
- if (selection.empty()) {
- // Return to browse mode if selected DOM elements have
- // disappeared because the user moved them out of view..
- var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent;
- if (drawn && source && (source.type === 'mousemove' || source.type === 'touchmove')) {
- context.enter(modeBrowse(context));
- }
-
- } else {
- selection
- .classed('selected', true);
+ if (selection.empty()) {
+ // Return to browse mode if selected DOM elements have
+ // disappeared because the user moved them out of view..
+ var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent;
+ if (drawn && source && (source.type === 'mousemove' || source.type === 'touchmove')) {
+ context.enter(modeBrowse(context));
}
- }
- function esc() {
- context.enter(modeBrowse(context));
+ } else {
+ selection
+ .classed('selected', true);
+ context.selectedNoteID(selectedNoteID);
}
+ }
+
+ function esc() {
+ context.enter(modeBrowse(context));
+ }
+
+
+ mode.newFeature = function(_) {
+ if (!arguments.length) return newFeature;
+ newFeature = _;
+ return mode;
+ };
+
+
+ mode.enter = function() {
var note = checkSelectedID();
if (!note) return;
- behaviors.forEach(function(behavior) {
- context.install(behavior);
- });
+ behaviors.forEach(context.install);
+ keybinding.on('⎋', esc, true);
+ d3_select(document).call(keybinding);
- keybinding
- .on('⎋', esc, true);
-
- d3_select(document)
- .call(keybinding);
+ selectNote();
context.ui().sidebar
.show(noteEditor.note(note));
context.map()
.on('drawn.select', selectNote);
-
- selectNote();
};
mode.exit = function() {
- behaviors.forEach(function(behavior) {
- context.uninstall(behavior);
- });
-
+ behaviors.forEach(context.uninstall);
keybinding.off();
context.surface()
- .selectAll('.note.selected')
- .classed('selected hovered', false);
+ .selectAll('.layer-notes .selected')
+ .classed('selected hover', false);
context.map()
.on('drawn.select', null);
context.ui().sidebar
.hide();
+
+ context.selectedNoteID(null);
};
diff --git a/modules/operations/detach_node.js b/modules/operations/detach_node.js
new file mode 100644
index 000000000..268b8554e
--- /dev/null
+++ b/modules/operations/detach_node.js
@@ -0,0 +1,85 @@
+import _some from 'lodash-es/some';
+
+import { actionDetachNode, actionMoveNode } from '../actions';
+import { behaviorOperation } from '../behavior';
+import { modeMove } from '../modes';
+import { t } from '../util/locale';
+
+
+export function operationDetachNode(selectedIDs, context) {
+ var nodeID = selectedIDs.length && selectedIDs[0];
+ var action = actionDetachNode(nodeID);
+
+ var operation = function () {
+ context.perform(action); // do the detach
+
+ var mouse = context.map().mouseCoordinates();
+ if (mouse.some(isNaN)) {
+ enterMoveMode();
+
+ } else {
+ // move detached node to the mouse location (transitioned)
+ context.perform(actionMoveNode(nodeID, mouse));
+
+ // after transition completes, put at final mouse location and enter move mode.
+ window.setTimeout(function() {
+ mouse = context.map().mouseCoordinates();
+ context.replace(actionMoveNode(nodeID, mouse));
+ enterMoveMode();
+ }, 150);
+ }
+
+ function enterMoveMode() {
+ var baseGraph = context.graph();
+ context.enter(modeMove(context, [nodeID], baseGraph));
+ }
+ };
+
+
+ operation.available = function () {
+ if (selectedIDs.length !== 1) return false;
+
+ var graph = context.graph();
+ var entity = graph.hasEntity(nodeID);
+ if (!entity) return false;
+
+ return entity.type === 'node' &&
+ entity.hasInterestingTags() &&
+ graph.parentWays(entity).length > 0;
+ };
+
+
+ operation.disabled = function () {
+ var reason;
+ if (_some(selectedIDs, context.hasHiddenConnections)) {
+ reason = 'connected_to_hidden';
+ }
+ return action.disabled(context.graph()) || reason;
+ };
+
+
+ operation.tooltip = function () {
+ var disableReason = operation.disabled();
+ if (disableReason) {
+ return t('operations.detach_node.' + disableReason,
+ { relation: context.presets().item('type/restriction').name() });
+ } else {
+ return t('operations.detach_node.description');
+ }
+ };
+
+
+ operation.annotation = function () {
+ return t('operations.detach_node.annotation');
+ };
+
+
+ operation.id = 'detach-node';
+ operation.keys = [t('operations.detach_node.key')];
+ operation.title = t('operations.detach_node.title');
+ operation.behavior = behaviorOperation(context).which(operation);
+
+
+ return operation;
+}
+
diff --git a/modules/operations/index.js b/modules/operations/index.js
index 1ad24cd97..8339d8205 100644
--- a/modules/operations/index.js
+++ b/modules/operations/index.js
@@ -10,3 +10,4 @@ export { operationReverse } from './reverse';
export { operationRotate } from './rotate';
export { operationSplit } from './split';
export { operationStraighten } from './straighten';
+export { operationDetachNode } from './detach_node';
diff --git a/modules/osm/note.js b/modules/osm/note.js
index 3e10e8cf1..305849cae 100644
--- a/modules/osm/note.js
+++ b/modules/osm/note.js
@@ -39,7 +39,7 @@ _extend(osmNote.prototype, {
}
if (!this.id) {
- this.id = osmNote.id();
+ this.id = osmNote.id() + ''; // as string
}
return this;
@@ -50,11 +50,15 @@ _extend(osmNote.prototype, {
},
update: function(attrs) {
- return osmNote(this, attrs, {v: 1 + (this.v || 0)});
+ return osmNote(this, attrs); // {v: 1 + (this.v || 0)}
},
isNew: function() {
return this.id < 0;
+ },
+
+ move: function(loc) {
+ return this.update({ loc: loc });
}
});
diff --git a/modules/osm/relation.js b/modules/osm/relation.js
index 8532990ed..1ba283df4 100644
--- a/modules/osm/relation.js
+++ b/modules/osm/relation.js
@@ -42,8 +42,7 @@ _extend(osmRelation.prototype, {
copy: function(resolver, copies) {
- if (copies[this.id])
- return copies[this.id];
+ if (copies[this.id]) return copies[this.id];
var copy = osmEntity.prototype.copy.call(this, resolver, copies);
diff --git a/modules/osm/way.js b/modules/osm/way.js
index af1b3d476..c4da27c25 100644
--- a/modules/osm/way.js
+++ b/modules/osm/way.js
@@ -31,8 +31,7 @@ _extend(osmWay.prototype, {
copy: function(resolver, copies) {
- if (copies[this.id])
- return copies[this.id];
+ if (copies[this.id]) return copies[this.id];
var copy = osmEntity.prototype.copy.call(this, resolver, copies);
@@ -239,9 +238,9 @@ _extend(osmWay.prototype, {
unclose: function() {
if (!this.isClosed()) return this;
- var nodes = this.nodes.slice(),
- connector = this.first(),
- i = nodes.length - 1;
+ var nodes = this.nodes.slice();
+ var connector = this.first();
+ var i = nodes.length - 1;
// remove trailing connectors..
while (i > 0 && nodes.length > 1 && nodes[i] === connector) {
@@ -260,9 +259,9 @@ _extend(osmWay.prototype, {
// Consecutive duplicates are eliminated including existing ones.
// Circularity is always preserved when adding a node.
addNode: function(id, index) {
- var nodes = this.nodes.slice(),
- isClosed = this.isClosed(),
- max = isClosed ? nodes.length - 1 : nodes.length;
+ var nodes = this.nodes.slice();
+ var isClosed = this.isClosed();
+ var max = isClosed ? nodes.length - 1 : nodes.length;
if (index === undefined) {
index = max;
@@ -309,9 +308,9 @@ _extend(osmWay.prototype, {
// Consecutive duplicates are eliminated including existing ones.
// Circularity is preserved when updating a node.
updateNode: function(id, index) {
- var nodes = this.nodes.slice(),
- isClosed = this.isClosed(),
- max = nodes.length - 1;
+ var nodes = this.nodes.slice();
+ var isClosed = this.isClosed();
+ var max = nodes.length - 1;
if (index === undefined || index < 0 || index > max) {
throw new RangeError('index ' + index + ' out of range 0..' + max);
@@ -353,13 +352,13 @@ _extend(osmWay.prototype, {
// Replaces each occurrence of node id needle with replacement.
// Consecutive duplicates are eliminated including existing ones.
// Circularity is preserved.
- replaceNode: function(needle, replacement) {
- var nodes = this.nodes.slice(),
- isClosed = this.isClosed();
+ replaceNode: function(needleID, replacementID) {
+ var nodes = this.nodes.slice();
+ var isClosed = this.isClosed();
for (var i = 0; i < nodes.length; i++) {
- if (nodes[i] === needle) {
- nodes[i] = replacement;
+ if (nodes[i] === needleID) {
+ nodes[i] = replacementID;
}
}
@@ -374,12 +373,12 @@ _extend(osmWay.prototype, {
},
- // Removes each occurrence of node id needle with replacement.
+ // Removes each occurrence of node id.
// Consecutive duplicates are eliminated including existing ones.
// Circularity is preserved.
removeNode: function(id) {
- var nodes = this.nodes.slice(),
- isClosed = this.isClosed();
+ var nodes = this.nodes.slice();
+ var isClosed = this.isClosed();
nodes = nodes
.filter(function(node) { return node !== id; })
diff --git a/modules/presets/field.js b/modules/presets/field.js
index b658c43a9..33b51ab8d 100644
--- a/modules/presets/field.js
+++ b/modules/presets/field.js
@@ -21,7 +21,7 @@ export function presetField(id, field) {
field.label = function() {
- return field.t('label', {'default': id});
+ return field.overrideLabel || field.t('label', {'default': id});
};
diff --git a/modules/renderer/background.js b/modules/renderer/background.js
index 644986cad..b52db0415 100644
--- a/modules/renderer/background.js
+++ b/modules/renderer/background.js
@@ -4,6 +4,8 @@ import { dispatch as d3_dispatch } from 'd3-dispatch';
import { interpolateNumber as d3_interpolateNumber } from 'd3-interpolate';
import { select as d3_select } from 'd3-selection';
+import whichPolygon from 'which-polygon';
+
import { data } from '../../data';
import { geoExtent, geoMetersToOffset, geoOffsetToMeters} from '../geo';
import { rendererBackgroundSource } from './background_source';
@@ -168,12 +170,9 @@ export function rendererBackground(context) {
.filter(function (d) { return !d.source().isLocatorOverlay() && !d.source().isHidden(); })
.forEach(function (d) { imageryUsed.push(d.source().imageryUsed()); });
- var gpx = context.layers().layer('gpx');
- if (gpx && gpx.enabled() && gpx.hasGpx()) {
- // Include a string like '.gpx data file' or '.geojson data file'
- var match = gpx.getSrc().match(/(kml|gpx|(?:geo)?json)$/i);
- var extension = match ? ('.' + match[0].toLowerCase() + ' ') : '';
- imageryUsed.push(extension + 'data file');
+ var data = context.layers().layer('data');
+ if (data && data.enabled() && data.hasData()) {
+ imageryUsed.push(data.getSrc());
}
var streetside = context.layers().layer('streetside');
@@ -181,14 +180,6 @@ export function rendererBackground(context) {
imageryUsed.push('Bing Streetside');
}
- var mvt = context.layers().layer('mvt');
- if (mvt && mvt.enabled() && mvt.hasMvt()) {
- // Include a string like '.mvt data file' or '.geojson data file'
- var matchmvt = mvt.getSrc().match(/(pbf|mvt|(?:geo)?json)$/i);
- var extensionmvt = matchmvt ? ('.' + matchmvt[0].toLowerCase() + ' ') : '';
- imageryUsed.push(extensionmvt + 'data file');
- }
-
var mapillary_images = context.layers().layer('mapillary-images');
if (mapillary_images && mapillary_images.enabled()) {
imageryUsed.push('Mapillary Images');
@@ -209,18 +200,24 @@ export function rendererBackground(context) {
background.sources = function(extent) {
+ if (!data.imagery || !data.imagery.query) return []; // called before init()?
+
+ var matchIDs = {};
+ var matchImagery = data.imagery.query.bbox(extent.rectangle(), true) || [];
+ matchImagery.forEach(function(d) { matchIDs[d.id] = true; });
+
return _backgroundSources.filter(function(source) {
- return source.intersects(extent);
+ return matchIDs[source.id] || !source.polygon; // no polygon = worldwide
});
};
- background.dimensions = function(_) {
- if (!_) return;
- baseLayer.dimensions(_);
+ background.dimensions = function(d) {
+ if (!d) return;
+ baseLayer.dimensions(d);
_overlayLayers.forEach(function(layer) {
- layer.dimensions(_);
+ layer.dimensions(d);
});
};
@@ -366,15 +363,37 @@ export function rendererBackground(context) {
return geoExtent([args[2], args[1]]);
}
- var dataImagery = data.imagery || [];
var q = utilStringQs(window.location.hash.substring(1));
var requested = q.background || q.layer;
var extent = parseMap(q.map);
var first;
var best;
+
+ data.imagery = data.imagery || [];
+ data.imagery.features = {};
+
+ // build efficient index and querying for data.imagery
+ var features = data.imagery.map(function(source) {
+ if (!source.polygon) return null;
+ var feature = {
+ type: 'Feature',
+ properties: { id: source.id },
+ geometry: { type: 'MultiPolygon', coordinates: [ source.polygon ] }
+ };
+
+ data.imagery.features[source.id] = feature;
+ return feature;
+ }).filter(Boolean);
+
+ data.imagery.query = whichPolygon({
+ type: 'FeatureCollection',
+ features: features
+ });
+
+
// Add all the available imagery sources
- _backgroundSources = dataImagery.map(function(source) {
+ _backgroundSources = data.imagery.map(function(source) {
if (source.type === 'bing') {
return rendererBackgroundSource.Bing(source, dispatch);
} else if (/^EsriWorldImagery/.test(source.id)) {
@@ -431,16 +450,9 @@ export function rendererBackground(context) {
});
if (q.gpx) {
- var gpx = context.layers().layer('gpx');
+ var gpx = context.layers().layer('data');
if (gpx) {
- gpx.url(q.gpx);
- }
- }
-
- if (q.mvt) {
- var mvt = context.layers().layer('mvt');
- if (mvt) {
- mvt.url(q.mvt);
+ gpx.url(q.gpx, '.gpx');
}
}
diff --git a/modules/renderer/background_source.js b/modules/renderer/background_source.js
index 76eba00af..f3e2d5014 100644
--- a/modules/renderer/background_source.js
+++ b/modules/renderer/background_source.js
@@ -9,13 +9,7 @@ import {
import { json as d3_json } from 'd3-request';
import { t } from '../util/locale';
-
-import {
- geoExtent,
- geoPolygonIntersectsPolygon,
- geoSphericalDistance
-} from '../geo';
-
+import { geoExtent, geoSphericalDistance } from '../geo';
import { utilDetect } from '../util/detect';
@@ -48,10 +42,10 @@ export function rendererBackgroundSource(data) {
var best = !!source.best;
var template = source.template;
- source.scaleExtent = data.scaleExtent || [0, 22];
+ source.tileSize = data.tileSize || 256;
+ source.zoomExtent = data.zoomExtent || [0, 22];
source.overzoom = data.overzoom !== false;
-
source.offset = function(_) {
if (!arguments.length) return offset;
offset = _;
@@ -116,7 +110,7 @@ export function rendererBackgroundSource(data) {
var lat = Math.atan(sinh(Math.PI * (1 - 2 * y / zoomSize)));
switch (this.projection) {
- case 'EPSG:4326': // todo: alternative codes of WGS 84?
+ case 'EPSG:4326':
return {
x: lon * 180 / Math.PI,
y: lat * 180 / Math.PI
@@ -133,8 +127,8 @@ export function rendererBackgroundSource(data) {
var minXmaxY = tileToProjectedCoords(coord[0], coord[1], coord[2]);
var maxXminY = tileToProjectedCoords(coord[0]+1, coord[1]+1, coord[2]);
return template
- .replace('{width}', 256)
- .replace('{height}', 256)
+ .replace('{width}', this.tileSize)
+ .replace('{height}', this.tileSize)
.replace('{proj}', this.projection)
.replace('{bbox}', minXmaxY.x + ',' + maxXminY.y + ',' + maxXminY.x + ',' + minXmaxY.y);
}
@@ -162,17 +156,9 @@ export function rendererBackgroundSource(data) {
};
- source.intersects = function(extent) {
- extent = extent.polygon();
- return !data.polygon || data.polygon.some(function(polygon) {
- return geoPolygonIntersectsPolygon(polygon, extent, true);
- });
- };
-
-
source.validZoom = function(z) {
- return source.scaleExtent[0] <= z &&
- (source.overzoom || source.scaleExtent[1] > z);
+ return source.zoomExtent[0] <= z &&
+ (source.overzoom || source.zoomExtent[1] > z);
};
@@ -330,8 +316,8 @@ rendererBackgroundSource.Esri = function(data) {
var x = (Math.floor((center[0] + 180) / 360 * Math.pow(2, z)));
var y = (Math.floor((1 - Math.log(Math.tan(center[1] * Math.PI / 180) + 1 / Math.cos(center[1] * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, z)));
- // fetch an 8x8 grid because responses to leverage cache
- var tilemapUrl = dummyUrl.replace(/tile\/[0-9]+\/[0-9]+\/[0-9]+/, 'tilemap') + '/' + z + '/' + y + ' /' + x + '/8/8';
+ // fetch an 8x8 grid to leverage cache
+ var tilemapUrl = dummyUrl.replace(/tile\/[0-9]+\/[0-9]+\/[0-9]+\?blankTile=false/, 'tilemap') + '/' + z + '/' + y + '/' + x + '/8/8';
// make the request and introspect the response from the tilemap server
d3_json(tilemapUrl, function (err, tilemap) {
@@ -347,13 +333,13 @@ rendererBackgroundSource.Esri = function(data) {
}
// if any tiles are missing at level 20 we restrict maxZoom to 19
- esri.scaleExtent[1] = (hasTiles ? 22 : 19);
+ esri.zoomExtent[1] = (hasTiles ? 22 : 19);
});
};
esri.getMetadata = function(center, tileCoord, callback) {
var tileId = tileCoord.slice(0, 3).join('/');
- var zoom = Math.min(tileCoord[2], esri.scaleExtent[1]);
+ var zoom = Math.min(tileCoord[2], esri.zoomExtent[1]);
var centerPoint = center[0] + ',' + center[1]; // long, lat (as it should be)
var unknown = t('info_panels.background.unknown');
var metadataLayer;
diff --git a/modules/renderer/map.js b/modules/renderer/map.js
index 1118488b7..2878d8a48 100644
--- a/modules/renderer/map.js
+++ b/modules/renderer/map.js
@@ -213,7 +213,6 @@ export function rendererMap(context) {
context.on('enter.map', function() {
if (map.editable() && !_transformed) {
-
// redraw immediately any objects affected by a change in selectedIDs.
var graph = context.graph();
var selectedAndParents = {};
@@ -241,7 +240,6 @@ export function rendererMap(context) {
dispatch.call('drawn', this, { full: false });
-
// redraw everything else later
scheduleRedraw();
}
@@ -350,7 +348,7 @@ export function rendererMap(context) {
surface.selectAll('.layer-osm *').remove();
var mode = context.mode();
- if (mode && mode.id !== 'save' && mode.id !== 'select_note') {
+ if (mode && mode.id !== 'save' && mode.id !== 'select-note' && mode.id !== 'select-data') {
context.enter(modeBrowse(context));
}
@@ -483,7 +481,7 @@ export function rendererMap(context) {
// OSM
if (map.editable()) {
- context.loadTiles(projection, dimensions);
+ context.loadTiles(projection);
drawVector(difference, extent);
} else {
editOff();
@@ -849,6 +847,14 @@ export function rendererMap(context) {
};
+ map.notesEditable = function() {
+ var noteLayer = surface.selectAll('.data-layer-notes');
+ if (!noteLayer.empty() && noteLayer.classed('disabled')) return false;
+
+ return map.zoom() >= context.minEditableZoom();
+ };
+
+
map.minzoom = function(_) {
if (!arguments.length) return minzoom;
minzoom = _;
diff --git a/modules/renderer/tile_layer.js b/modules/renderer/tile_layer.js
index 475147e43..3df159d3f 100644
--- a/modules/renderer/tile_layer.js
+++ b/modules/renderer/tile_layer.js
@@ -1,16 +1,15 @@
import { select as d3_select } from 'd3-selection';
import { t } from '../util/locale';
-import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile';
import { geoScaleToZoom, geoVecLength } from '../geo';
-import { utilPrefixCSSProperty } from '../util';
+import { utilPrefixCSSProperty, utilTiler } from '../util';
export function rendererTileLayer(context) {
- var tileSize = 256;
var transformProp = utilPrefixCSSProperty('Transform');
- var geotile = d3_geoTile();
+ var tiler = utilTiler();
+ var _tileSize = 256;
var _projection;
var _cache = {};
var _tileOrigin;
@@ -18,22 +17,9 @@ export function rendererTileLayer(context) {
var _source;
- // blacklist overlay tiles around Null Island..
- function nearNullIsland(x, y, z) {
- if (z >= 7) {
- var center = Math.pow(2, z - 1);
- var width = Math.pow(2, z - 6);
- var min = center - (width / 2);
- var max = center + (width / 2) - 1;
- return x >= min && x <= max && y >= min && y <= max;
- }
- return false;
- }
-
-
function tileSizeAtZoom(d, z) {
- var EPSILON = 0.002;
- return ((tileSize * Math.pow(2, z - d[2])) / tileSize) + EPSILON;
+ var EPSILON = 0.002; // close seams
+ return ((_tileSize * Math.pow(2, z - d[2])) / _tileSize) + EPSILON;
}
@@ -78,7 +64,7 @@ export function rendererTileLayer(context) {
// Update tiles based on current state of `projection`.
function background(selection) {
- _zoom = geoScaleToZoom(_projection.scale(), tileSize);
+ _zoom = geoScaleToZoom(_projection.scale(), _tileSize);
var pixelOffset;
if (_source) {
@@ -95,7 +81,7 @@ export function rendererTileLayer(context) {
_projection.translate()[1] + pixelOffset[1]
];
- geotile
+ tiler
.scale(_projection.scale() * 2 * Math.PI)
.translate(translate);
@@ -117,7 +103,9 @@ export function rendererTileLayer(context) {
var showDebug = context.getDebug('tile') && !_source.overlay;
if (_source.validZoom(_zoom)) {
- geotile().forEach(function(d) {
+ tiler.skipNullIsland(!!_source.overlay);
+
+ tiler().forEach(function(d) {
addSource(d);
if (d[3] === '') return;
if (typeof d[3] !== 'string') return; // Workaround for #2295
@@ -128,15 +116,11 @@ export function rendererTileLayer(context) {
});
requests = uniqueBy(requests, 3).filter(function(r) {
- if (!!_source.overlay && nearNullIsland(r[0], r[1], r[2])) {
- return false;
- }
// don't re-request tiles which have failed in the past
return _cache[r[3]] !== false;
});
}
-
function load(d) {
_cache[d[3]] = true;
d3_select(this)
@@ -156,7 +140,7 @@ export function rendererTileLayer(context) {
}
function imageTransform(d) {
- var ts = tileSize * Math.pow(2, _zoom - d[2]);
+ var ts = _tileSize * Math.pow(2, _zoom - d[2]);
var scale = tileSizeAtZoom(d, _zoom);
return 'translate(' +
((d[0] * ts) - _tileOrigin[0]) + 'px,' +
@@ -165,7 +149,7 @@ export function rendererTileLayer(context) {
}
function tileCenter(d) {
- var ts = tileSize * Math.pow(2, _zoom - d[2]);
+ var ts = _tileSize * Math.pow(2, _zoom - d[2]);
return [
((d[0] * ts) - _tileOrigin[0] + (ts / 2)),
((d[1] * ts) - _tileOrigin[1] + (ts / 2))
@@ -180,7 +164,7 @@ export function rendererTileLayer(context) {
// Pick a representative tile near the center of the viewport
// (This is useful for sampling the imagery vintage)
- var dims = geotile.size();
+ var dims = tiler.size();
var mapCenter = [dims[0] / 2, dims[1] / 2];
var minDist = Math.max(dims[0], dims[1]);
var nearCenter;
@@ -277,8 +261,8 @@ export function rendererTileLayer(context) {
background.dimensions = function(_) {
- if (!arguments.length) return geotile.size();
- geotile.size(_);
+ if (!arguments.length) return tiler.size();
+ tiler.size(_);
return background;
};
@@ -286,8 +270,9 @@ export function rendererTileLayer(context) {
background.source = function(_) {
if (!arguments.length) return _source;
_source = _;
+ _tileSize = _source.tileSize;
_cache = {};
- geotile.scaleExtent(_source.scaleExtent);
+ tiler.tileSize(_source.tileSize).zoomExtent(_source.zoomExtent);
return background;
};
diff --git a/modules/services/index.js b/modules/services/index.js
index 789628ed5..59c9d9524 100644
--- a/modules/services/index.js
+++ b/modules/services/index.js
@@ -1,30 +1,37 @@
import serviceMapillary from './mapillary';
+import serviceMapRules from './maprules';
import serviceNominatim from './nominatim';
import serviceOpenstreetcam from './openstreetcam';
import serviceOsm from './osm';
import serviceStreetside from './streetside';
import serviceTaginfo from './taginfo';
+import serviceVectorTile from './vector_tile';
import serviceWikidata from './wikidata';
import serviceWikipedia from './wikipedia';
+
export var services = {
geocoder: serviceNominatim,
mapillary: serviceMapillary,
openstreetcam: serviceOpenstreetcam,
osm: serviceOsm,
+ maprules: serviceMapRules,
streetside: serviceStreetside,
taginfo: serviceTaginfo,
+ vectorTile: serviceVectorTile,
wikidata: serviceWikidata,
wikipedia: serviceWikipedia
};
export {
serviceMapillary,
+ serviceMapRules,
serviceNominatim,
serviceOpenstreetcam,
serviceOsm,
serviceStreetside,
serviceTaginfo,
+ serviceVectorTile,
serviceWikidata,
serviceWikipedia
};
diff --git a/modules/services/mapillary.js b/modules/services/mapillary.js
index 80340d790..8be72b3f1 100644
--- a/modules/services/mapillary.js
+++ b/modules/services/mapillary.js
@@ -1,5 +1,4 @@
/* global Mapillary:false */
-import _filter from 'lodash-es/filter';
import _find from 'lodash-es/find';
import _flatten from 'lodash-es/flatten';
import _forEach from 'lodash-es/forEach';
@@ -18,10 +17,10 @@ import {
import rbush from 'rbush';
-import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile';
-import { geoExtent } from '../geo';
+import { geoExtent, geoScaleToZoom } from '../geo';
import { svgDefs } from '../svg';
-import { utilQsString, utilRebind } from '../util';
+import { utilQsString, utilRebind, utilTiler } from '../util';
+
var apibase = 'https://a.mapillary.com/v3/';
var viewercss = 'mapillary-js/mapillary.min.css';
@@ -29,7 +28,8 @@ var viewerjs = 'mapillary-js/mapillary.min.js';
var clientId = 'NzNRM2otQkR2SHJzaXJmNmdQWVQ0dzo1ZWYyMmYwNjdmNDdlNmVi';
var maxResults = 1000;
var tileZoom = 14;
-var dispatch = d3_dispatch('loadedImages', 'loadedSigns');
+var tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);
+var dispatch = d3_dispatch('loadedImages', 'loadedSigns', 'bearingChanged');
var _mlyFallback = false;
var _mlyCache;
var _mlyClicks;
@@ -42,18 +42,6 @@ function abortRequest(i) {
}
-function nearNullIsland(x, y, z) {
- if (z >= 7) {
- var center = Math.pow(2, z - 1);
- var width = Math.pow(2, z - 6);
- var min = center - (width / 2);
- var max = center + (width / 2) - 1;
- return x >= min && x <= max && y >= min && y <= max;
- }
- return false;
-}
-
-
function maxPageAtZoom(z) {
if (z < 15) return 2;
if (z === 15) return 5;
@@ -63,50 +51,22 @@ function maxPageAtZoom(z) {
if (z > 18) return 80;
}
-function getTiles(projection) {
- var s = projection.scale() * 2 * Math.PI;
- var z = Math.max(Math.log(s) / Math.log(2) - 8, 0);
- var ts = 256 * Math.pow(2, z - tileZoom);
- var origin = [
- s / 2 - projection.translate()[0],
- s / 2 - projection.translate()[1]
- ];
-
- return d3_geoTile()
- .scaleExtent([tileZoom, tileZoom])
- .scale(s)
- .size(projection.clipExtent()[1])
- .translate(projection.translate())()
- .map(function(tile) {
- var x = tile[0] * ts - origin[0];
- var y = tile[1] * ts - origin[1];
-
- return {
- id: tile.toString(),
- xyz: tile,
- extent: geoExtent(
- projection.invert([x, y + ts]),
- projection.invert([x + ts, y])
- )
- };
- });
-}
-
function loadTiles(which, url, projection) {
- var s = projection.scale() * 2 * Math.PI;
- var currZoom = Math.floor(Math.max(Math.log(s) / Math.log(2) - 8, 0));
+ var currZoom = Math.floor(geoScaleToZoom(projection.scale()));
+ var tiles = tiler.getTiles(projection);
- var tiles = getTiles(projection).filter(function(t) {
- return !nearNullIsland(t.xyz[0], t.xyz[1], t.xyz[2]);
+ // abort inflight requests that are no longer needed
+ var cache = _mlyCache[which];
+ _forEach(cache.inflight, function(v, k) {
+ var wanted = _find(tiles, function(tile) { return k.indexOf(tile.id + ',') === 0; });
+
+ if (!wanted) {
+ abortRequest(v);
+ delete cache.inflight[k];
+ }
});
- _filter(which.inflight, function(v, k) {
- var wanted = _find(tiles, function(tile) { return k === (tile.id + ',0'); });
- if (!wanted) delete which.inflight[k];
- return !wanted;
- }).map(abortRequest);
-
tiles.forEach(function(tile) {
loadNextTilePage(which, currZoom, url, tile);
});
@@ -410,6 +370,13 @@ export default {
// load mapillary signs sprite
var defs = context.container().select('defs');
defs.call(svgDefs(context).addSprites, ['mapillary-sprite']);
+
+ // Register viewer resize handler
+ context.ui().on('photoviewerResize', function() {
+ if (_mlyViewer) {
+ _mlyViewer.resize();
+ }
+ });
},
@@ -504,6 +471,7 @@ export default {
_mlyViewer = new Mapillary.Viewer('mly', clientId, null, opts);
_mlyViewer.on('nodechanged', nodeChanged);
+ _mlyViewer.on('bearingchanged', bearingChanged);
_mlyViewer.moveToKey(imageKey)
.catch(function(e) { console.error('mly3', e); }); // eslint-disable-line no-console
}
@@ -538,6 +506,10 @@ export default {
that.selectImage(undefined, node.key, true);
}
}
+
+ function bearingChanged(e) {
+ dispatch.call('bearingChanged', undefined, e);
+ }
},
diff --git a/modules/services/maprules.js b/modules/services/maprules.js
new file mode 100644
index 000000000..fd49f6fa4
--- /dev/null
+++ b/modules/services/maprules.js
@@ -0,0 +1,213 @@
+import _isMatch from 'lodash-es/isMatch';
+import _intersection from 'lodash-es/intersection';
+import _reduce from 'lodash-es/reduce';
+import _every from 'lodash-es/every';
+
+var ruleChecks,
+ validationRules;
+
+var buildRuleChecks = function() {
+ return {
+ equals: function (equals) {
+ return function(tags) {
+ return _isMatch(tags, equals);
+ };
+ },
+ notEquals: function (notEquals) {
+ return function(tags) {
+ return !_isMatch(tags, notEquals);
+ };
+ },
+ absence: function(absence) {
+ return function(tags) {
+ return Object.keys(tags).indexOf(absence) === -1;
+ };
+ },
+ presence: function(presence) {
+ return function(tags) {
+ return Object.keys(tags).indexOf(presence) > -1;
+ };
+ },
+ greaterThan: function(greaterThan) {
+ var key = Object.keys(greaterThan)[0];
+ var value = greaterThan[key];
+
+ return function(tags) {
+ return tags[key] > value;
+ };
+ },
+ greaterThanEqual: function(greaterThanEqual) {
+ var key = Object.keys(greaterThanEqual)[0];
+ var value = greaterThanEqual[key];
+
+ return function(tags) {
+ return tags[key] >= value;
+ };
+ },
+ lessThan: function(lessThan) {
+ var key = Object.keys(lessThan)[0];
+ var value = lessThan[key];
+
+ return function(tags) {
+ return tags[key] < value;
+ };
+ },
+ lessThanEqual: function(lessThanEqual) {
+ var key = Object.keys(lessThanEqual)[0];
+ var value = lessThanEqual[key];
+
+ return function(tags) {
+ return tags[key] <= value;
+ };
+ },
+ positiveRegex: function(positiveRegex) {
+ var tagKey = Object.keys(positiveRegex)[0];
+ var expression = positiveRegex[tagKey].join('|');
+ var regex = new RegExp(expression);
+
+ return function(tags) {
+ return regex.test(tags[tagKey]);
+ };
+ },
+ negativeRegex: function(negativeRegex) {
+ var tagKey = Object.keys(negativeRegex)[0];
+ var expression = negativeRegex[tagKey].join('|');
+ var regex = new RegExp(expression);
+
+ return function(tags) {
+ return !regex.test(tags[tagKey]);
+ };
+ }
+ };
+};
+
+
+export default {
+ init: function() {
+ ruleChecks = buildRuleChecks();
+ validationRules = [];
+ },
+ // list of rules only relevant to tag checks...
+ filterRuleChecks: function(selector) {
+ return _reduce(Object.keys(selector), function(rules, key) {
+ if (['geometry', 'error', 'warning'].indexOf(key) === -1) {
+ rules.push(ruleChecks[key](selector[key]));
+ }
+ return rules;
+ }, []);
+ },
+ // builds tagMap from mapcss-parse selector object...
+ buildTagMap: function(selector) {
+ var getRegexValues = function(regexes) {
+ return regexes.map(function(regex) {
+ return regex.replace(/\$|\^/g, '');
+ });
+ };
+
+ var selectorKeys = Object.keys(selector);
+ var tagMap = _reduce(selectorKeys, function (expectedTags, key) {
+ var values;
+ var isRegex = /regex/gi.test(key);
+ var isEqual = /equals/gi.test(key);
+
+ if (isRegex || isEqual) {
+ Object.keys(selector[key]).forEach(function(selectorKey) {
+ values = isEqual ? [selector[key][selectorKey]] : getRegexValues(selector[key][selectorKey]);
+
+ if (expectedTags.hasOwnProperty(selectorKey)) {
+ values = values.concat(expectedTags[selectorKey]);
+ }
+
+ expectedTags[selectorKey] = values;
+ });
+
+ } else if (/(greater|less)Than(Equal)?|presence/g.test(key)) {
+ var tagKey = /presence/.test(key) ? selector[key] : Object.keys(selector[key])[0];
+ expectedTags[tagKey] = [];
+
+ }
+
+ return expectedTags;
+ }, {});
+
+ return tagMap;
+ },
+ // inspired by osmWay#isArea()
+ inferGeometry: function(tagMap, areaKeys) {
+ var lineKeys = {
+ highway: {
+ rest_area: true,
+ services: true
+ },
+ railway: {
+ roundhouse: true,
+ station: true,
+ traverser: true,
+ turntable: true,
+ wash: true
+ }
+ };
+ var isAreaKeyBlackList = function(key) {
+ return _intersection(tagMap[key], Object.keys(areaKeys[key])).length > 0;
+ };
+ var isLineKeysWhiteList = function(key) {
+ return _intersection(tagMap[key], Object.keys(lineKeys[key])).length > 0;
+ };
+
+ if (tagMap.hasOwnProperty('area')) {
+ if (tagMap.area.indexOf('yes') > -1) {
+ return 'area';
+ }
+ if (tagMap.area.indexOf('no') > -1) {
+ return 'line';
+ }
+ }
+
+ for (var key in tagMap) {
+ if (key in areaKeys && !isAreaKeyBlackList(key)) {
+ return 'area';
+ }
+ if (key in lineKeys && isLineKeysWhiteList(key)) {
+ return 'area';
+ }
+ }
+
+ return 'line';
+ },
+ // adds from mapcss-parse selector check...
+ addRule: function(selector, areaKeys) {
+ var rule = {
+ // checks relevant to mapcss-selector
+ checks: this.filterRuleChecks(selector),
+ // true if all conditions for a tag error are true..
+ matches: function(entity) {
+ return _every(this.checks, function(check) {
+ return check(entity.tags);
+ });
+ },
+ // borrowed from Way#isArea()
+ inferredGeometry: this.inferGeometry(this.buildTagMap(selector), areaKeys),
+ geometryMatches: function(entity, graph) {
+ if (entity.type === 'node' || entity.type === 'relation') {
+ return selector.geometry === entity.type;
+ } else if (entity.type === 'way') {
+ return this.inferredGeometry === entity.geometry(graph);
+ }
+ },
+ // when geometries match and tag matches are present, return a warning...
+ findWarnings: function (entity, graph, warnings) {
+ if (this.geometryMatches(entity, graph) && this.matches(entity)) {
+ var type = Object.keys(selector).indexOf('error') > -1 ? 'error' : 'warning';
+ warnings.push({
+ id: 'mapcss_' + type,
+ message: selector[type],
+ entity: entity
+ });
+ }
+ }
+ };
+ validationRules.push(rule);
+ },
+ // returns validationRules...
+ validationRules: function() { return validationRules; }
+};
diff --git a/modules/services/openstreetcam.js b/modules/services/openstreetcam.js
index 14ded2d6b..40044e8ca 100644
--- a/modules/services/openstreetcam.js
+++ b/modules/services/openstreetcam.js
@@ -1,4 +1,3 @@
-import _filter from 'lodash-es/filter';
import _find from 'lodash-es/find';
import _flatten from 'lodash-es/flatten';
import _forEach from 'lodash-es/forEach';
@@ -22,29 +21,29 @@ import {
import rbush from 'rbush';
-import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile';
-import { geoExtent } from '../geo';
-
+import { geoExtent, geoScaleToZoom } from '../geo';
import { utilDetect } from '../util/detect';
import {
utilQsString,
utilRebind,
- utilSetTransform
+ utilSetTransform,
+ utilTiler
} from '../util';
-var apibase = 'https://openstreetcam.org',
- maxResults = 1000,
- tileZoom = 14,
- dispatch = d3_dispatch('loadedImages'),
- imgZoom = d3_zoom()
- .extent([[0, 0], [320, 240]])
- .translateExtent([[0, 0], [320, 240]])
- .scaleExtent([1, 15])
- .on('zoom', zoomPan),
- _oscCache,
- _oscSelectedImage;
+var apibase = 'https://openstreetcam.org';
+var maxResults = 1000;
+var tileZoom = 14;
+var tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);
+var dispatch = d3_dispatch('loadedImages');
+var imgZoom = d3_zoom()
+ .extent([[0, 0], [320, 240]])
+ .translateExtent([[0, 0], [320, 240]])
+ .scaleExtent([1, 15])
+ .on('zoom', zoomPan);
+var _oscCache;
+var _oscSelectedImage;
function abortRequest(i) {
@@ -52,18 +51,6 @@ function abortRequest(i) {
}
-function nearNullIsland(x, y, z) {
- if (z >= 7) {
- var center = Math.pow(2, z - 1),
- width = Math.pow(2, z - 6),
- min = center - (width / 2),
- max = center + (width / 2) - 1;
- return x >= min && x <= max && y >= min && y <= max;
- }
- return false;
-}
-
-
function maxPageAtZoom(z) {
if (z < 15) return 2;
if (z === 15) return 5;
@@ -74,48 +61,20 @@ function maxPageAtZoom(z) {
}
-function getTiles(projection) {
- var s = projection.scale() * 2 * Math.PI,
- z = Math.max(Math.log(s) / Math.log(2) - 8, 0),
- ts = 256 * Math.pow(2, z - tileZoom),
- origin = [
- s / 2 - projection.translate()[0],
- s / 2 - projection.translate()[1]];
-
- return d3_geoTile()
- .scaleExtent([tileZoom, tileZoom])
- .scale(s)
- .size(projection.clipExtent()[1])
- .translate(projection.translate())()
- .map(function(tile) {
- var x = tile[0] * ts - origin[0],
- y = tile[1] * ts - origin[1];
-
- return {
- id: tile.toString(),
- xyz: tile,
- extent: geoExtent(
- projection.invert([x, y + ts]),
- projection.invert([x + ts, y])
- )
- };
- });
-}
-
-
function loadTiles(which, url, projection) {
- var s = projection.scale() * 2 * Math.PI,
- currZoom = Math.floor(Math.max(Math.log(s) / Math.log(2) - 8, 0));
+ var currZoom = Math.floor(geoScaleToZoom(projection.scale()));
+ var tiles = tiler.getTiles(projection);
- var tiles = getTiles(projection).filter(function(t) {
- return !nearNullIsland(t.xyz[0], t.xyz[1], t.xyz[2]);
- });
+ // abort inflight requests that are no longer needed
+ var cache = _oscCache[which];
+ _forEach(cache.inflight, function(v, k) {
+ var wanted = _find(tiles, function(tile) { return k.indexOf(tile.id + ',') === 0; });
- _filter(which.inflight, function(v, k) {
- var wanted = _find(tiles, function(tile) { return k === (tile.id + ',0'); });
- if (!wanted) delete which.inflight[k];
- return !wanted;
- }).map(abortRequest);
+ if (!wanted) {
+ abortRequest(v);
+ delete cache.inflight[k];
+ }
+ });
tiles.forEach(function(tile) {
loadNextTilePage(which, currZoom, url, tile);
@@ -129,12 +88,12 @@ function loadNextTilePage(which, currZoom, url, tile) {
var maxPages = maxPageAtZoom(currZoom);
var nextPage = cache.nextPage[tile.id] || 1;
var params = utilQsString({
- ipp: maxResults,
- page: nextPage,
- // client_id: clientId,
- bbTopLeft: [bbox.maxY, bbox.minX].join(','),
- bbBottomRight: [bbox.minY, bbox.maxX].join(',')
- }, true);
+ ipp: maxResults,
+ page: nextPage,
+ // client_id: clientId,
+ bbTopLeft: [bbox.maxY, bbox.minX].join(','),
+ bbBottomRight: [bbox.minY, bbox.maxX].join(',')
+ }, true);
if (nextPage > maxPages) return;
@@ -160,8 +119,8 @@ function loadNextTilePage(which, currZoom, url, tile) {
}
var features = data.currentPageItems.map(function(item) {
- var loc = [+item.lng, +item.lat],
- d;
+ var loc = [+item.lng, +item.lat];
+ var d;
if (which === 'images') {
d = {
@@ -209,14 +168,14 @@ function loadNextTilePage(which, currZoom, url, tile) {
function partitionViewport(psize, projection) {
var dimensions = projection.clipExtent()[1];
psize = psize || 16;
- var cols = d3_range(0, dimensions[0], psize),
- rows = d3_range(0, dimensions[1], psize),
- partitions = [];
+ var cols = d3_range(0, dimensions[0], psize);
+ var rows = d3_range(0, dimensions[1], psize);
+ var partitions = [];
rows.forEach(function(y) {
cols.forEach(function(x) {
- var min = [x, y + psize],
- max = [x + psize, y];
+ var min = [x, y + psize];
+ var max = [x + psize, y];
partitions.push(
geoExtent(projection.invert(min), projection.invert(max)));
});
@@ -367,6 +326,16 @@ export default {
.attr('class', 'osc-image-wrap');
+ // Register viewer resize handler
+ context.ui().on('photoviewerResize', function(dimensions) {
+ imgZoom = d3_zoom()
+ .extent([[0, 0], dimensions])
+ .translateExtent([[0, 0], dimensions])
+ .scaleExtent([1, 15])
+ .on('zoom', zoomPan);
+ });
+
+
function rotate(deg) {
return function() {
if (!_oscSelectedImage) return;
diff --git a/modules/services/osm.js b/modules/services/osm.js
index 7e9faf49d..9e4f355a8 100644
--- a/modules/services/osm.js
+++ b/modules/services/osm.js
@@ -2,7 +2,6 @@ import _chunk from 'lodash-es/chunk';
import _cloneDeep from 'lodash-es/cloneDeep';
import _extend from 'lodash-es/extend';
import _forEach from 'lodash-es/forEach';
-import _filter from 'lodash-es/filter';
import _find from 'lodash-es/find';
import _groupBy from 'lodash-es/groupBy';
import _isEmpty from 'lodash-es/isEmpty';
@@ -17,7 +16,6 @@ import { xml as d3_xml } from 'd3-request';
import osmAuth from 'osm-auth';
import { JXON } from '../util/jxon';
-import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile';
import { geoExtent, geoVecAdd } from '../geo';
import {
@@ -31,10 +29,12 @@ import {
import {
utilRebind,
utilIdleWorker,
+ utilTiler,
utilQsString
} from '../util';
+var tiler = utilTiler();
var dispatch = d3_dispatch('authLoading', 'authDone', 'change', 'loading', 'loaded', 'loadedNotes');
var urlroot = 'https://www.openstreetmap.org';
var oauth = osmAuth({
@@ -77,6 +77,17 @@ function abortRequest(i) {
}
+function abortUnwantedRequests(cache, tiles) {
+ _forEach(cache.inflight, function(v, k) {
+ var wanted = _find(tiles, function(tile) { return k === tile.id; });
+ if (!wanted) {
+ abortRequest(v);
+ delete cache.inflight[k];
+ }
+ });
+}
+
+
function getLoc(attrs) {
var lon = attrs.lon && attrs.lon.value;
var lat = attrs.lat && attrs.lat.value;
@@ -159,6 +170,17 @@ function parseComments(comments) {
}
+function encodeNoteRtree(note) {
+ return {
+ minX: note.loc[0],
+ minY: note.loc[1],
+ maxX: note.loc[0],
+ maxY: note.loc[1],
+ data: note
+ };
+}
+
+
var parsers = {
node: function nodeData(obj, uid) {
var attrs = obj.attributes;
@@ -239,9 +261,10 @@ var parsers = {
}
var note = new osmNote(props);
- var item = { minX: note.loc[0], minY: note.loc[1], maxX: note.loc[0], maxY: note.loc[1], data: note };
- _noteCache.rtree.insert(item);
+ var item = encodeNoteRtree(note);
_noteCache.note[note.id] = note;
+ _noteCache.rtree.insert(item);
+
return note;
},
@@ -314,6 +337,16 @@ function parseXML(xml, callback, options) {
}
+// replace or remove note from rtree
+function updateRtree(item, replace) {
+ _noteCache.rtree.remove(item, function isEql(a, b) { return a.data.id === b.data.id; });
+
+ if (replace) {
+ _noteCache.rtree.insert(item);
+ }
+}
+
+
function wrapcb(thisArg, callback, cid) {
return function(err, result) {
if (err) {
@@ -519,7 +552,7 @@ export default {
return callback({ message: 'Changeset already inflight', status: -2 }, changeset);
} else if (_changeset.open) { // reuse existing open changeset..
- return createdChangeset(null, _changeset.open);
+ return createdChangeset.call(this, null, _changeset.open);
} else { // Open a new changeset..
var options = {
@@ -749,103 +782,44 @@ export default {
},
- // Load data (entities or notes) from the API in tiles
+ // Load data (entities) from the API in tiles
// GET /api/0.6/map?bbox=
- // GET /api/0.6/notes?bbox=
- loadTiles: function(projection, dimensions, callback, noteOptions) {
+ loadTiles: function(projection, callback) {
if (_off) return;
var that = this;
+ var path = '/api/0.6/map?bbox=';
- // are we loading entities or notes?
- var loadingNotes = (noteOptions !== undefined);
- var path, cache, tilezoom, throttleLoadUsers;
+ // determine the needed tiles to cover the view
+ var tiles = tiler.zoomExtent([_tileZoom, _tileZoom]).getTiles(projection);
- if (loadingNotes) {
- noteOptions = _extend({ limit: 10000, closed: 7}, noteOptions);
- path = '/api/0.6/notes?limit=' + noteOptions.limit + '&closed=' + noteOptions.closed + '&bbox=';
- cache = _noteCache;
- tilezoom = _noteZoom;
- throttleLoadUsers = _throttle(function() {
- var uids = Object.keys(_userCache.toLoad);
- if (!uids.length) return;
- that.loadUsers(uids, function() {}); // eagerly load user details
- }, 750);
- } else {
- path = '/api/0.6/map?bbox=';
- cache = _tileCache;
- tilezoom = _tileZoom;
- }
-
- var s = projection.scale() * 2 * Math.PI;
- var z = Math.max(Math.log(s) / Math.log(2) - 8, 0);
- var ts = 256 * Math.pow(2, z - tilezoom);
- var origin = [
- s / 2 - projection.translate()[0],
- s / 2 - projection.translate()[1]
- ];
-
- // what tiles cover the view?
- var tiler = d3_geoTile()
- .scaleExtent([tilezoom, tilezoom])
- .scale(s)
- .size(dimensions)
- .translate(projection.translate());
-
- var tiles = tiler().map(function(tile) {
- var x = tile[0] * ts - origin[0];
- var y = tile[1] * ts - origin[1];
-
- return {
- id: tile.toString(),
- extent: geoExtent(
- projection.invert([x, y + ts]),
- projection.invert([x + ts, y])
- )
- };
- });
-
- // remove inflight requests that no longer cover the view..
- var hadRequests = !_isEmpty(cache.inflight);
- _filter(cache.inflight, function(v, i) {
- var wanted = _find(tiles, function(tile) { return i === tile.id; });
- if (!wanted) {
- delete cache.inflight[i];
- }
- return !wanted;
- }).map(abortRequest);
-
- if (hadRequests && !loadingNotes && _isEmpty(cache.inflight)) {
+ // abort inflight requests that are no longer needed
+ var hadRequests = !_isEmpty(_tileCache.inflight);
+ abortUnwantedRequests(_tileCache, tiles);
+ if (hadRequests && _isEmpty(_tileCache.inflight)) {
dispatch.call('loaded'); // stop the spinner
}
// issue new requests..
tiles.forEach(function(tile) {
- if (cache.loaded[tile.id] || cache.inflight[tile.id]) return;
- if (!loadingNotes && _isEmpty(cache.inflight)) {
+ if (_tileCache.loaded[tile.id] || _tileCache.inflight[tile.id]) return;
+ if (_isEmpty(_tileCache.inflight)) {
dispatch.call('loading'); // start the spinner
}
- var options = { skipSeen: !loadingNotes };
- cache.inflight[tile.id] = that.loadFromAPI(
+ var options = { skipSeen: true };
+ _tileCache.inflight[tile.id] = that.loadFromAPI(
path + tile.extent.toParam(),
function(err, parsed) {
- delete cache.inflight[tile.id];
+ delete _tileCache.inflight[tile.id];
if (!err) {
- cache.loaded[tile.id] = true;
+ _tileCache.loaded[tile.id] = true;
}
-
- if (loadingNotes) {
- throttleLoadUsers();
- dispatch.call('loadedNotes');
-
- } else {
- if (callback) {
- callback(err, _extend({ data: parsed }, tile));
- }
- if (_isEmpty(cache.inflight)) {
- dispatch.call('loaded'); // stop the spinner
- }
+ if (callback) {
+ callback(err, _extend({ data: parsed }, tile));
+ }
+ if (_isEmpty(_tileCache.inflight)) {
+ dispatch.call('loaded'); // stop the spinner
}
},
options
@@ -854,18 +828,86 @@ export default {
},
- // Load notes from the API (just calls this.loadTiles)
+ // Load notes from the API in tiles
// GET /api/0.6/notes?bbox=
- loadNotes: function(projection, dimensions, noteOptions) {
- noteOptions = _extend({ limit: 10000, closed: 7}, noteOptions);
- this.loadTiles(projection, dimensions, null, noteOptions);
+ loadNotes: function(projection, noteOptions) {
+ noteOptions = _extend({ limit: 10000, closed: 7 }, noteOptions);
+ if (_off) return;
+
+ var that = this;
+ var path = '/api/0.6/notes?limit=' + noteOptions.limit + '&closed=' + noteOptions.closed + '&bbox=';
+ var throttleLoadUsers = _throttle(function() {
+ var uids = Object.keys(_userCache.toLoad);
+ if (!uids.length) return;
+ that.loadUsers(uids, function() {}); // eagerly load user details
+ }, 750);
+
+ // determine the needed tiles to cover the view
+ var tiles = tiler.zoomExtent([_noteZoom, _noteZoom]).getTiles(projection);
+
+ // abort inflight requests that are no longer needed
+ abortUnwantedRequests(_noteCache, tiles);
+
+ // issue new requests..
+ tiles.forEach(function(tile) {
+ if (_noteCache.loaded[tile.id] || _noteCache.inflight[tile.id]) return;
+
+ var options = { skipSeen: false };
+ _noteCache.inflight[tile.id] = that.loadFromAPI(
+ path + tile.extent.toParam(),
+ function(err) {
+ delete _noteCache.inflight[tile.id];
+ if (!err) {
+ _noteCache.loaded[tile.id] = true;
+ }
+ throttleLoadUsers();
+ dispatch.call('loadedNotes');
+ },
+ options
+ );
+ });
},
// Create a note
// POST /api/0.6/notes?params
postNoteCreate: function(note, callback) {
- // todo
+ if (!this.authenticated()) {
+ return callback({ message: 'Not Authenticated', status: -3 }, note);
+ }
+ if (_noteCache.inflightPost[note.id]) {
+ return callback({ message: 'Note update already inflight', status: -2 }, note);
+ }
+
+ if (!note.loc[0] || !note.loc[1] || !note.newComment) return; // location & description required
+
+ var comment = note.newComment;
+ if (note.newCategory && note.newCategory !== 'None') { comment += ' #' + note.newCategory; }
+
+ var path = '/api/0.6/notes?' + utilQsString({ lon: note.loc[0], lat: note.loc[1], text: comment });
+
+ _noteCache.inflightPost[note.id] = oauth.xhr(
+ { method: 'POST', path: path },
+ wrapcb(this, done, _connectionID)
+ );
+
+
+ function done(err, xml) {
+ delete _noteCache.inflightPost[note.id];
+ if (err) { return callback(err); }
+
+ // we get the updated note back, remove from caches and reparse..
+ this.removeNote(note);
+
+ var options = { skipSeen: false };
+ return parseXML(xml, function(err, results) {
+ if (err) {
+ return callback(err);
+ } else {
+ return callback(undefined, results[0]);
+ }
+ }, options);
+ }
},
@@ -888,6 +930,7 @@ export default {
action = 'reopen';
} else {
action = 'comment';
+ if (!note.newComment) return; // when commenting, comment required
}
var path = '/api/0.6/notes/' + note.id + '/' + action;
@@ -906,9 +949,7 @@ export default {
if (err) { return callback(err); }
// we get the updated note back, remove from caches and reparse..
- var item = { minX: note.loc[0], minY: note.loc[1], maxX: note.loc[0], maxY: note.loc[1], data: note };
- _noteCache.rtree.remove(item, function isEql(a, b) { return a.data.id === b.data.id; });
- delete _noteCache.note[note.id];
+ this.removeNote(note);
var options = { skipSeen: false };
return parseXML(xml, function(err, results) {
@@ -944,6 +985,11 @@ export default {
},
+ isChangesetInflight: function() {
+ return !!_changeset.inflight;
+ },
+
+
// get/set cached data
// This is used to save/restore the state when entering/exiting the walkthrough
// Also used for testing purposes.
@@ -1051,12 +1097,22 @@ export default {
},
+ // remove a single note from the cache
+ removeNote: function(note) {
+ if (!(note instanceof osmNote) || !note.id) return;
+
+ delete _noteCache.note[note.id];
+ updateRtree(encodeNoteRtree(note), false); // false = remove
+ },
+
+
// replace a single note in the cache
- replaceNote: function(n) {
- if (n instanceof osmNote) {
- _noteCache.note[n.id] = n;
- }
- return n;
+ replaceNote: function(note) {
+ if (!(note instanceof osmNote) || !note.id) return;
+
+ _noteCache.note[note.id] = note;
+ updateRtree(encodeNoteRtree(note), true); // true = replace
+ return note;
}
};
diff --git a/modules/services/streetside.js b/modules/services/streetside.js
index f13470b6c..8a0cd1b1e 100644
--- a/modules/services/streetside.js
+++ b/modules/services/streetside.js
@@ -1,4 +1,5 @@
import _extend from 'lodash-es/extend';
+import _find from 'lodash-es/find';
import _flatten from 'lodash-es/flatten';
import _forEach from 'lodash-es/forEach';
import _map from 'lodash-es/map';
@@ -17,7 +18,6 @@ import {
import rbush from 'rbush';
import { t } from '../util/locale';
import { jsonpRequest } from '../util/jsonp_request';
-import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile';
import {
geoExtent,
@@ -29,10 +29,11 @@ import {
} from '../geo';
import { utilDetect } from '../util/detect';
-import { utilQsString, utilRebind } from '../util';
+import { utilQsString, utilRebind, utilTiler } from '../util';
import Q from 'q';
+
var bubbleApi = 'https://dev.virtualearth.net/mapcontrol/HumanScaleServices/GetBubbles.ashx?';
var streetsideImagesApi = 'https://t.ssl.ak.tiles.virtualearth.net/tiles/';
var bubbleAppKey = 'AuftgJsO0Xs8Ts4M1xZUQJQXJNsvmh3IV8DkNieCiy3tCwCUMq76-WpkrBtNAuEm';
@@ -40,10 +41,12 @@ var pannellumViewerCSS = 'pannellum-streetside/pannellum.css';
var pannellumViewerJS = 'pannellum-streetside/pannellum.js';
var maxResults = 2000;
var tileZoom = 16.5;
+var tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);
var dispatch = d3_dispatch('loadedBubbles', 'viewerChanged');
var minHfov = 10; // zoom in degrees: 20, 10, 5
var maxHfov = 90; // zoom out degrees
var defaultHfov = 45;
+
var _hires = false;
var _resolution = 512; // higher numbers are slower - 512, 1024, 2048, 4096
var _currScene = 0;
@@ -52,6 +55,7 @@ var _pannellumViewer;
var _sceneOptions;
var _dataUrlArray = [];
+
/**
* abortRequest().
*/
@@ -59,19 +63,6 @@ function abortRequest(i) {
i.abort();
}
-/**
- * nearNullIsland().
- */
-function nearNullIsland(x, y, z) {
- if (z >= 7) {
- var center = Math.pow(2, z - 1);
- var width = Math.pow(2, z - 6);
- var min = center - (width / 2);
- var max = center + (width / 2) - 1;
- return x >= min && x <= max && y >= min && y <= max;
- }
- return false;
-}
/**
* localeTimeStamp().
@@ -85,68 +76,33 @@ function localeTimestamp(s) {
return d.toLocaleString(detected.locale, options);
}
-/**
- * getTiles() returns array of d3 geo tiles.
- * Using d3.geo.tiles.js from lib, gets tile extents for each grid tile in a grid created from
- * an area around (and including) the current map view extents.
- */
-function getTiles(projection, margin) {
- // s is the current map scale
- // z is the 'Level of Detail', or zoom-level, where Level 1 is far from the earth, and Level 23 is close to the ground.
- // ts ('tile size') here is the formula for determining the width/height of the map in pixels, but with a modification.
- // See 'Ground Resolution and Map Scale': //https://msdn.microsoft.com/en-us/library/bb259689.aspx.
- // As used here, by subtracting constant 'tileZoom' from z (the level), you end up with a much smaller value for the tile size (in pixels).
- var s = projection.scale() * 2 * Math.PI;
- var z = Math.max(Math.log(s) / Math.log(2) - 8, 0);
- var ts = 256 * Math.pow(2, z - tileZoom);
- var origin = [
- s / 2 - projection.translate()[0],
- s / 2 - projection.translate()[1]
- ];
-
- var tiler = d3_geoTile()
- .scaleExtent([tileZoom, tileZoom])
- .scale(s)
- .size(projection.clipExtent()[1])
- .translate(projection.translate())
- .margin(margin || 0); // request nearby tiles so we can connect sequences.
-
- return tiler()
- .map(function(tile) {
- var x = tile[0] * ts - origin[0];
- var y = tile[1] * ts - origin[1];
- return {
- id: tile.toString(),
- xyz: tile,
- extent: geoExtent(
- projection.invert([x, y + ts]),
- projection.invert([x + ts, y])
- )
- };
- });
-}
/**
* loadTiles() wraps the process of generating tiles and then fetching image points for each tile.
*/
function loadTiles(which, url, projection, margin) {
- var s = projection.scale() * 2 * Math.PI;
- var currZoom = Math.floor(Math.max(Math.log(s) / Math.log(2) - 8, 0));
+ var tiles = tiler.margin(margin).getTiles(projection);
- // breakup the map view into tiles
- var tiles = getTiles(projection, margin).filter(function (t) {
- return !nearNullIsland(t.xyz[0], t.xyz[1], t.xyz[2]);
+ // abort inflight requests that are no longer needed
+ var cache = _ssCache[which];
+ _forEach(cache.inflight, function(v, k) {
+ var wanted = _find(tiles, function(tile) { return k.indexOf(tile.id + ',') === 0; });
+
+ if (!wanted) {
+ abortRequest(v);
+ delete cache.inflight[k];
+ }
});
tiles.forEach(function (tile) {
- loadNextTilePage(which, currZoom, url, tile);
+ loadNextTilePage(which, url, tile);
});
}
/**
* loadNextTilePage() load data for the next tile page in line.
*/
-function loadNextTilePage(which, currZoom, url, tile) {
+function loadNextTilePage(which, url, tile) {
var cache = _ssCache[which];
var nextPage = cache.nextPage[tile.id] || 0;
var id = tile.id + ',' + String(nextPage);
@@ -160,7 +116,7 @@ function loadNextTilePage(which, currZoom, url, tile) {
// [].shift() removes the first element, some statistics info, not a bubble point
bubbles.shift();
- var features = bubbles.map(function (bubble) {
+ var features = bubbles.map(function(bubble) {
if (cache.points[bubble.id]) return null; // skip duplicates
var loc = [bubble.lo, bubble.la];
@@ -669,6 +625,14 @@ export default {
.attr('src', context.asset(pannellumViewerJS));
+ // Register viewer resize handler
+ context.ui().on('photoviewerResize', function() {
+ if (_pannellumViewer) {
+ _pannellumViewer.resize();
+ }
+ });
+
+
function step(stepBy) {
return function() {
var viewer = d3_select('#photoviewer');
diff --git a/modules/services/taginfo.js b/modules/services/taginfo.js
index 88c0c6c5b..cc8df7041 100644
--- a/modules/services/taginfo.js
+++ b/modules/services/taginfo.js
@@ -264,7 +264,7 @@ export default {
// A few OSM keys expect values to contain uppercase values (see #3377).
// This is not an exhaustive list (e.g. `name` also has uppercase values)
// but these are the fields where taginfo value lookup is most useful.
- var re = /network|taxon|genus|species|brand|grape_variety|royal_cypher|booth|rating|:output|_hours|_times/;
+ var re = /network|taxon|genus|species|brand|grape_variety|royal_cypher|listed_status|booth|rating|stars|:output|_hours|_times/;
var allowUpperCase = (params.key.match(re) !== null);
var f = filterValues(allowUpperCase);
diff --git a/modules/services/vector_tile.js b/modules/services/vector_tile.js
new file mode 100644
index 000000000..d34a5181a
--- /dev/null
+++ b/modules/services/vector_tile.js
@@ -0,0 +1,215 @@
+import _clone from 'lodash-es/clone';
+import _find from 'lodash-es/find';
+import _isEqual from 'lodash-es/isEqual';
+import _forEach from 'lodash-es/forEach';
+
+import { dispatch as d3_dispatch } from 'd3-dispatch';
+import { request as d3_request } from 'd3-request';
+
+import turf_bboxClip from '@turf/bbox-clip';
+import stringify from 'fast-json-stable-stringify';
+import martinez from 'martinez-polygon-clipping';
+
+import Protobuf from 'pbf';
+import vt from '@mapbox/vector-tile';
+
+import { utilHashcode, utilRebind, utilTiler } from '../util';
+
+
+var tiler = utilTiler().tileSize(512).margin(1);
+var dispatch = d3_dispatch('loadedData');
+var _vtCache;
+
+
+function abortRequest(i) {
+ i.abort();
+}
+
+
+function vtToGeoJSON(data, tile, mergeCache) {
+ var vectorTile = new vt.VectorTile(new Protobuf(data.response));
+ var layers = Object.keys(vectorTile.layers);
+ if (!Array.isArray(layers)) { layers = [layers]; }
+
+ var features = [];
+ layers.forEach(function(layerID) {
+ var layer = vectorTile.layers[layerID];
+ if (layer) {
+ for (var i = 0; i < layer.length; i++) {
+ var feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);
+ var geometry = feature.geometry;
+
+ // Treat all Polygons as MultiPolygons
+ if (geometry.type === 'Polygon') {
+ geometry.type = 'MultiPolygon';
+ geometry.coordinates = [geometry.coordinates];
+ }
+
+ // Clip to tile bounds
+ if (geometry.type === 'MultiPolygon') {
+ var isClipped = false;
+ var featureClip = turf_bboxClip(feature, tile.extent.rectangle());
+ if (!_isEqual(feature.geometry, featureClip.geometry)) {
+ // feature = featureClip;
+ isClipped = true;
+ }
+ if (!feature.geometry.coordinates.length) continue; // not actually on this tile
+ if (!feature.geometry.coordinates[0].length) continue; // not actually on this tile
+ }
+
+ // Generate some unique IDs and add some metadata
+ var featurehash = utilHashcode(stringify(feature));
+ var propertyhash = utilHashcode(stringify(feature.properties || {}));
+ feature.__layerID__ = layerID.replace(/[^_a-zA-Z0-9\-]/g, '_');
+ feature.__featurehash__ = featurehash;
+ feature.__propertyhash__ = propertyhash;
+ features.push(feature);
+
+ // Clipped Polygons at same zoom with identical properties can get merged
+ if (isClipped && geometry.type === 'MultiPolygon') {
+ var merged = mergeCache[propertyhash];
+ if (merged && merged.length) {
+ var other = merged[0];
+ var coords = martinez.union(
+ feature.geometry.coordinates, other.geometry.coordinates
+ );
+
+ if (!coords || !coords.length) {
+ continue; // something failed in martinez union
+ }
+
+ merged.push(feature);
+ for (var j = 0; j < merged.length; j++) { // all these features get...
+ merged[j].geometry.coordinates = coords; // same coords
+ merged[j].__featurehash__ = featurehash; // same hash, so deduplication works
+ }
+ } else {
+ mergeCache[propertyhash] = [feature];
+ }
+ }
+ }
+ }
+ });
+
+ return features;
+}
+
+
+function loadTile(source, tile) {
+ if (source.loaded[tile.id] || source.inflight[tile.id]) return;
+
+ var url = source.template
+ .replace('{x}', tile.xyz[0])
+ .replace('{y}', tile.xyz[1])
+ // TMS-flipped y coordinate
+ .replace(/\{[t-]y\}/, Math.pow(2, tile.xyz[2]) - tile.xyz[1] - 1)
+ .replace(/\{z(oom)?\}/, tile.xyz[2])
+ .replace(/\{switch:([^}]+)\}/, function(s, r) {
+ var subdomains = r.split(',');
+ return subdomains[(tile.xyz[0] + tile.xyz[1]) % subdomains.length];
+ });
+
+ source.inflight[tile.id] = d3_request(url)
+ .responseType('arraybuffer')
+ .get(function(err, data) {
+ source.loaded[tile.id] = [];
+ delete source.inflight[tile.id];
+ if (err || !data) return;
+
+ var z = tile.xyz[2];
+ if (!source.canMerge[z]) {
+ source.canMerge[z] = {}; // initialize mergeCache
+ }
+
+ source.loaded[tile.id] = vtToGeoJSON(data, tile, source.canMerge[z]);
+ dispatch.call('loadedData');
+ });
+}
+
+
+export default {
+
+ init: function() {
+ if (!_vtCache) {
+ this.reset();
+ }
+
+ this.event = utilRebind(this, dispatch, 'on');
+ },
+
+
+ reset: function() {
+ for (var sourceID in _vtCache) {
+ var source = _vtCache[sourceID];
+ if (source && source.inflight) {
+ _forEach(source.inflight, abortRequest);
+ }
+ }
+
+ _vtCache = {};
+ },
+
+
+ addSource: function(sourceID, template) {
+ _vtCache[sourceID] = { template: template, inflight: {}, loaded: {}, canMerge: {} };
+ return _vtCache[sourceID];
+ },
+
+
+ data: function(sourceID, projection) {
+ var source = _vtCache[sourceID];
+ if (!source) return [];
+
+ var tiles = tiler.getTiles(projection);
+ var seen = {};
+ var results = [];
+
+ for (var i = 0; i < tiles.length; i++) {
+ var features = source.loaded[tiles[i].id];
+ if (!features || !features.length) continue;
+
+ for (var j = 0; j < features.length; j++) {
+ var feature = features[j];
+ var hash = feature.__featurehash__;
+ if (seen[hash]) continue;
+ seen[hash] = true;
+
+ // return a shallow clone, because the hash may change
+ // later if this feature gets merged with another
+ results.push(_clone(feature));
+ }
+ }
+
+ return results;
+ },
+
+
+ loadTiles: function(sourceID, template, projection) {
+ var source = _vtCache[sourceID];
+ if (!source) {
+ source = this.addSource(sourceID, template);
+ }
+
+ var tiles = tiler.getTiles(projection);
+
+ // abort inflight requests that are no longer needed
+ _forEach(source.inflight, function(v, k) {
+ var wanted = _find(tiles, function(tile) { return k === tile.id; });
+
+ if (!wanted) {
+ abortRequest(v);
+ delete source.inflight[k];
+ }
+ });
+
+ tiles.forEach(function(tile) {
+ loadTile(source, tile);
+ });
+ },
+
+
+ cache: function() {
+ return _vtCache;
+ }
+
+};
diff --git a/modules/svg/areas.js b/modules/svg/areas.js
index 01a12de7f..546919ecd 100644
--- a/modules/svg/areas.js
+++ b/modules/svg/areas.js
@@ -132,7 +132,7 @@ export function svgAreas(projection, context) {
fill: areas
};
- var clipPaths = context.surface().selectAll('defs').selectAll('.clipPath')
+ var clipPaths = context.surface().selectAll('defs').selectAll('.clipPath-osm')
.filter(filter)
.data(data.clip, osmEntity.key);
@@ -141,7 +141,7 @@ export function svgAreas(projection, context) {
var clipPathsEnter = clipPaths.enter()
.append('clipPath')
- .attr('class', 'clipPath')
+ .attr('class', 'clipPath-osm')
.attr('id', function(entity) { return entity.id + '-clippath'; });
clipPathsEnter
diff --git a/modules/svg/data.js b/modules/svg/data.js
new file mode 100644
index 000000000..57330d993
--- /dev/null
+++ b/modules/svg/data.js
@@ -0,0 +1,541 @@
+import _flatten from 'lodash-es/flatten';
+import _isEmpty from 'lodash-es/isEmpty';
+import _reduce from 'lodash-es/reduce';
+import _union from 'lodash-es/union';
+import _throttle from 'lodash-es/throttle';
+
+import {
+ geoBounds as d3_geoBounds,
+ geoPath as d3_geoPath
+} from 'd3-geo';
+
+import { text as d3_text } from 'd3-request';
+
+import {
+ event as d3_event,
+ select as d3_select
+} from 'd3-selection';
+
+import stringify from 'fast-json-stable-stringify';
+import toGeoJSON from '@mapbox/togeojson';
+
+import { geoExtent, geoPolygonIntersectsPolygon } from '../geo';
+import { services } from '../services';
+import { svgPath } from './index';
+import { utilDetect } from '../util/detect';
+import { utilHashcode } from '../util';
+
+
+var _initialized = false;
+var _enabled = false;
+var _geojson;
+
+
+export function svgData(projection, context, dispatch) {
+ var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
+ var _showLabels = true;
+ var detected = utilDetect();
+ var layer = d3_select(null);
+ var _vtService;
+ var _fileList;
+ var _template;
+ var _src;
+
+
+ function init() {
+ if (_initialized) return; // run once
+
+ _geojson = {};
+ _enabled = true;
+
+ function over() {
+ d3_event.stopPropagation();
+ d3_event.preventDefault();
+ d3_event.dataTransfer.dropEffect = 'copy';
+ }
+
+ d3_select('body')
+ .attr('dropzone', 'copy')
+ .on('drop.svgData', function() {
+ d3_event.stopPropagation();
+ d3_event.preventDefault();
+ if (!detected.filedrop) return;
+ drawData.fileList(d3_event.dataTransfer.files);
+ })
+ .on('dragenter.svgData', over)
+ .on('dragexit.svgData', over)
+ .on('dragover.svgData', over);
+
+ _initialized = true;
+ }
+
+
+ function getService() {
+ if (services.vectorTile && !_vtService) {
+ _vtService = services.vectorTile;
+ _vtService.event.on('loadedData', throttledRedraw);
+ } else if (!services.vectorTile && _vtService) {
+ _vtService = null;
+ }
+
+ return _vtService;
+ }
+
+
+ function showLayer() {
+ layerOn();
+
+ layer
+ .style('opacity', 0)
+ .transition()
+ .duration(250)
+ .style('opacity', 1)
+ .on('end', function () { dispatch.call('change'); });
+ }
+
+
+ function hideLayer() {
+ throttledRedraw.cancel();
+
+ layer
+ .transition()
+ .duration(250)
+ .style('opacity', 0)
+ .on('end', layerOff);
+ }
+
+
+ function layerOn() {
+ layer.style('display', 'block');
+ }
+
+
+ function layerOff() {
+ layer.selectAll('.viewfield-group').remove();
+ layer.style('display', 'none');
+ }
+
+
+ // ensure that all geojson features in a collection have IDs
+ function ensureIDs(gj) {
+ if (!gj) return null;
+
+ if (gj.type === 'FeatureCollection') {
+ for (var i = 0; i < gj.features.length; i++) {
+ ensureFeatureID(gj.features[i]);
+ }
+ } else {
+ ensureFeatureID(gj);
+ }
+ return gj;
+ }
+
+
+ // ensure that each single Feature object has a unique ID
+ function ensureFeatureID(feature) {
+ if (!feature) return;
+ feature.__featurehash__ = utilHashcode(stringify(feature));
+ return feature;
+ }
+
+
+ // Prefer an array of Features instead of a FeatureCollection
+ function getFeatures(gj) {
+ if (!gj) return [];
+
+ if (gj.type === 'FeatureCollection') {
+ return gj.features;
+ } else {
+ return [gj];
+ }
+ }
+
+
+ function featureKey(d) {
+ return d.__featurehash__;
+ }
+
+
+ function isPolygon(d) {
+ return d.geometry.type === 'Polygon' || d.geometry.type === 'MultiPolygon';
+ }
+
+
+ function clipPathID(d) {
+ return 'data-' + d.__featurehash__ + '-clippath';
+ }
+
+
+ function featureClasses(d) {
+ return [
+ 'data' + d.__featurehash__,
+ d.geometry.type,
+ isPolygon(d) ? 'area' : '',
+ d.__layerID__ || ''
+ ].filter(Boolean).join(' ');
+ }
+
+
+ function drawData(selection) {
+ var vtService = getService();
+ var getPath = svgPath(projection).geojson;
+ var getAreaPath = svgPath(projection, null, true).geojson;
+ var hasData = drawData.hasData();
+
+ layer = selection.selectAll('.layer-mapdata')
+ .data(_enabled && hasData ? [0] : []);
+
+ layer.exit()
+ .remove();
+
+ layer = layer.enter()
+ .append('g')
+ .attr('class', 'layer-mapdata')
+ .merge(layer);
+
+ var surface = context.surface();
+ if (!surface || surface.empty()) return; // not ready to draw yet, starting up
+
+
+ // Gather data
+ var geoData, polygonData;
+ if (_template && vtService) { // fetch data from vector tile service
+ var sourceID = _template;
+ vtService.loadTiles(sourceID, _template, projection);
+ geoData = vtService.data(sourceID, projection);
+ } else {
+ geoData = getFeatures(_geojson);
+ }
+ geoData = geoData.filter(getPath);
+ polygonData = geoData.filter(isPolygon);
+
+
+ // Draw clip paths for polygons
+ var clipPaths = surface.selectAll('defs').selectAll('.clipPath-data')
+ .data(polygonData, featureKey);
+
+ clipPaths.exit()
+ .remove();
+
+ var clipPathsEnter = clipPaths.enter()
+ .append('clipPath')
+ .attr('class', 'clipPath-data')
+ .attr('id', clipPathID);
+
+ clipPathsEnter
+ .append('path');
+
+ clipPaths.merge(clipPathsEnter)
+ .selectAll('path')
+ .attr('d', getAreaPath);
+
+
+ // Draw fill, shadow, stroke layers
+ var datagroups = layer
+ .selectAll('g.datagroup')
+ .data(['fill', 'shadow', 'stroke']);
+
+ datagroups = datagroups.enter()
+ .append('g')
+ .attr('class', function(d) { return 'datagroup datagroup-' + d; })
+ .merge(datagroups);
+
+
+ // Draw paths
+ var pathData = {
+ fill: polygonData,
+ shadow: geoData,
+ stroke: geoData
+ };
+
+ var paths = datagroups
+ .selectAll('path')
+ .data(function(layer) { return pathData[layer]; }, featureKey);
+
+ // exit
+ paths.exit()
+ .remove();
+
+ // enter/update
+ paths = paths.enter()
+ .append('path')
+ .attr('class', function(d) {
+ var datagroup = this.parentNode.__data__;
+ return 'pathdata ' + datagroup + ' ' + featureClasses(d);
+ })
+ .attr('clip-path', function(d) {
+ var datagroup = this.parentNode.__data__;
+ return datagroup === 'fill' ? ('url(#' + clipPathID(d) + ')') : null;
+ })
+ .merge(paths)
+ .attr('d', function(d) {
+ var datagroup = this.parentNode.__data__;
+ return datagroup === 'fill' ? getAreaPath(d) : getPath(d);
+ });
+
+
+ // Draw labels
+ layer
+ .call(drawLabels, 'label-halo', geoData)
+ .call(drawLabels, 'label', geoData);
+
+
+ function drawLabels(selection, textClass, data) {
+ var labelPath = d3_geoPath(projection);
+ var labelData = data.filter(function(d) {
+ return _showLabels && d.properties && (d.properties.desc || d.properties.name);
+ });
+
+ var labels = selection.selectAll('text.' + textClass)
+ .data(labelData, featureKey);
+
+ // exit
+ labels.exit()
+ .remove();
+
+ // enter/update
+ labels = labels.enter()
+ .append('text')
+ .attr('class', function(d) { return textClass + ' ' + featureClasses(d); })
+ .merge(labels)
+ .text(function(d) {
+ return d.properties.desc || d.properties.name;
+ })
+ .attr('x', function(d) {
+ var centroid = labelPath.centroid(d);
+ return centroid[0] + 11;
+ })
+ .attr('y', function(d) {
+ var centroid = labelPath.centroid(d);
+ return centroid[1];
+ });
+ }
+ }
+
+
+ function getExtension(fileName) {
+ if (!fileName) return;
+
+ var re = /\.(gpx|kml|(geo)?json)$/i;
+ var match = fileName.toLowerCase().match(re);
+ return match && match.length && match[0];
+ }
+
+
+ function xmlToDom(textdata) {
+ return (new DOMParser()).parseFromString(textdata, 'text/xml');
+ }
+
+
+ drawData.setFile = function(extension, data) {
+ _template = null;
+ _fileList = null;
+ _geojson = null;
+ _src = null;
+
+ var gj;
+ switch (extension) {
+ case '.gpx':
+ gj = toGeoJSON.gpx(xmlToDom(data));
+ break;
+ case '.kml':
+ gj = toGeoJSON.kml(xmlToDom(data));
+ break;
+ case '.geojson':
+ case '.json':
+ gj = JSON.parse(data);
+ break;
+ }
+
+ if (!_isEmpty(gj)) {
+ _geojson = ensureIDs(gj);
+ _src = extension + ' data file';
+ this.fitZoom();
+ }
+
+ dispatch.call('change');
+ return this;
+ };
+
+
+ drawData.showLabels = function(val) {
+ if (!arguments.length) return _showLabels;
+
+ _showLabels = val;
+ return this;
+ };
+
+
+ drawData.enabled = function(val) {
+ if (!arguments.length) return _enabled;
+
+ _enabled = val;
+ if (_enabled) {
+ showLayer();
+ } else {
+ hideLayer();
+ }
+
+ dispatch.call('change');
+ return this;
+ };
+
+
+ drawData.hasData = function() {
+ return !!(_template || !_isEmpty(_geojson));
+ };
+
+
+ drawData.template = function(val, src) {
+ if (!arguments.length) return _template;
+
+ // test source against OSM imagery blacklists..
+ var osm = context.connection();
+ if (osm) {
+ var blacklists = osm.imageryBlacklists();
+ var fail = false;
+ var tested = 0;
+ var regex;
+
+ for (var i = 0; i < blacklists.length; i++) {
+ try {
+ regex = new RegExp(blacklists[i]);
+ fail = regex.test(val);
+ tested++;
+ if (fail) break;
+ } catch (e) {
+ /* noop */
+ }
+ }
+
+ // ensure at least one test was run.
+ if (!tested) {
+ regex = new RegExp('.*\.google(apis)?\..*/(vt|kh)[\?/].*([xyz]=.*){3}.*');
+ fail = regex.test(val);
+ }
+ }
+
+ _template = val;
+ _fileList = null;
+ _geojson = null;
+
+ // strip off the querystring/hash from the template,
+ // it often includes the access token
+ _src = src || ('vectortile:' + val.split(/[?#]/)[0]);
+
+ dispatch.call('change');
+ return this;
+ };
+
+
+ drawData.geojson = function(gj, src) {
+ if (!arguments.length) return _geojson;
+
+ _template = null;
+ _fileList = null;
+ _geojson = null;
+ _src = null;
+
+ if (!_isEmpty(gj)) {
+ _geojson = ensureIDs(gj);
+ _src = src || 'unknown.geojson';
+ }
+
+ dispatch.call('change');
+ return this;
+ };
+
+
+ drawData.fileList = function(fileList) {
+ if (!arguments.length) return _fileList;
+
+ _template = null;
+ _fileList = fileList;
+ _geojson = null;
+ _src = null;
+
+ if (!fileList || !fileList.length) return this;
+ var f = fileList[0];
+ var extension = getExtension(f.name);
+ var reader = new FileReader();
+ reader.onload = (function() {
+ return function(e) {
+ drawData.setFile(extension, e.target.result);
+ };
+ })(f);
+
+ reader.readAsText(f);
+
+ return this;
+ };
+
+
+ drawData.url = function(url, defaultExtension) {
+ _template = null;
+ _fileList = null;
+ _geojson = null;
+ _src = null;
+
+ // strip off any querystring/hash from the url before checking extension
+ var testUrl = url.split(/[?#]/)[0];
+ var extension = getExtension(testUrl) || defaultExtension;
+ if (extension) {
+ _template = null;
+ d3_text(url, function(err, data) {
+ if (err) return;
+ drawData.setFile(extension, data);
+ });
+ } else {
+ drawData.template(url);
+ }
+
+ return this;
+ };
+
+
+ drawData.getSrc = function() {
+ return _src || '';
+ };
+
+
+ drawData.fitZoom = function() {
+ var features = getFeatures(_geojson);
+ if (!features.length) return;
+
+ var map = context.map();
+ var viewport = map.trimmedExtent().polygon();
+ var coords = _reduce(features, function(coords, feature) {
+ var c = feature.geometry.coordinates;
+
+ /* eslint-disable no-fallthrough */
+ switch (feature.geometry.type) {
+ case 'Point':
+ c = [c];
+ case 'MultiPoint':
+ case 'LineString':
+ break;
+
+ case 'MultiPolygon':
+ c = _flatten(c);
+ case 'Polygon':
+ case 'MultiLineString':
+ c = _flatten(c);
+ break;
+ }
+ /* eslint-enable no-fallthrough */
+
+ return _union(coords, c);
+ }, []);
+
+ if (!geoPolygonIntersectsPolygon(viewport, coords, true)) {
+ var extent = geoExtent(d3_geoBounds({ type: 'LineString', coordinates: coords }));
+ map.centerZoom(extent.center(), map.trimmedExtentZoom(extent));
+ }
+
+ return this;
+ };
+
+
+ init();
+ return drawData;
+}
diff --git a/modules/svg/debug.js b/modules/svg/debug.js
index 26e6e112e..8476991ff 100644
--- a/modules/svg/debug.js
+++ b/modules/svg/debug.js
@@ -2,22 +2,12 @@ import _values from 'lodash-es/values';
import { select as d3_select } from 'd3-selection';
-import { geoPolygonIntersectsPolygon } from '../geo';
import { data, dataImperial, dataDriveLeft } from '../../data';
import { svgPath } from './index';
export function svgDebug(projection, context) {
- function multipolygons(imagery) {
- return imagery.map(function(data) {
- return {
- type: 'MultiPolygon',
- coordinates: [ data.polygon ]
- };
- });
- }
-
function drawDebug(selection) {
var showsTile = context.getDebug('tile');
var showsCollision = context.getDebug('collision');
@@ -89,16 +79,11 @@ export function svgDebug(projection, context) {
var extent = context.map().extent();
- var dataImagery = data.imagery || [];
- var availableImagery = showsImagery && multipolygons(dataImagery.filter(function(source) {
- if (!source.polygon) return false;
- return source.polygon.some(function(polygon) {
- return geoPolygonIntersectsPolygon(polygon, extent, true);
- });
- }));
+ var matchImagery = (showsImagery && data.imagery.query.bbox(extent.rectangle(), true)) || [];
+ var features = matchImagery.map(function(d) { return data.imagery.features[d.id]; });
var imagery = layer.selectAll('path.debug-imagery')
- .data(showsImagery ? availableImagery : []);
+ .data(features);
imagery.exit()
.remove();
diff --git a/modules/svg/gpx.js b/modules/svg/gpx.js
deleted file mode 100644
index cfd785742..000000000
--- a/modules/svg/gpx.js
+++ /dev/null
@@ -1,266 +0,0 @@
-import _flatten from 'lodash-es/flatten';
-import _isEmpty from 'lodash-es/isEmpty';
-import _reduce from 'lodash-es/reduce';
-import _union from 'lodash-es/union';
-
-import { geoBounds as d3_geoBounds } from 'd3-geo';
-import { text as d3_text } from 'd3-request';
-import {
- event as d3_event,
- select as d3_select
-} from 'd3-selection';
-
-import { geoExtent, geoPolygonIntersectsPolygon } from '../geo';
-import { svgPath } from './index';
-import { utilDetect } from '../util/detect';
-import toGeoJSON from '@mapbox/togeojson';
-
-
-var _initialized = false;
-var _enabled = false;
-var _geojson;
-
-
-export function svgGpx(projection, context, dispatch) {
- var _showLabels = true;
- var detected = utilDetect();
- var layer;
- var _src;
-
-
- function init() {
- if (_initialized) return; // run once
-
- _geojson = {};
- _enabled = true;
-
- function over() {
- d3_event.stopPropagation();
- d3_event.preventDefault();
- d3_event.dataTransfer.dropEffect = 'copy';
- }
-
- d3_select('body')
- .attr('dropzone', 'copy')
- .on('drop.localgpx', function() {
- d3_event.stopPropagation();
- d3_event.preventDefault();
- if (!detected.filedrop) return;
- drawGpx.files(d3_event.dataTransfer.files);
- })
- .on('dragenter.localgpx', over)
- .on('dragexit.localgpx', over)
- .on('dragover.localgpx', over);
-
- _initialized = true;
- }
-
-
- function drawGpx(selection) {
- var getPath = svgPath(projection).geojson;
-
- layer = selection.selectAll('.layer-gpx')
- .data(_enabled ? [0] : []);
-
- layer.exit()
- .remove();
-
- layer = layer.enter()
- .append('g')
- .attr('class', 'layer-gpx')
- .merge(layer);
-
-
- var paths = layer
- .selectAll('path')
- .data([_geojson]);
-
- paths.exit()
- .remove();
-
- paths = paths.enter()
- .append('path')
- .attr('class', 'gpx')
- .merge(paths);
-
- paths
- .attr('d', getPath);
-
-
- var labelData = _showLabels && _geojson.features ? _geojson.features : [];
- labelData = labelData.filter(getPath);
-
- layer
- .call(drawLabels, 'gpxlabel-halo', labelData)
- .call(drawLabels, 'gpxlabel', labelData);
-
-
- function drawLabels(selection, textClass, data) {
- var labels = selection.selectAll('text.' + textClass)
- .data(data);
-
- // exit
- labels.exit()
- .remove();
-
- // enter/update
- labels = labels.enter()
- .append('text')
- .attr('class', textClass)
- .merge(labels)
- .text(function(d) {
- if (d.properties) {
- return d.properties.desc || d.properties.name;
- }
- return null;
- })
- .attr('x', function(d) {
- var centroid = getPath.centroid(d);
- return centroid[0] + 11;
- })
- .attr('y', function(d) {
- var centroid = getPath.centroid(d);
- return centroid[1];
- });
- }
- }
-
-
- function toDom(x) {
- return (new DOMParser()).parseFromString(x, 'text/xml');
- }
-
-
- function getExtension(fileName) {
- if (fileName === undefined) {
- return '';
- }
-
- var lastDotIndex = fileName.lastIndexOf('.');
- if (lastDotIndex < 0) {
- return '';
- }
-
- return fileName.substr(lastDotIndex);
- }
-
-
- function parseSaveAndZoom(extension, data, src) {
- switch (extension) {
- default:
- drawGpx.geojson(toGeoJSON.gpx(toDom(data)), src).fitZoom();
- break;
- case '.kml':
- drawGpx.geojson(toGeoJSON.kml(toDom(data)), src).fitZoom();
- break;
- case '.geojson':
- case '.json':
- drawGpx.geojson(JSON.parse(data), src).fitZoom();
- break;
- }
- }
-
-
- drawGpx.showLabels = function(_) {
- if (!arguments.length) return _showLabels;
- _showLabels = _;
- return this;
- };
-
-
- drawGpx.enabled = function(_) {
- if (!arguments.length) return _enabled;
- _enabled = _;
- dispatch.call('change');
- return this;
- };
-
-
- drawGpx.hasGpx = function() {
- return (!(_isEmpty(_geojson) || _isEmpty(_geojson.features)));
- };
-
-
- drawGpx.geojson = function(gj, src) {
- if (!arguments.length) return _geojson;
- if (_isEmpty(gj) || _isEmpty(gj.features)) return this;
- _geojson = gj;
- _src = src || 'unknown.geojson';
- dispatch.call('change');
- return this;
- };
-
-
- drawGpx.url = function(url) {
- d3_text(url, function(err, data) {
- if (!err) {
- var extension = getExtension(url);
- parseSaveAndZoom(extension, data, url);
- }
- });
- return this;
- };
-
-
- drawGpx.files = function(fileList) {
- if (!fileList.length) return this;
- var f = fileList[0];
- var reader = new FileReader();
-
- reader.onload = (function(file) {
- var extension = getExtension(file.name);
- return function (e) {
- parseSaveAndZoom(extension, e.target.result, file.name);
- };
- })(f);
-
- reader.readAsText(f);
- return this;
- };
-
-
- drawGpx.getSrc = function () {
- return _src;
- };
-
-
- drawGpx.fitZoom = function() {
- if (!this.hasGpx()) return this;
-
- var map = context.map();
- var viewport = map.trimmedExtent().polygon();
- var coords = _reduce(_geojson.features, function(coords, feature) {
- var c = feature.geometry.coordinates;
-
- /* eslint-disable no-fallthrough */
- switch (feature.geometry.type) {
- case 'Point':
- c = [c];
- case 'MultiPoint':
- case 'LineString':
- break;
-
- case 'MultiPolygon':
- c = _flatten(c);
- case 'Polygon':
- case 'MultiLineString':
- c = _flatten(c);
- break;
- }
- /* eslint-enable no-fallthrough */
-
- return _union(coords, c);
- }, []);
-
- if (!geoPolygonIntersectsPolygon(viewport, coords, true)) {
- var extent = geoExtent(d3_geoBounds({ type: 'LineString', coordinates: coords }));
- map.centerZoom(extent.center(), map.trimmedExtentZoom(extent));
- }
-
- return this;
- };
-
-
- init();
- return drawGpx;
-}
diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js
index fe57f3926..6c9bd344a 100644
--- a/modules/svg/helpers.js
+++ b/modules/svg/helpers.js
@@ -168,7 +168,17 @@ export function svgPath(projection, graph, isArea) {
}
};
- svgpath.geojson = path;
+ svgpath.geojson = function(d) {
+ if (d.__featurehash__ !== undefined) {
+ if (d.__featurehash__ in cache) {
+ return cache[d.__featurehash__];
+ } else {
+ return cache[d.__featurehash__] = path(d);
+ }
+ } else {
+ return path(d);
+ }
+ };
return svgpath;
}
diff --git a/modules/svg/index.js b/modules/svg/index.js
index c7760b2aa..4b1bd37a3 100644
--- a/modules/svg/index.js
+++ b/modules/svg/index.js
@@ -1,8 +1,7 @@
export { svgAreas } from './areas.js';
+export { svgData } from './data.js';
export { svgDebug } from './debug.js';
export { svgDefs } from './defs.js';
-export { svgGpx } from './gpx.js';
-export { svgMvt } from './mvt.js';
export { svgIcon } from './icon.js';
export { svgLabels } from './labels.js';
export { svgLayers } from './layers.js';
diff --git a/modules/svg/layers.js b/modules/svg/layers.js
index 31067aea2..7493cc695 100644
--- a/modules/svg/layers.js
+++ b/modules/svg/layers.js
@@ -7,10 +7,9 @@ import _reject from 'lodash-es/reject';
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { select as d3_select } from 'd3-selection';
+import { svgData } from './data';
import { svgDebug } from './debug';
-import { svgGpx } from './gpx';
import { svgStreetside } from './streetside';
-import { svgMvt } from './mvt';
import { svgMapillaryImages } from './mapillary_images';
import { svgMapillarySigns } from './mapillary_signs';
import { svgOpenstreetcamImages } from './openstreetcam_images';
@@ -26,8 +25,7 @@ export function svgLayers(projection, context) {
var layers = [
{ id: 'osm', layer: svgOsm(projection, context, dispatch) },
{ id: 'notes', layer: svgNotes(projection, context, dispatch) },
- { id: 'gpx', layer: svgGpx(projection, context, dispatch) },
- { id: 'mvt', layer: svgMvt(projection, context, dispatch) },
+ { id: 'data', layer: svgData(projection, context, dispatch) },
{ id: 'streetside', layer: svgStreetside(projection, context, dispatch)},
{ id: 'mapillary-images', layer: svgMapillaryImages(projection, context, dispatch) },
{ id: 'mapillary-signs', layer: svgMapillarySigns(projection, context, dispatch) },
diff --git a/modules/svg/mapillary_images.js b/modules/svg/mapillary_images.js
index fb8a4b4b8..641882790 100644
--- a/modules/svg/mapillary_images.js
+++ b/modules/svg/mapillary_images.js
@@ -1,4 +1,5 @@
import _throttle from 'lodash-es/throttle';
+import _isNumber from 'lodash-es/isNumber';
import { select as d3_select } from 'd3-selection';
import { svgPath, svgPointTransform } from './index';
import { services } from '../services';
@@ -11,6 +12,7 @@ export function svgMapillaryImages(projection, context, dispatch) {
var minViewfieldZoom = 18;
var layer = d3_select(null);
var _mapillary;
+ var viewerCompassAngle;
function init() {
@@ -24,6 +26,19 @@ export function svgMapillaryImages(projection, context, dispatch) {
if (services.mapillary && !_mapillary) {
_mapillary = services.mapillary;
_mapillary.event.on('loadedImages', throttledRedraw);
+ _mapillary.event.on('bearingChanged', function(e) {
+ viewerCompassAngle = e;
+
+ // avoid updating if the map is currently transformed
+ // e.g. during drags or easing.
+ if (context.map().isTransformed()) return;
+
+ layer.selectAll('.viewfield-group.selected')
+ .filter(function(d) {
+ return d.pano;
+ })
+ .attr('transform', transform);
+ });
} else if (!services.mapillary && _mapillary) {
_mapillary = null;
}
@@ -102,7 +117,9 @@ export function svgMapillaryImages(projection, context, dispatch) {
function transform(d) {
var t = svgPointTransform(projection)(d);
- if (d.ca) {
+ if (d.pano && _isNumber(viewerCompassAngle)) {
+ t += ' rotate(' + Math.floor(viewerCompassAngle) + ',0,0)';
+ } else if (d.ca) {
t += ' rotate(' + Math.floor(d.ca) + ',0,0)';
}
return t;
@@ -184,6 +201,7 @@ export function svgMapillaryImages(projection, context, dispatch) {
viewfields.enter() // viewfields may or may not be drawn...
.insert('path', 'circle') // but if they are, draw below the circles
.attr('class', 'viewfield')
+ .classed('pano', function() { return this.parentNode.__data__.pano; })
.attr('transform', 'scale(1.5,1.5),translate(-8, -13)')
.attr('d', viewfieldPath);
diff --git a/modules/svg/mvt.js b/modules/svg/mvt.js
deleted file mode 100644
index 7eab31335..000000000
--- a/modules/svg/mvt.js
+++ /dev/null
@@ -1,301 +0,0 @@
-import _flatten from 'lodash-es/flatten';
-import _isEmpty from 'lodash-es/isEmpty';
-import _reduce from 'lodash-es/reduce';
-import _union from 'lodash-es/union';
-
-import { geoBounds as d3_geoBounds } from 'd3-geo';
-import { request as d3_request } from 'd3-request';
-
-import {
- event as d3_event,
- select as d3_select
-} from 'd3-selection';
-
-import vt from '@mapbox/vector-tile';
-import Protobuf from 'pbf';
-
-import { geoExtent, geoPolygonIntersectsPolygon } from '../geo';
-import { svgPath } from './index';
-import { utilDetect } from '../util/detect';
-
-
-var _initialized = false;
-var _enabled = false;
-var _geojson;
-
-
-export function svgMvt(projection, context, dispatch) {
- var _showLabels = true;
- var detected = utilDetect();
- var layer;
- var _src;
-
-
- function init() {
- if (_initialized) return; // run once
-
- _geojson = {};
- _enabled = true;
-
- function over() {
- d3_event.stopPropagation();
- d3_event.preventDefault();
- d3_event.dataTransfer.dropEffect = 'copy';
- }
-
- d3_select('body')
- .attr('dropzone', 'copy')
- .on('drop.localmvt', function() {
- d3_event.stopPropagation();
- d3_event.preventDefault();
- if (!detected.filedrop) return;
- drawMvt.files(d3_event.dataTransfer.files);
- })
- .on('dragenter.localmvt', over)
- .on('dragexit.localmvt', over)
- .on('dragover.localmvt', over);
-
- _initialized = true;
- }
-
-
- function drawMvt(selection) {
- var getPath = svgPath(projection).geojson;
-
- layer = selection.selectAll('.layer-mvt')
- .data(_enabled ? [0] : []);
-
- layer.exit()
- .remove();
-
- layer = layer.enter()
- .append('g')
- .attr('class', 'layer-mvt')
- .merge(layer);
-
-
- var paths = layer
- .selectAll('path')
- .data([_geojson]);
-
- paths.exit()
- .remove();
-
- paths = paths.enter()
- .append('path')
- .attr('class', 'mvt')
- .merge(paths);
-
- paths
- .attr('d', getPath);
-
-
- var labelData = _showLabels && _geojson.features ? _geojson.features : [];
- labelData = labelData.filter(getPath);
-
- layer
- .call(drawLabels, 'mvtlabel-halo', labelData)
- .call(drawLabels, 'mvtlabel', labelData);
-
-
- function drawLabels(selection, textClass, data) {
- var labels = selection.selectAll('text.' + textClass)
- .data(data);
-
- // exit
- labels.exit()
- .remove();
-
- // enter/update
- labels = labels.enter()
- .append('text')
- .attr('class', textClass)
- .merge(labels)
- .text(function(d) {
- if (d.properties) {
- return d.properties.desc || d.properties.name;
- }
- return null;
- })
- .attr('x', function(d) {
- var centroid = getPath.centroid(d);
- return centroid[0] + 11;
- })
- .attr('y', function(d) {
- var centroid = getPath.centroid(d);
- return centroid[1];
- });
- }
- }
-
-
- function vtToGeoJson(bufferdata) {
- var tile = new vt.VectorTile(new Protobuf(bufferdata.data));
- var layers = Object.keys(tile.layers);
- if (!Array.isArray(layers)) { layers = [layers]; }
-
- var collection = {type: 'FeatureCollection', features: []};
-
- layers.forEach(function (layerID) {
- var layer = tile.layers[layerID];
- if (layer) {
- for (var i = 0; i < layer.length; i++) {
- var feature = layer.feature(i).toGeoJSON(bufferdata.zxy[2], bufferdata.zxy[3], bufferdata.zxy[1]);
- if (layers.length > 1) feature.properties.vt_layer = layerID;
- collection.features.push(feature);
- }
- }
- });
- return collection;
- }
-
-
- function getExtension(fileName) {
- if (fileName === undefined) {
- return '';
- }
-
- var lastDotIndex = fileName.lastIndexOf('.');
- if (lastDotIndex < 0) {
- return '';
- }
-
- return fileName.substr(lastDotIndex);
- }
-
-
- function parseSaveAndZoom(extension, bufferdata) {
- switch (extension) {
- case '.pbf':
- drawMvt.geojson(vtToGeoJson(bufferdata)).fitZoom();
- break;
- case '.mvt':
- drawMvt.geojson(vtToGeoJson(bufferdata)).fitZoom();
- break;
- }
- }
-
-
- drawMvt.showLabels = function(_) {
- if (!arguments.length) return _showLabels;
- _showLabels = _;
- return this;
- };
-
-
- drawMvt.enabled = function(_) {
- if (!arguments.length) return _enabled;
- _enabled = _;
- dispatch.call('change');
- return this;
- };
-
-
- drawMvt.hasMvt = function() {
- return (!(_isEmpty(_geojson) || _isEmpty(_geojson.features)));
- };
-
-
- drawMvt.geojson = function(gj) {
- if (!arguments.length) return _geojson;
- if (_isEmpty(gj) || _isEmpty(gj.features)) return this;
- _geojson = gj;
- dispatch.call('change');
- return this;
- };
-
-
- drawMvt.url = function(url) {
- d3_request(url)
- .responseType('arraybuffer')
- .get(function(err, data) {
- if (err || !data) return;
-
- _src = url;
- var match = url.match(/(pbf|mvt)/i);
- var extension = match ? ('.' + match[0].toLowerCase()) : '';
- var zxy = url.match(/\/(\d+)\/(\d+)\/(\d+)/);
- var bufferdata = {
- data : data,
- zxy : zxy
- };
- parseSaveAndZoom(extension, bufferdata);
- });
-
- return this;
- };
-
-
- drawMvt.files = function(fileList) {
- if (!fileList.length) return this;
- var f = fileList[0],
- reader = new FileReader();
-
- reader.onload = (function(file) {
-
-return; // todo find x,y,z
-var data = [];
-var zxy = [0,0,0];
-
- _src = file.name;
- var extension = getExtension(file.name);
- var bufferdata = {
- data: data,
- zxy: zxy
- };
- return function (e) {
- bufferdata.data = e.target.result;
- parseSaveAndZoom(extension, bufferdata);
- };
- })(f);
-
- reader.readAsArrayBuffer(f);
- return this;
- };
-
-
- drawMvt.getSrc = function () {
- return _src;
- };
-
-
- drawMvt.fitZoom = function() {
- if (!this.hasMvt()) return this;
-
- var map = context.map();
- var viewport = map.trimmedExtent().polygon();
- var coords = _reduce(_geojson.features, function(coords, feature) {
- var c = feature.geometry.coordinates;
-
- /* eslint-disable no-fallthrough */
- switch (feature.geometry.type) {
- case 'Point':
- c = [c];
- case 'MultiPoint':
- case 'LineString':
- break;
-
- case 'MultiPolygon':
- c = _flatten(c);
- case 'Polygon':
- case 'MultiLineString':
- c = _flatten(c);
- break;
- }
- /* eslint-enable no-fallthrough */
-
- return _union(coords, c);
- }, []);
-
- if (!geoPolygonIntersectsPolygon(viewport, coords, true)) {
- var extent = geoExtent(d3_geoBounds({ type: 'LineString', coordinates: coords }));
- map.centerZoom(extent.center(), map.trimmedExtentZoom(extent));
- }
-
- return this;
- };
-
-
- init();
- return drawMvt;
-}
diff --git a/modules/svg/notes.js b/modules/svg/notes.js
index b416e8ee4..bc97adb55 100644
--- a/modules/svg/notes.js
+++ b/modules/svg/notes.js
@@ -1,17 +1,26 @@
import _throttle from 'lodash-es/throttle';
import { select as d3_select } from 'd3-selection';
+import { dispatch as d3_dispatch } from 'd3-dispatch';
+import { modeBrowse } from '../modes';
import { svgPointTransform } from './index';
import { services } from '../services';
export function svgNotes(projection, context, dispatch) {
+ if (!dispatch) { dispatch = d3_dispatch('change'); }
var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
var minZoom = 12;
var layer = d3_select(null);
var _notes;
+ function markerPath(selection, klass) {
+ selection
+ .attr('class', klass)
+ .attr('transform', 'translate(-8, -22)')
+ .attr('d', 'm17.5,0l-15,0c-1.37,0 -2.5,1.12 -2.5,2.5l0,11.25c0,1.37 1.12,2.5 2.5,2.5l3.75,0l0,3.28c0,0.38 0.43,0.6 0.75,0.37l4.87,-3.65l5.62,0c1.37,0 2.5,-1.12 2.5,-2.5l0,-11.25c0,-1.37 -1.12,-2.5 -2.5,-2.5z');
+ }
function init() {
if (svgNotes.initialized) return; // run once
@@ -46,22 +55,32 @@ export function svgNotes(projection, context, dispatch) {
editOn();
layer
+ .classed('disabled', false)
.style('opacity', 0)
.transition()
.duration(250)
.style('opacity', 1)
- .on('end', function () { dispatch.call('change'); });
+ .on('end interrupt', function () {
+ dispatch.call('change');
+ });
}
function hideLayer() {
+ editOff();
+
throttledRedraw.cancel();
+ layer.interrupt();
layer
.transition()
.duration(250)
.style('opacity', 0)
- .on('end', editOff);
+ .on('end interrupt', function () {
+ layer.classed('disabled', true);
+ dispatch.call('change');
+ });
+
}
@@ -80,37 +99,42 @@ export function svgNotes(projection, context, dispatch) {
// enter
var notesEnter = notes.enter()
.append('g')
- .attr('class', function(d) { return 'note note-' + d.id + ' ' + d.status; });
+ .attr('class', function(d) { return 'note note-' + d.id + ' ' + d.status; })
+ .classed('new', function(d) { return d.id < 0; });
- // notesEnter
- // .append('use')
- // .attr('class', 'note-shadow')
- // .attr('width', '24px')
- // .attr('height', '24px')
- // .attr('x', '-12px')
- // .attr('y', '-24px')
- // .attr('xlink:href', '#iD-icon-note');
+ notesEnter
+ .append('ellipse')
+ .attr('cx', 0.5)
+ .attr('cy', 1)
+ .attr('rx', 6.5)
+ .attr('ry', 3)
+ .attr('class', 'stroke');
+
+ notesEnter
+ .append('path')
+ .call(markerPath, 'shadow');
notesEnter
.append('use')
.attr('class', 'note-fill')
.attr('width', '20px')
.attr('height', '20px')
- .attr('x', '-10px')
+ .attr('x', '-8px')
.attr('y', '-22px')
.attr('xlink:href', '#iD-icon-note');
- // add dots if there's a comment thread
notesEnter.selectAll('.note-annotation')
- .data(function(d) { return d.comments.length > 1 ? [0] : []; })
+ .data(function(d) { return [d]; })
.enter()
.append('use')
- .attr('class', 'note-annotation thread')
- .attr('width', '14px')
- .attr('height', '14px')
- .attr('x', '-7px')
- .attr('y', '-20px')
- .attr('xlink:href', '#iD-icon-more');
+ .attr('class', 'note-annotation')
+ .attr('width', '10px')
+ .attr('height', '10px')
+ .attr('x', '-3px')
+ .attr('y', '-19px')
+ .attr('xlink:href', function(d) {
+ return '#iD-icon-' + (d.id < 0 ? 'plus' : (d.status === 'open' ? 'close' : 'apply'));
+ });
// update
notes
@@ -156,14 +180,19 @@ export function svgNotes(projection, context, dispatch) {
}
}
- drawNotes.enabled = function(_) {
+ drawNotes.enabled = function(val) {
if (!arguments.length) return svgNotes.enabled;
- svgNotes.enabled = _;
+
+ svgNotes.enabled = val;
if (svgNotes.enabled) {
showLayer();
} else {
hideLayer();
+ if (context.selectedNoteID()) {
+ context.enter(modeBrowse(context));
+ }
}
+
dispatch.call('change');
return this;
};
diff --git a/modules/ui/background.js b/modules/ui/background.js
index 4dac81036..3dbc1c454 100644
--- a/modules/ui/background.js
+++ b/modules/ui/background.js
@@ -21,6 +21,7 @@ import { uiDisclosure } from './disclosure';
import { uiHelp } from './help';
import { uiMapData } from './map_data';
import { uiMapInMap } from './map_in_map';
+import { uiSettingsCustomBackground } from './settings/custom_background';
import { uiTooltipHtml } from './tooltipHtml';
import { utilCallWhenIdle } from '../util';
import { tooltip } from '../util/tooltip';
@@ -41,6 +42,9 @@ export function uiBackground(context) {
var backgroundDisplayOptions = uiBackgroundDisplayOptions(context);
var backgroundOffset = uiBackgroundOffset(context);
+ var settingsCustomBackground = uiSettingsCustomBackground(context)
+ .on('change', customChanged);
+
function setTooltips(selection) {
selection.each(function(d, i, nodes) {
@@ -100,24 +104,24 @@ export function uiBackground(context) {
}
- function editCustom() {
- d3_event.preventDefault();
- var example = 'https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png';
- var template = window.prompt(
- t('background.custom_prompt', { example: example }),
- _customSource.template() || example
- );
-
- if (template) {
- context.storage('background-custom-template', template);
- _customSource.template(template);
+ function customChanged(d) {
+ if (d && d.template) {
+ _customSource.template(d.template);
chooseBackground(_customSource);
} else {
- _backgroundList.call(updateLayerSelections);
+ _customSource.template('');
+ chooseBackground(context.background().findSource('none'));
}
}
+ function editCustom() {
+ d3_event.preventDefault();
+ context.container()
+ .call(settingsCustomBackground);
+ }
+
+
function chooseOverlay(d) {
d3_event.preventDefault();
context.background().toggleOverlayLayer(d);
@@ -147,11 +151,11 @@ export function uiBackground(context) {
.append('button')
.attr('class', 'layer-browse')
.call(tooltip()
- .title(t('background.custom_button'))
+ .title(t('settings.custom_background.tooltip'))
.placement((textDirection === 'rtl') ? 'right' : 'left')
)
.on('click', editCustom)
- .call(svgIcon('#iD-icon-edit'));
+ .call(svgIcon('#iD-icon-more'));
enter.filter(function(d) { return d.best(); })
.append('div')
diff --git a/modules/ui/confirm.js b/modules/ui/confirm.js
index 2fd0e4383..8eec4b6cd 100644
--- a/modules/ui/confirm.js
+++ b/modules/ui/confirm.js
@@ -23,7 +23,7 @@ export function uiConfirm(selection) {
modalSelection.okButton = function() {
buttons
.append('button')
- .attr('class', 'action col4')
+ .attr('class', 'button ok-button action col4')
.on('click.confirm', function() {
modalSelection.remove();
})
diff --git a/modules/ui/data_editor.js b/modules/ui/data_editor.js
new file mode 100644
index 000000000..593517de6
--- /dev/null
+++ b/modules/ui/data_editor.js
@@ -0,0 +1,79 @@
+import { t } from '../util/locale';
+import { modeBrowse } from '../modes';
+import { svgIcon } from '../svg';
+
+import {
+ uiDataHeader,
+ uiRawTagEditor
+} from './index';
+
+
+export function uiDataEditor(context) {
+ var dataHeader = uiDataHeader();
+ var rawTagEditor = uiRawTagEditor(context);
+ var _datum;
+
+
+ function dataEditor(selection) {
+ var header = selection.selectAll('.header')
+ .data([0]);
+
+ var headerEnter = header.enter()
+ .append('div')
+ .attr('class', 'header fillL');
+
+ headerEnter
+ .append('button')
+ .attr('class', 'fr data-editor-close')
+ .on('click', function() {
+ context.enter(modeBrowse(context));
+ })
+ .call(svgIcon('#iD-icon-close'));
+
+ headerEnter
+ .append('h3')
+ .text(t('map_data.title'));
+
+
+ var body = selection.selectAll('.body')
+ .data([0]);
+
+ body = body.enter()
+ .append('div')
+ .attr('class', 'body')
+ .merge(body);
+
+ var editor = body.selectAll('.data-editor')
+ .data([0]);
+
+ editor.enter()
+ .append('div')
+ .attr('class', 'modal-section data-editor')
+ .merge(editor)
+ .call(dataHeader.datum(_datum));
+
+ var rte = body.selectAll('.raw-tag-editor')
+ .data([0]);
+
+ rte.enter()
+ .append('div')
+ .attr('class', 'inspector-border raw-tag-editor inspector-inner data-editor')
+ .merge(rte)
+ .call(rawTagEditor
+ .expanded(true)
+ .readOnlyTags([/./])
+ .tags((_datum && _datum.properties) || {})
+ .state('hover')
+ );
+ }
+
+
+ dataEditor.datum = function(val) {
+ if (!arguments.length) return _datum;
+ _datum = val;
+ return this;
+ };
+
+
+ return dataEditor;
+}
diff --git a/modules/ui/data_header.js b/modules/ui/data_header.js
new file mode 100644
index 000000000..fec0026af
--- /dev/null
+++ b/modules/ui/data_header.js
@@ -0,0 +1,47 @@
+import { t } from '../util/locale';
+import { svgIcon } from '../svg';
+
+
+export function uiDataHeader() {
+ var _datum;
+
+
+ function dataHeader(selection) {
+ var header = selection.selectAll('.data-header')
+ .data(
+ (_datum ? [_datum] : []),
+ function(d) { return d.__featurehash__; }
+ );
+
+ header.exit()
+ .remove();
+
+ var headerEnter = header.enter()
+ .append('div')
+ .attr('class', 'data-header');
+
+ var iconEnter = headerEnter
+ .append('div')
+ .attr('class', 'data-header-icon');
+
+ iconEnter
+ .append('div')
+ .attr('class', 'preset-icon-28')
+ .call(svgIcon('#iD-icon-data', 'note-fill'));
+
+ headerEnter
+ .append('div')
+ .attr('class', 'data-header-label')
+ .text(t('map_data.layers.custom.title'));
+ }
+
+
+ dataHeader.datum = function(val) {
+ if (!arguments.length) return _datum;
+ _datum = val;
+ return this;
+ };
+
+
+ return dataHeader;
+}
diff --git a/modules/ui/field.js b/modules/ui/field.js
index a430c2f97..585e38bb8 100644
--- a/modules/ui/field.js
+++ b/modules/ui/field.js
@@ -34,12 +34,15 @@ export function uiField(context, presetField, entity, options) {
var _tags = {};
+ // field implementation
field.impl = uiFields[field.type](field, context)
.on('change', function(t, onInput) {
dispatch.call('change', field, t, onInput);
});
+ // if this field cares about the entity, pass it along
if (entity && field.impl.entity) {
+ field.entityID = entity.id;
field.impl.entity(entity);
}
diff --git a/modules/ui/fields/access.js b/modules/ui/fields/access.js
index 11fa0c29d..4df935d45 100644
--- a/modules/ui/fields/access.js
+++ b/modules/ui/fields/access.js
@@ -81,7 +81,7 @@ export function uiFieldAccess(field, context) {
access.options = function(type) {
- var options = ['no', 'permissive', 'private', 'destination'];
+ var options = ['no', 'permissive', 'private', 'permit', 'destination'];
if (type !== 'access') {
options.unshift('yes');
diff --git a/modules/ui/fields/combo.js b/modules/ui/fields/combo.js
index 3da0e37ad..1df90468b 100644
--- a/modules/ui/fields/combo.js
+++ b/modules/ui/fields/combo.js
@@ -8,11 +8,17 @@ import _some from 'lodash-es/some';
import _uniq from 'lodash-es/uniq';
import { dispatch as d3_dispatch } from 'd3-dispatch';
-import { event as d3_event } from 'd3-selection';
+
+import {
+ event as d3_event,
+ select as d3_select
+} from 'd3-selection';
+
import { d3combobox as d3_combobox } from '../../lib/d3.combobox.js';
import { t } from '../../util/locale';
import { services } from '../../services';
+
import {
utilGetSetValue,
utilNoAuto,
@@ -28,29 +34,29 @@ export {
export function uiFieldCombo(field, context) {
- var dispatch = d3_dispatch('change'),
- nominatim = services.geocoder,
- taginfo = services.taginfo,
- isMulti = (field.type === 'multiCombo'),
- isNetwork = (field.type === 'networkCombo'),
- isSemi = (field.type === 'semiCombo'),
- optstrings = field.strings && field.strings.options,
- optarray = field.options,
- snake_case = (field.snake_case || (field.snake_case === undefined)),
- caseSensitive = field.caseSensitive,
- combobox = d3_combobox()
- .container(context.container())
- .caseSensitive(caseSensitive)
- .minItems(isMulti || isSemi ? 1 : 2),
- comboData = [],
- multiData = [],
- container,
- input,
- entity,
- country;
+ var dispatch = d3_dispatch('change');
+ var nominatim = services.geocoder;
+ var taginfo = services.taginfo;
+ var isMulti = (field.type === 'multiCombo');
+ var isNetwork = (field.type === 'networkCombo');
+ var isSemi = (field.type === 'semiCombo');
+ var optstrings = field.strings && field.strings.options;
+ var optarray = field.options;
+ var snake_case = (field.snake_case || (field.snake_case === undefined));
+ var caseSensitive = field.caseSensitive;
+ var combobox = d3_combobox()
+ .container(context.container())
+ .caseSensitive(caseSensitive)
+ .minItems(isMulti || isSemi ? 1 : 2);
+ var container = d3_select(null);
+ var input = d3_select(null);
+ var _comboData = [];
+ var _multiData = [];
+ var _entity;
+ var _country;
// ensure multiCombo field.key ends with a ':'
- if (isMulti && field.key.match(/:$/) === null) {
+ if (isMulti && /[^:]$/.test(field.key)) {
field.key += ':';
}
@@ -76,11 +82,11 @@ export function uiFieldCombo(field, context) {
dval = clean(dval || '');
if (optstrings) {
- var match = _find(comboData, function(o) {
+ var found = _find(_comboData, function(o) {
return o.key && clean(o.value) === dval;
});
- if (match) {
- return match.key;
+ if (found) {
+ return found.key;
}
}
@@ -98,9 +104,9 @@ export function uiFieldCombo(field, context) {
tval = tval || '';
if (optstrings) {
- var match = _find(comboData, function(o) { return o.key === tval && o.value; });
- if (match) {
- return match.value;
+ var found = _find(_comboData, function(o) { return o.key === tval && o.value; });
+ if (found) {
+ return found.value;
}
}
@@ -140,7 +146,7 @@ export function uiFieldCombo(field, context) {
if (!(optstrings || optarray)) return;
if (optstrings) {
- comboData = Object.keys(optstrings).map(function(k) {
+ _comboData = Object.keys(optstrings).map(function(k) {
var v = field.t('options.' + k, { 'default': optstrings[k] });
return {
key: k,
@@ -150,7 +156,7 @@ export function uiFieldCombo(field, context) {
});
} else if (optarray) {
- comboData = optarray.map(function(k) {
+ _comboData = optarray.map(function(k) {
var v = snake_case ? unsnake(k) : k;
return {
key: k,
@@ -160,17 +166,17 @@ export function uiFieldCombo(field, context) {
});
}
- combobox.data(objectDifference(comboData, multiData));
- if (callback) callback(comboData);
+ combobox.data(objectDifference(_comboData, _multiData));
+ if (callback) callback(_comboData);
}
function setTaginfoValues(q, callback) {
var fn = isMulti ? 'multikeys' : 'values';
var query = (isMulti ? field.key : '') + q;
- var hasCountryPrefix = isNetwork && country && country.indexOf(q.toLowerCase()) === 0;
+ var hasCountryPrefix = isNetwork && _country && _country.indexOf(q.toLowerCase()) === 0;
if (hasCountryPrefix) {
- query = country + ':';
+ query = _country + ':';
}
var params = {
@@ -179,19 +185,19 @@ export function uiFieldCombo(field, context) {
query: query
};
- if (entity) {
- params.geometry = context.geometry(entity.id);
+ if (_entity) {
+ params.geometry = context.geometry(_entity.id);
}
taginfo[fn](params, function(err, data) {
if (err) return;
if (hasCountryPrefix) {
data = _filter(data, function(d) {
- return d.value.toLowerCase().indexOf(country + ':') === 0;
+ return d.value.toLowerCase().indexOf(_country + ':') === 0;
});
}
- comboData = _map(data, function(d) {
+ _comboData = _map(data, function(d) {
var k = d.value;
if (isMulti) k = k.replace(field.key, '');
var v = snake_case ? unsnake(k) : k;
@@ -202,8 +208,8 @@ export function uiFieldCombo(field, context) {
};
});
- comboData = objectDifference(comboData, multiData);
- if (callback) callback(comboData);
+ _comboData = objectDifference(_comboData, _multiData);
+ if (callback) callback(_comboData);
});
}
@@ -219,7 +225,7 @@ export function uiFieldCombo(field, context) {
ph = field.placeholder() || placeholders.slice(0, 3).join(', ');
}
- if (ph.match(/(…|\.\.\.)$/) === null) {
+ if (!/(…|\.\.\.)$/.test(ph)) {
ph += '…';
}
@@ -229,21 +235,31 @@ export function uiFieldCombo(field, context) {
function change() {
- var val = tagValue(utilGetSetValue(input)),
- t = {};
+ var val = tagValue(utilGetSetValue(input));
+ var t = {};
if (isMulti || isSemi) {
if (!val) return;
container.classed('active', false);
utilGetSetValue(input, '');
+
if (isMulti) {
- field.keys.push(field.key + val);
- t[field.key + val] = 'yes';
+ var key = field.key + val;
+ if (_entity) {
+ // don't set a multicombo value to 'yes' if it already has a non-'no' value
+ // e.g. `language:de=main`
+ var old = _entity.tags[key] || '';
+ if (old && old.toLowerCase() !== 'no') return;
+ }
+ field.keys.push(key);
+ t[key] = 'yes';
+
} else if (isSemi) {
- var arr = multiData.map(function(d) { return d.key; });
+ var arr = _multiData.map(function(d) { return d.key; });
arr.push(val);
t[field.key] = _compact(_uniq(arr)).join(';');
}
+
window.setTimeout(function() { input.node().focus(); }, 10);
} else {
@@ -260,8 +276,8 @@ export function uiFieldCombo(field, context) {
if (isMulti) {
t[d.key] = undefined;
} else if (isSemi) {
- _remove(multiData, function(md) { return md.key === d.key; });
- var arr = multiData.map(function(md) { return md.key; });
+ _remove(_multiData, function(md) { return md.key === d.key; });
+ var arr = _multiData.map(function(md) { return md.key; });
arr = _compact(_uniq(arr));
t[field.key] = arr.length ? arr.join(';') : undefined;
}
@@ -296,10 +312,10 @@ export function uiFieldCombo(field, context) {
.call(initCombo, selection)
.merge(input);
- if (isNetwork && nominatim && entity) {
- var center = entity.extent(context.graph()).center();
+ if (isNetwork && nominatim && _entity) {
+ var center = _entity.extent(context.graph()).center();
nominatim.countryCode(center, function (err, code) {
- country = code;
+ _country = code;
});
}
@@ -322,35 +338,37 @@ export function uiFieldCombo(field, context) {
combo.tags = function(tags) {
if (isMulti || isSemi) {
- multiData = [];
+ _multiData = [];
if (isMulti) {
- // Build multiData array containing keys already set..
- Object.keys(tags).forEach(function(key) {
- if (key.indexOf(field.key) !== 0 || tags[key].toLowerCase() !== 'yes') return;
+ // Build _multiData array containing keys already set..
+ for (var k in tags) {
+ if (k.indexOf(field.key) !== 0) continue;
+ var v = (tags[k] || '').toLowerCase();
+ if (v === '' || v === 'no') continue;
- var suffix = key.substring(field.key.length);
- multiData.push({
- key: key,
+ var suffix = k.substring(field.key.length);
+ _multiData.push({
+ key: k,
value: displayValue(suffix)
});
- });
+ }
// Set keys for form-field modified (needed for undo and reset buttons)..
- field.keys = _map(multiData, 'key');
+ field.keys = _map(_multiData, 'key');
} else if (isSemi) {
var arr = _compact(_uniq((tags[field.key] || '').split(';')));
- multiData = arr.map(function(key) {
+ _multiData = arr.map(function(k) {
return {
- key: key,
- value: displayValue(key)
+ key: k,
+ value: displayValue(k)
};
});
}
// Exclude existing multikeys from combo options..
- var available = objectDifference(comboData, multiData);
+ var available = objectDifference(_comboData, _multiData);
combobox.data(available);
// Hide 'Add' button if this field uses fixed set of
@@ -361,7 +379,7 @@ export function uiFieldCombo(field, context) {
// Render chips
var chips = container.selectAll('.chips')
- .data(multiData);
+ .data(_multiData);
chips.exit()
.remove();
@@ -394,9 +412,9 @@ export function uiFieldCombo(field, context) {
};
- combo.entity = function(_) {
- if (!arguments.length) return entity;
- entity = _;
+ combo.entity = function(val) {
+ if (!arguments.length) return _entity;
+ _entity = val;
return combo;
};
diff --git a/modules/ui/fields/input.js b/modules/ui/fields/input.js
index 73023d891..b8c98df56 100644
--- a/modules/ui/fields/input.js
+++ b/modules/ui/fields/input.js
@@ -92,7 +92,7 @@ export function uiFieldText(field, context) {
// parse as a number
function parsed(val) {
- return parseInt(val || 0, 10) || 0;
+ return parseFloat(val || 0, 10) || 0;
}
// clamp number to min/max
diff --git a/modules/ui/form_fields.js b/modules/ui/form_fields.js
index e2c6de218..0bf150b22 100644
--- a/modules/ui/form_fields.js
+++ b/modules/ui/form_fields.js
@@ -6,7 +6,7 @@ import { utilGetSetValue, utilNoAuto } from '../util';
export function uiFormFields(context) {
- var fieldsArr;
+ var _fieldsArr;
function formFields(selection, klass) {
@@ -15,9 +15,8 @@ export function uiFormFields(context) {
function render(selection, klass) {
-
- var shown = fieldsArr.filter(function(field) { return field.isShown(); }),
- notShown = fieldsArr.filter(function(field) { return !field.isShown(); });
+ var shown = _fieldsArr.filter(function(field) { return field.isShown(); });
+ var notShown = _fieldsArr.filter(function(field) { return !field.isShown(); });
var container = selection.selectAll('.form-fields-container')
.data([0]);
@@ -29,7 +28,7 @@ export function uiFormFields(context) {
var fields = container.selectAll('.wrap-form-field')
- .data(shown, function(d) { return d.id; });
+ .data(shown, function(d) { return d.id + (d.entityID || ''); });
fields.exit()
.remove();
@@ -112,9 +111,9 @@ export function uiFormFields(context) {
}
- formFields.fieldsArr = function(_) {
- if (!arguments.length) return fieldsArr;
- fieldsArr = _;
+ formFields.fieldsArr = function(val) {
+ if (!arguments.length) return _fieldsArr;
+ _fieldsArr = val;
return formFields;
};
diff --git a/modules/ui/help.js b/modules/ui/help.js
index 5b5c71993..29942ca2a 100644
--- a/modules/ui/help.js
+++ b/modules/ui/help.js
@@ -147,6 +147,17 @@ export function uiHelp(context) {
'boundary',
'boundary_add'
]],
+ ['notes', [
+ 'intro',
+ 'add_note_h',
+ 'add_note',
+ 'move_note',
+ 'update_note_h',
+ 'update_note',
+ 'save_note_h',
+ 'save_note'
+ ]],
+
['imagery', [
'intro',
'sources_h',
@@ -210,6 +221,9 @@ export function uiHelp(context) {
'help.relations.turn_restriction_h': 3,
'help.relations.route_h': 3,
'help.relations.boundary_h': 3,
+ 'help.notes.add_note_h': 3,
+ 'help.notes.update_note_h': 3,
+ 'help.notes.save_note_h': 3,
'help.imagery.sources_h': 3,
'help.imagery.offsets_h': 3,
'help.streetlevel.using_h': 3,
@@ -220,6 +234,7 @@ export function uiHelp(context) {
point: icon('#iD-icon-point', 'pre-text'),
line: icon('#iD-icon-line', 'pre-text'),
area: icon('#iD-icon-area', 'pre-text'),
+ note: icon('#iD-icon-note', 'pre-text add-note'),
plus: icon('#iD-icon-plus', 'pre-text'),
minus: icon('#iD-icon-minus', 'pre-text'),
orthogonalize: icon('#iD-operation-orthogonalize', 'pre-text'),
diff --git a/modules/ui/index.js b/modules/ui/index.js
index 35d9a8517..3c2641519 100644
--- a/modules/ui/index.js
+++ b/modules/ui/index.js
@@ -13,6 +13,8 @@ export { uiConfirm } from './confirm';
export { uiConflicts } from './conflicts';
export { uiContributors } from './contributors';
export { uiCurtain } from './curtain';
+export { uiDataEditor } from './data_editor';
+export { uiDataHeader } from './data_header';
export { uiDisclosure } from './disclosure';
export { uiEditMenu } from './edit_menu';
export { uiEntityEditor } from './entity_editor';
diff --git a/modules/ui/init.js b/modules/ui/init.js
index 7e1d134fc..4059f9420 100644
--- a/modules/ui/init.js
+++ b/modules/ui/init.js
@@ -2,6 +2,7 @@ import {
event as d3_event,
select as d3_select
} from 'd3-selection';
+import { dispatch as d3_dispatch } from 'd3-dispatch';
import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js';
@@ -13,6 +14,7 @@ import { modeBrowse } from '../modes';
import { services } from '../services';
import { svgDefs, svgIcon } from '../svg';
import { utilGetDimensions } from '../util/dimensions';
+import { utilRebind } from '../util';
import { uiAccount } from './account';
import { uiAttribution } from './attribution';
@@ -45,6 +47,7 @@ import { uiCmd } from './cmd';
export function uiInit(context) {
var uiInitCounter = 0;
+ var dispatch = d3_dispatch('photoviewerResize');
function render(container) {
@@ -256,7 +259,33 @@ export function uiInit(context) {
.append('div')
.call(svgIcon('#iD-icon-close'));
+ photoviewer
+ .append('button')
+ .attr('class', 'resize-handle-xy')
+ .on(
+ 'mousedown',
+ buildResizeListener(photoviewer, 'photoviewerResize', dispatch, { resizeOnX: true, resizeOnY: true })
+ );
+ photoviewer
+ .append('button')
+ .attr('class', 'resize-handle-x')
+ .on(
+ 'mousedown',
+ buildResizeListener(photoviewer, 'photoviewerResize', dispatch, { resizeOnX: true })
+ );
+
+ photoviewer
+ .append('button')
+ .attr('class', 'resize-handle-y')
+ .on(
+ 'mousedown',
+ buildResizeListener(photoviewer, 'photoviewerResize', dispatch, { resizeOnY: true })
+ );
+
+ var mapDimensions = map.dimensions();
+
+ // bind events
window.onbeforeunload = function() {
return context.save();
};
@@ -265,30 +294,13 @@ export function uiInit(context) {
context.history().unlock();
};
- var mapDimensions = map.dimensions();
-
-
- function onResize() {
- mapDimensions = utilGetDimensions(content, true);
- map.dimensions(mapDimensions);
- }
-
d3_select(window)
.on('resize.editor', onResize);
onResize();
- function pan(d) {
- return function() {
- d3_event.preventDefault();
- context.pan(d, 100);
- };
- }
-
-
- // pan amount
- var pa = 80;
+ var pa = 80; // pan amount
var keybinding = d3_keybinding('main')
.on('⌫', function() { d3_event.preventDefault(); })
.on('←', pan([pa, 0]))
@@ -316,8 +328,8 @@ export function uiInit(context) {
.call(uiShortcuts(context));
}
- var osm = context.connection(),
- auth = uiLoading(context).message(t('loading_auth')).blocking(true);
+ var osm = context.connection();
+ var auth = uiLoading(context).message(t('loading_auth')).blocking(true);
if (osm && auth) {
osm
@@ -336,6 +348,85 @@ export function uiInit(context) {
hash.startWalkthrough = false;
context.container().call(uiIntro(context));
}
+
+
+ function onResize() {
+ mapDimensions = utilGetDimensions(content, true);
+ map.dimensions(mapDimensions);
+
+ // shrink photo viewer if it is too big
+ // (-90 preserves space at top and bottom of map used by menus)
+ var photoDimensions = utilGetDimensions(photoviewer, true);
+ if (photoDimensions[0] > mapDimensions[0] || photoDimensions[1] > (mapDimensions[1] - 90)) {
+ var setPhotoDimensions = [
+ Math.min(photoDimensions[0], mapDimensions[0]),
+ Math.min(photoDimensions[1], mapDimensions[1] - 90),
+ ];
+
+ photoviewer
+ .style('width', setPhotoDimensions[0] + 'px')
+ .style('height', setPhotoDimensions[1] + 'px');
+
+ dispatch.call('photoviewerResize', photoviewer, setPhotoDimensions);
+ }
+ }
+
+
+ function pan(d) {
+ return function() {
+ d3_event.preventDefault();
+ context.pan(d, 100);
+ };
+ }
+
+ function buildResizeListener(target, eventName, dispatch, options) {
+ var resizeOnX = !!options.resizeOnX;
+ var resizeOnY = !!options.resizeOnY;
+ var minHeight = options.minHeight || 240;
+ var minWidth = options.minWidth || 320;
+ var startX;
+ var startY;
+ var startWidth;
+ var startHeight;
+
+ function startResize() {
+ var mapSize = context.map().dimensions();
+
+ if (resizeOnX) {
+ var maxWidth = mapSize[0];
+ var newWidth = clamp((startWidth + d3_event.clientX - startX), minWidth, maxWidth);
+ target.style('width', newWidth + 'px');
+ }
+
+ if (resizeOnY) {
+ var maxHeight = mapSize[1] - 90; // preserve space at top/bottom of map
+ var newHeight = clamp((startHeight + startY - d3_event.clientY), minHeight, maxHeight);
+ target.style('height', newHeight + 'px');
+ }
+
+ dispatch.call(eventName, target, utilGetDimensions(target, true));
+ }
+
+ function clamp(num, min, max) {
+ return Math.max(min, Math.min(num, max));
+ }
+
+ function stopResize() {
+ d3_select(window)
+ .on('.' + eventName, null);
+ }
+
+ return function initResize() {
+ startX = d3_event.clientX;
+ startY = d3_event.clientY;
+ startWidth = target.node().getBoundingClientRect().width;
+ startHeight = target.node().getBoundingClientRect().height;
+
+ d3_select(window)
+ .on('mousemove.' + eventName, startResize, false)
+ .on('mouseup.' + eventName, stopResize, false);
+ };
+ }
}
@@ -370,5 +461,5 @@ export function uiInit(context) {
ui.sidebar = uiSidebar(context);
- return ui;
+ return utilRebind(ui, dispatch, 'on');
}
diff --git a/modules/ui/intro/area.js b/modules/ui/intro/area.js
index 70ff8e95f..528bdaae4 100644
--- a/modules/ui/intro/area.js
+++ b/modules/ui/intro/area.js
@@ -17,12 +17,12 @@ import { icon, pad, transitionTime } from './helper';
export function uiIntroArea(context, reveal) {
- var dispatch = d3_dispatch('done'),
- playground = [-85.63552, 41.94159],
- playgroundPreset = context.presets().item('leisure/playground'),
- descriptionField = context.presets().field('description'),
- timeouts = [],
- areaId;
+ var dispatch = d3_dispatch('done');
+ var playground = [-85.63552, 41.94159];
+ var playgroundPreset = context.presets().item('leisure/playground');
+ var descriptionField = context.presets().field('description');
+ var timeouts = [];
+ var _areaID;
var chapter = {
@@ -51,7 +51,7 @@ export function uiIntroArea(context, reveal) {
function addArea() {
context.enter(modeBrowse(context));
context.history().reset('initial');
- areaId = null;
+ _areaID = null;
var msec = transitionTime(playground, context.map().center());
if (msec) { reveal(null, null, { duration: 0 }); }
@@ -85,7 +85,7 @@ export function uiIntroArea(context, reveal) {
return chapter.restart();
}
- areaId = null;
+ _areaID = null;
context.map().zoomEase(19.5, 500);
timeout(function() {
@@ -120,7 +120,7 @@ export function uiIntroArea(context, reveal) {
return chapter.restart();
}
- areaId = null;
+ _areaID = null;
revealPlayground(playground,
t('intro.areas.continue_playground', { alt: uiCmd.display('⌥') }),
{ duration: 250 }
@@ -144,7 +144,7 @@ export function uiIntroArea(context, reveal) {
return;
}
} else if (mode.id === 'select') {
- areaId = context.selectedIDs()[0];
+ _areaID = context.selectedIDs()[0];
return continueTo(searchPresets);
} else {
return chapter.restart();
@@ -164,7 +164,7 @@ export function uiIntroArea(context, reveal) {
return chapter.restart();
}
- areaId = null;
+ _areaID = null;
revealPlayground(playground,
t('intro.areas.finish_playground'), { duration: 250 }
);
@@ -181,7 +181,7 @@ export function uiIntroArea(context, reveal) {
if (mode.id === 'draw-area') {
return;
} else if (mode.id === 'select') {
- areaId = context.selectedIDs()[0];
+ _areaID = context.selectedIDs()[0];
return continueTo(searchPresets);
} else {
return chapter.restart();
@@ -197,12 +197,12 @@ export function uiIntroArea(context, reveal) {
function searchPresets() {
- if (!areaId || !context.hasEntity(areaId)) {
+ if (!_areaID || !context.hasEntity(_areaID)) {
return addArea();
}
var ids = context.selectedIDs();
- if (context.mode().id !== 'select' || !ids.length || ids[0] !== areaId) {
- context.enter(modeSelect(context, [areaId]));
+ if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {
+ context.enter(modeSelect(context, [_areaID]));
}
// disallow scrolling
@@ -222,14 +222,14 @@ export function uiIntroArea(context, reveal) {
}, 400); // after preset list pane visible..
context.on('enter.intro', function(mode) {
- if (!areaId || !context.hasEntity(areaId)) {
+ if (!_areaID || !context.hasEntity(_areaID)) {
return continueTo(addArea);
}
var ids = context.selectedIDs();
- if (mode.id !== 'select' || !ids.length || ids[0] !== areaId) {
+ if (mode.id !== 'select' || !ids.length || ids[0] !== _areaID) {
// keep the user's area selected..
- context.enter(modeSelect(context, [areaId]));
+ context.enter(modeSelect(context, [_areaID]));
// reset pane, in case user somehow happened to change it..
d3_select('.inspector-wrap .panewrap').style('right', '-100%');
@@ -278,11 +278,11 @@ export function uiIntroArea(context, reveal) {
function clickAddField() {
- if (!areaId || !context.hasEntity(areaId)) {
+ if (!_areaID || !context.hasEntity(_areaID)) {
return addArea();
}
var ids = context.selectedIDs();
- if (context.mode().id !== 'select' || !ids.length || ids[0] !== areaId) {
+ if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {
return searchPresets();
}
@@ -299,7 +299,7 @@ export function uiIntroArea(context, reveal) {
// It's possible for the user to add a description in a previous step..
// If they did this already, just continue to next step.
- var entity = context.entity(areaId);
+ var entity = context.entity(_areaID);
if (entity.tags.description) {
return continueTo(play);
}
@@ -351,11 +351,11 @@ export function uiIntroArea(context, reveal) {
function chooseDescriptionField() {
- if (!areaId || !context.hasEntity(areaId)) {
+ if (!_areaID || !context.hasEntity(_areaID)) {
return addArea();
}
var ids = context.selectedIDs();
- if (context.mode().id !== 'select' || !ids.length || ids[0] !== areaId) {
+ if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {
return searchPresets();
}
@@ -400,11 +400,11 @@ export function uiIntroArea(context, reveal) {
function describePlayground() {
- if (!areaId || !context.hasEntity(areaId)) {
+ if (!_areaID || !context.hasEntity(_areaID)) {
return addArea();
}
var ids = context.selectedIDs();
- if (context.mode().id !== 'select' || !ids.length || ids[0] !== areaId) {
+ if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {
return searchPresets();
}
@@ -432,11 +432,11 @@ export function uiIntroArea(context, reveal) {
function retryChooseDescription() {
- if (!areaId || !context.hasEntity(areaId)) {
+ if (!_areaID || !context.hasEntity(_areaID)) {
return addArea();
}
var ids = context.selectedIDs();
- if (context.mode().id !== 'select' || !ids.length || ids[0] !== areaId) {
+ if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {
return searchPresets();
}
diff --git a/modules/ui/intro/building.js b/modules/ui/intro/building.js
index c0fea7e75..67e3c5d0f 100644
--- a/modules/ui/intro/building.js
+++ b/modules/ui/intro/building.js
@@ -14,15 +14,15 @@ import { icon, pad, isMostlySquare, selectMenuItem, transitionTime } from './hel
export function uiIntroBuilding(context, reveal) {
- var dispatch = d3_dispatch('done'),
- house = [-85.62815, 41.95638],
- tank = [-85.62732, 41.95347],
- buildingCatetory = context.presets().item('category-building'),
- housePreset = context.presets().item('building/house'),
- tankPreset = context.presets().item('man_made/storage_tank'),
- timeouts = [],
- houseId = null,
- tankId = null;
+ var dispatch = d3_dispatch('done');
+ var house = [-85.62815, 41.95638];
+ var tank = [-85.62732, 41.95347];
+ var buildingCatetory = context.presets().item('category-building');
+ var housePreset = context.presets().item('building/house');
+ var tankPreset = context.presets().item('man_made/storage_tank');
+ var timeouts = [];
+ var _houseID = null;
+ var _tankID = null;
var chapter = {
@@ -76,7 +76,7 @@ export function uiIntroBuilding(context, reveal) {
function addHouse() {
context.enter(modeBrowse(context));
context.history().reset('initial');
- houseId = null;
+ _houseID = null;
var msec = transitionTime(house, context.map().center());
if (msec) { reveal(null, null, { duration: 0 }); }
@@ -110,7 +110,7 @@ export function uiIntroBuilding(context, reveal) {
return continueTo(addHouse);
}
- houseId = null;
+ _houseID = null;
context.map().zoomEase(20, 500);
timeout(function() {
@@ -140,7 +140,7 @@ export function uiIntroBuilding(context, reveal) {
return continueTo(addHouse);
}
- houseId = null;
+ _houseID = null;
revealHouse(house, t('intro.buildings.continue_building'));
@@ -152,13 +152,13 @@ export function uiIntroBuilding(context, reveal) {
if (mode.id === 'draw-area') {
return;
} else if (mode.id === 'select') {
- var graph = context.graph(),
- way = context.entity(context.selectedIDs()[0]),
- nodes = graph.childNodes(way),
- points = _uniq(nodes).map(function(n) { return context.projection(n.loc); });
+ var graph = context.graph();
+ var way = context.entity(context.selectedIDs()[0]);
+ var nodes = graph.childNodes(way);
+ var points = _uniq(nodes).map(function(n) { return context.projection(n.loc); });
if (isMostlySquare(points)) {
- houseId = way.id;
+ _houseID = way.id;
return continueTo(chooseCategoryBuilding);
} else {
return continueTo(retryHouse);
@@ -198,12 +198,12 @@ export function uiIntroBuilding(context, reveal) {
function chooseCategoryBuilding() {
- if (!houseId || !context.hasEntity(houseId)) {
+ if (!_houseID || !context.hasEntity(_houseID)) {
return addHouse();
}
var ids = context.selectedIDs();
- if (context.mode().id !== 'select' || !ids.length || ids[0] !== houseId) {
- context.enter(modeSelect(context, [houseId]));
+ if (context.mode().id !== 'select' || !ids.length || ids[0] !== _houseID) {
+ context.enter(modeSelect(context, [_houseID]));
}
// disallow scrolling
@@ -228,11 +228,11 @@ export function uiIntroBuilding(context, reveal) {
context.on('enter.intro', function(mode) {
- if (!houseId || !context.hasEntity(houseId)) {
+ if (!_houseID || !context.hasEntity(_houseID)) {
return continueTo(addHouse);
}
var ids = context.selectedIDs();
- if (mode.id !== 'select' || !ids.length || ids[0] !== houseId) {
+ if (mode.id !== 'select' || !ids.length || ids[0] !== _houseID) {
return continueTo(chooseCategoryBuilding);
}
});
@@ -247,12 +247,12 @@ export function uiIntroBuilding(context, reveal) {
function choosePresetHouse() {
- if (!houseId || !context.hasEntity(houseId)) {
+ if (!_houseID || !context.hasEntity(_houseID)) {
return addHouse();
}
var ids = context.selectedIDs();
- if (context.mode().id !== 'select' || !ids.length || ids[0] !== houseId) {
- context.enter(modeSelect(context, [houseId]));
+ if (context.mode().id !== 'select' || !ids.length || ids[0] !== _houseID) {
+ context.enter(modeSelect(context, [_houseID]));
}
// disallow scrolling
@@ -274,15 +274,14 @@ export function uiIntroBuilding(context, reveal) {
continueTo(closeEditorHouse);
});
-
}, 400); // after preset list pane visible..
context.on('enter.intro', function(mode) {
- if (!houseId || !context.hasEntity(houseId)) {
+ if (!_houseID || !context.hasEntity(_houseID)) {
return continueTo(addHouse);
}
var ids = context.selectedIDs();
- if (mode.id !== 'select' || !ids.length || ids[0] !== houseId) {
+ if (mode.id !== 'select' || !ids.length || ids[0] !== _houseID) {
return continueTo(chooseCategoryBuilding);
}
});
@@ -297,12 +296,12 @@ export function uiIntroBuilding(context, reveal) {
function closeEditorHouse() {
- if (!houseId || !context.hasEntity(houseId)) {
+ if (!_houseID || !context.hasEntity(_houseID)) {
return addHouse();
}
var ids = context.selectedIDs();
- if (context.mode().id !== 'select' || !ids.length || ids[0] !== houseId) {
- context.enter(modeSelect(context, [houseId]));
+ if (context.mode().id !== 'select' || !ids.length || ids[0] !== _houseID) {
+ context.enter(modeSelect(context, [_houseID]));
}
context.history().checkpoint('hasHouse');
@@ -325,7 +324,7 @@ export function uiIntroBuilding(context, reveal) {
function rightClickHouse() {
- if (!houseId) return chapter.restart();
+ if (!_houseID) return chapter.restart();
context.enter(modeBrowse(context));
context.history().reset('hasHouse');
@@ -340,7 +339,7 @@ export function uiIntroBuilding(context, reveal) {
context.on('enter.intro', function(mode) {
if (mode.id !== 'select') return;
var ids = context.selectedIDs();
- if (ids.length !== 1 || ids[0] !== houseId) return;
+ if (ids.length !== 1 || ids[0] !== _houseID) return;
timeout(function() {
var node = selectMenuItem('orthogonalize').node();
@@ -367,8 +366,8 @@ export function uiIntroBuilding(context, reveal) {
function clickSquare() {
- if (!houseId) return chapter.restart();
- var entity = context.hasEntity(houseId);
+ if (!_houseID) return chapter.restart();
+ var entity = context.hasEntity(_houseID);
if (!entity) return continueTo(rightClickHouse);
var node = selectMenuItem('orthogonalize').node();
@@ -453,7 +452,7 @@ export function uiIntroBuilding(context, reveal) {
function addTank() {
context.enter(modeBrowse(context));
context.history().reset('doneSquare');
- tankId = null;
+ _tankID = null;
var msec = transitionTime(tank, context.map().center());
if (msec) { reveal(null, null, { duration: 0 }); }
@@ -482,7 +481,7 @@ export function uiIntroBuilding(context, reveal) {
return continueTo(addTank);
}
- tankId = null;
+ _tankID = null;
timeout(function() {
revealTank(tank, t('intro.buildings.start_tank'));
@@ -511,7 +510,7 @@ export function uiIntroBuilding(context, reveal) {
return continueTo(addTank);
}
- tankId = null;
+ _tankID = null;
revealTank(tank, t('intro.buildings.continue_tank'));
@@ -523,7 +522,7 @@ export function uiIntroBuilding(context, reveal) {
if (mode.id === 'draw-area') {
return;
} else if (mode.id === 'select') {
- tankId = context.selectedIDs()[0];
+ _tankID = context.selectedIDs()[0];
return continueTo(searchPresetTank);
} else {
return continueTo(addTank);
@@ -539,12 +538,12 @@ export function uiIntroBuilding(context, reveal) {
function searchPresetTank() {
- if (!tankId || !context.hasEntity(tankId)) {
+ if (!_tankID || !context.hasEntity(_tankID)) {
return addTank();
}
var ids = context.selectedIDs();
- if (context.mode().id !== 'select' || !ids.length || ids[0] !== tankId) {
- context.enter(modeSelect(context, [tankId]));
+ if (context.mode().id !== 'select' || !ids.length || ids[0] !== _tankID) {
+ context.enter(modeSelect(context, [_tankID]));
}
// disallow scrolling
@@ -564,14 +563,14 @@ export function uiIntroBuilding(context, reveal) {
}, 400); // after preset list pane visible..
context.on('enter.intro', function(mode) {
- if (!tankId || !context.hasEntity(tankId)) {
+ if (!_tankID || !context.hasEntity(_tankID)) {
return continueTo(addTank);
}
var ids = context.selectedIDs();
- if (mode.id !== 'select' || !ids.length || ids[0] !== tankId) {
+ if (mode.id !== 'select' || !ids.length || ids[0] !== _tankID) {
// keep the user's area selected..
- context.enter(modeSelect(context, [tankId]));
+ context.enter(modeSelect(context, [_tankID]));
// reset pane, in case user somehow happened to change it..
d3_select('.inspector-wrap .panewrap').style('right', '-100%');
@@ -620,12 +619,12 @@ export function uiIntroBuilding(context, reveal) {
function closeEditorTank() {
- if (!tankId || !context.hasEntity(tankId)) {
+ if (!_tankID || !context.hasEntity(_tankID)) {
return addTank();
}
var ids = context.selectedIDs();
- if (context.mode().id !== 'select' || !ids.length || ids[0] !== tankId) {
- context.enter(modeSelect(context, [tankId]));
+ if (context.mode().id !== 'select' || !ids.length || ids[0] !== _tankID) {
+ context.enter(modeSelect(context, [_tankID]));
}
context.history().checkpoint('hasTank');
@@ -648,7 +647,7 @@ export function uiIntroBuilding(context, reveal) {
function rightClickTank() {
- if (!tankId) return continueTo(addTank);
+ if (!_tankID) return continueTo(addTank);
context.enter(modeBrowse(context));
context.history().reset('hasTank');
@@ -658,7 +657,7 @@ export function uiIntroBuilding(context, reveal) {
context.on('enter.intro', function(mode) {
if (mode.id !== 'select') return;
var ids = context.selectedIDs();
- if (ids.length !== 1 || ids[0] !== tankId) return;
+ if (ids.length !== 1 || ids[0] !== _tankID) return;
timeout(function() {
var node = selectMenuItem('circularize').node();
@@ -689,8 +688,8 @@ export function uiIntroBuilding(context, reveal) {
function clickCircle() {
- if (!tankId) return chapter.restart();
- var entity = context.hasEntity(tankId);
+ if (!_tankID) return chapter.restart();
+ var entity = context.hasEntity(_tankID);
if (!entity) return continueTo(rightClickTank);
var node = selectMenuItem('circularize').node();
diff --git a/modules/ui/intro/helper.js b/modules/ui/intro/helper.js
index 4c8cdb361..de261ea1a 100644
--- a/modules/ui/intro/helper.js
+++ b/modules/ui/intro/helper.js
@@ -91,10 +91,10 @@ export function localize(obj) {
'postcode', 'province', 'quarter', 'state', 'subdistrict', 'suburb'
];
addrTags.forEach(function(k) {
- var key = 'intro.graph.' + k,
- tag = 'addr:' + k,
- val = obj.tags && obj.tags[tag],
- str = t(key, { default: val });
+ var key = 'intro.graph.' + k;
+ var tag = 'addr:' + k;
+ var val = obj.tags && obj.tags[tag];
+ var str = t(key, { default: val });
if (str) {
if (str.match(/^<.*>$/) !== null) {
@@ -114,10 +114,10 @@ export function localize(obj) {
export function isMostlySquare(points) {
// note: uses 15 here instead of the 12 from actionOrthogonalize because
// actionOrthogonalize can actually straighten some larger angles as it iterates
- var threshold = 15, // degrees within right or straight
- lowerBound = Math.cos((90 - threshold) * Math.PI / 180), // near right
- upperBound = Math.cos(threshold * Math.PI / 180), // near straight
- mag;
+ var threshold = 15; // degrees within right or straight
+ var lowerBound = Math.cos((90 - threshold) * Math.PI / 180); // near right
+ var upperBound = Math.cos(threshold * Math.PI / 180); // near straight
+ var mag;
for (var i = 0; i < points.length; i++) {
mag = Math.abs(normalizedDotProduct(i, points));
@@ -130,11 +130,11 @@ export function isMostlySquare(points) {
function normalizedDotProduct(i, points) {
- var a = points[(i - 1 + points.length) % points.length],
- b = points[i],
- c = points[(i + 1) % points.length],
- p = subtractPoints(a, b),
- q = subtractPoints(c, b);
+ var a = points[(i - 1 + points.length) % points.length];
+ var b = points[i];
+ var c = points[(i + 1) % points.length];
+ var p = subtractPoints(a, b);
+ var q = subtractPoints(c, b);
p = normalizePoint(p);
q = normalizePoint(q);
diff --git a/modules/ui/intro/line.js b/modules/ui/intro/line.js
index 986f21351..be40f73cf 100644
--- a/modules/ui/intro/line.js
+++ b/modules/ui/intro/line.js
@@ -15,29 +15,29 @@ import { icon, pad, selectMenuItem, transitionTime } from './helper';
export function uiIntroLine(context, reveal) {
- var dispatch = d3_dispatch('done'),
- timeouts = [],
- tulipRoadId = null,
- flowerRoadId = 'w646',
- tulipRoadStart = [-85.6297754121684, 41.95805253325314],
- tulipRoadMidpoint = [-85.62975395449628, 41.95787501510204],
- tulipRoadIntersection = [-85.62974496187628, 41.95742515554585],
- roadCategory = context.presets().item('category-road'),
- residentialPreset = context.presets().item('highway/residential'),
- woodRoadId = 'w525',
- woodRoadEndId = 'n2862',
- woodRoadAddNode = [-85.62390110349587, 41.95397111462291],
- woodRoadDragEndpoint = [-85.623867390213, 41.95466987786487],
- woodRoadDragMidpoint = [-85.62386254803509, 41.95430395953872],
- washingtonStreetId = 'w522',
- twelfthAvenueId = 'w1',
- eleventhAvenueEndId = 'n3550',
- twelfthAvenueEndId = 'n5',
- washingtonSegmentId = null,
- eleventhAvenueEnd = context.entity(eleventhAvenueEndId).loc,
- twelfthAvenueEnd = context.entity(twelfthAvenueEndId).loc,
- deleteLinesLoc = [-85.6219395542764, 41.95228033922477],
- twelfthAvenue = [-85.62219310052491, 41.952505413152956];
+ var dispatch = d3_dispatch('done');
+ var timeouts = [];
+ var _tulipRoadID = null;
+ var flowerRoadID = 'w646';
+ var tulipRoadStart = [-85.6297754121684, 41.95805253325314];
+ var tulipRoadMidpoint = [-85.62975395449628, 41.95787501510204];
+ var tulipRoadIntersection = [-85.62974496187628, 41.95742515554585];
+ var roadCategory = context.presets().item('category-road');
+ var residentialPreset = context.presets().item('highway/residential');
+ var woodRoadID = 'w525';
+ var woodRoadEndID = 'n2862';
+ var woodRoadAddNode = [-85.62390110349587, 41.95397111462291];
+ var woodRoadDragEndpoint = [-85.623867390213, 41.95466987786487];
+ var woodRoadDragMidpoint = [-85.62386254803509, 41.95430395953872];
+ var washingtonStreetID = 'w522';
+ var twelfthAvenueID = 'w1';
+ var eleventhAvenueEndID = 'n3550';
+ var twelfthAvenueEndID = 'n5';
+ var _washingtonSegmentID = null;
+ var eleventhAvenueEnd = context.entity(eleventhAvenueEndID).loc;
+ var twelfthAvenueEnd = context.entity(twelfthAvenueEndID).loc;
+ var deleteLinesLoc = [-85.6219395542764, 41.95228033922477];
+ var twelfthAvenue = [-85.62219310052491, 41.952505413152956];
var chapter = {
@@ -106,11 +106,9 @@ export function uiIntroLine(context, reveal) {
function startLine() {
- if (context.mode().id !== 'add-line') {
- return chapter.restart();
- }
+ if (context.mode().id !== 'add-line') return chapter.restart();
- tulipRoadId = null;
+ _tulipRoadID = null;
var padding = 70 * Math.pow(2, context.map().zoom() - 18);
var box = pad(tulipRoadStart, padding, context);
@@ -138,11 +136,9 @@ export function uiIntroLine(context, reveal) {
function drawLine() {
- if (context.mode().id !== 'draw-line') {
- return chapter.restart();
- }
+ if (context.mode().id !== 'draw-line') return chapter.restart();
- tulipRoadId = context.mode().selectedIDs()[0];
+ _tulipRoadID = context.mode().selectedIDs()[0];
context.map().centerEase(tulipRoadMidpoint, 500);
timeout(function() {
@@ -165,23 +161,20 @@ export function uiIntroLine(context, reveal) {
}, 550); // after easing..
context.history().on('change.intro', function() {
- var entity = tulipRoadId && context.hasEntity(tulipRoadId);
- if (!entity) return chapter.restart();
-
if (isLineConnected()) {
continueTo(continueLine);
}
});
context.on('enter.intro', function(mode) {
- if (mode.id === 'draw-line')
+ if (mode.id === 'draw-line') {
return;
- else if (mode.id === 'select') {
+ } else if (mode.id === 'select') {
continueTo(retryIntersect);
return;
- }
- else
+ } else {
return chapter.restart();
+ }
});
function continueTo(nextStep) {
@@ -194,13 +187,13 @@ export function uiIntroLine(context, reveal) {
function isLineConnected() {
- var entity = tulipRoadId && context.hasEntity(tulipRoadId);
+ var entity = _tulipRoadID && context.hasEntity(_tulipRoadID);
if (!entity) return false;
var drawNodes = context.graph().childNodes(entity);
return _some(drawNodes, function(node) {
return _some(context.graph().parentWays(node), function(parent) {
- return parent.id === flowerRoadId;
+ return parent.id === flowerRoadID;
});
});
}
@@ -220,7 +213,7 @@ export function uiIntroLine(context, reveal) {
function continueLine() {
if (context.mode().id !== 'draw-line') return chapter.restart();
- var entity = tulipRoadId && context.hasEntity(tulipRoadId);
+ var entity = _tulipRoadID && context.hasEntity(_tulipRoadID);
if (!entity) return chapter.restart();
context.map().centerEase(tulipRoadIntersection, 500);
@@ -244,9 +237,7 @@ export function uiIntroLine(context, reveal) {
function chooseCategoryRoad() {
- if (context.mode().id !== 'select') {
- return chapter.restart();
- }
+ if (context.mode().id !== 'select') return chapter.restart();
context.on('exit.intro', function() {
return chapter.restart();
@@ -282,9 +273,7 @@ export function uiIntroLine(context, reveal) {
function choosePresetResidential() {
- if (context.mode().id !== 'select') {
- return chapter.restart();
- }
+ if (context.mode().id !== 'select') return chapter.restart();
context.on('exit.intro', function() {
return chapter.restart();
@@ -320,9 +309,7 @@ export function uiIntroLine(context, reveal) {
// selected wrong road type
function retryPresetResidential() {
- if (context.mode().id !== 'select') {
- return chapter.restart();
- }
+ if (context.mode().id !== 'select') return chapter.restart();
context.on('exit.intro', function() {
return chapter.restart();
@@ -390,7 +377,7 @@ export function uiIntroLine(context, reveal) {
function updateLine() {
context.history().reset('doneAddLine');
- if (!context.hasEntity(woodRoadId) || !context.hasEntity(woodRoadEndId)) {
+ if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
return chapter.restart();
}
@@ -425,7 +412,7 @@ export function uiIntroLine(context, reveal) {
function addNode() {
context.history().reset('doneAddLine');
- if (!context.hasEntity(woodRoadId) || !context.hasEntity(woodRoadEndId)) {
+ if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
return chapter.restart();
}
@@ -440,7 +427,7 @@ export function uiIntroLine(context, reveal) {
});
context.history().on('change.intro', function(changed) {
- if (!context.hasEntity(woodRoadId) || !context.hasEntity(woodRoadEndId)) {
+ if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
return continueTo(updateLine);
}
if (changed.created().length === 1) {
@@ -464,7 +451,7 @@ export function uiIntroLine(context, reveal) {
function startDragEndpoint() {
- if (!context.hasEntity(woodRoadId) || !context.hasEntity(woodRoadEndId)) {
+ if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
return continueTo(updateLine);
}
var padding = 100 * Math.pow(2, context.map().zoom() - 19);
@@ -472,14 +459,14 @@ export function uiIntroLine(context, reveal) {
reveal(box, t('intro.lines.start_drag_endpoint'));
context.map().on('move.intro drawn.intro', function() {
- if (!context.hasEntity(woodRoadId) || !context.hasEntity(woodRoadEndId)) {
+ if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
return continueTo(updateLine);
}
var padding = 100 * Math.pow(2, context.map().zoom() - 19);
var box = pad(woodRoadDragEndpoint, padding, context);
reveal(box, t('intro.lines.start_drag_endpoint'), { duration: 0 });
- var entity = context.entity(woodRoadEndId);
+ var entity = context.entity(woodRoadEndID);
if (geoSphericalDistance(entity.loc, woodRoadDragEndpoint) <= 4) {
continueTo(finishDragEndpoint);
}
@@ -493,7 +480,7 @@ export function uiIntroLine(context, reveal) {
function finishDragEndpoint() {
- if (!context.hasEntity(woodRoadId) || !context.hasEntity(woodRoadEndId)) {
+ if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
return continueTo(updateLine);
}
@@ -502,14 +489,14 @@ export function uiIntroLine(context, reveal) {
reveal(box, t('intro.lines.finish_drag_endpoint'));
context.map().on('move.intro drawn.intro', function() {
- if (!context.hasEntity(woodRoadId) || !context.hasEntity(woodRoadEndId)) {
+ if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
return continueTo(updateLine);
}
var padding = 100 * Math.pow(2, context.map().zoom() - 19);
var box = pad(woodRoadDragEndpoint, padding, context);
reveal(box, t('intro.lines.finish_drag_endpoint'), { duration: 0 });
- var entity = context.entity(woodRoadEndId);
+ var entity = context.entity(woodRoadEndID);
if (geoSphericalDistance(entity.loc, woodRoadDragEndpoint) > 4) {
continueTo(startDragEndpoint);
}
@@ -528,11 +515,11 @@ export function uiIntroLine(context, reveal) {
function startDragMidpoint() {
- if (!context.hasEntity(woodRoadId) || !context.hasEntity(woodRoadEndId)) {
+ if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
return continueTo(updateLine);
}
- if (context.selectedIDs().indexOf(woodRoadId) === -1) {
- context.enter(modeSelect(context, [woodRoadId]));
+ if (context.selectedIDs().indexOf(woodRoadID) === -1) {
+ context.enter(modeSelect(context, [woodRoadID]));
}
var padding = 80 * Math.pow(2, context.map().zoom() - 19);
@@ -540,7 +527,7 @@ export function uiIntroLine(context, reveal) {
reveal(box, t('intro.lines.start_drag_midpoint'));
context.map().on('move.intro drawn.intro', function() {
- if (!context.hasEntity(woodRoadId) || !context.hasEntity(woodRoadEndId)) {
+ if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
return continueTo(updateLine);
}
var padding = 80 * Math.pow(2, context.map().zoom() - 19);
@@ -557,7 +544,7 @@ export function uiIntroLine(context, reveal) {
context.on('enter.intro', function(mode) {
if (mode.id !== 'select') {
// keep Wood Road selected so midpoint triangles are drawn..
- context.enter(modeSelect(context, [woodRoadId]));
+ context.enter(modeSelect(context, [woodRoadID]));
}
});
@@ -571,7 +558,7 @@ export function uiIntroLine(context, reveal) {
function continueDragMidpoint() {
- if (!context.hasEntity(woodRoadId) || !context.hasEntity(woodRoadEndId)) {
+ if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
return continueTo(updateLine);
}
@@ -589,7 +576,7 @@ export function uiIntroLine(context, reveal) {
);
context.map().on('move.intro drawn.intro', function() {
- if (!context.hasEntity(woodRoadId) || !context.hasEntity(woodRoadEndId)) {
+ if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
return continueTo(updateLine);
}
var padding = 100 * Math.pow(2, context.map().zoom() - 19);
@@ -611,9 +598,9 @@ export function uiIntroLine(context, reveal) {
context.history().reset('doneUpdateLine');
context.enter(modeBrowse(context));
- if (!context.hasEntity(washingtonStreetId) ||
- !context.hasEntity(twelfthAvenueId) ||
- !context.hasEntity(eleventhAvenueEndId)) {
+ if (!context.hasEntity(washingtonStreetID) ||
+ !context.hasEntity(twelfthAvenueID) ||
+ !context.hasEntity(eleventhAvenueEndID)) {
return chapter.restart();
}
@@ -683,7 +670,7 @@ export function uiIntroLine(context, reveal) {
context.on('enter.intro', function(mode) {
if (mode.id !== 'select') return;
var ids = context.selectedIDs();
- if (ids.length !== 1 || ids[0] !== eleventhAvenueEndId) return;
+ if (ids.length !== 1 || ids[0] !== eleventhAvenueEndID) return;
timeout(function() {
var node = selectMenuItem('split').node();
@@ -710,9 +697,9 @@ export function uiIntroLine(context, reveal) {
function splitIntersection() {
- if (!context.hasEntity(washingtonStreetId) ||
- !context.hasEntity(twelfthAvenueId) ||
- !context.hasEntity(eleventhAvenueEndId)) {
+ if (!context.hasEntity(washingtonStreetID) ||
+ !context.hasEntity(twelfthAvenueID) ||
+ !context.hasEntity(eleventhAvenueEndID)) {
return continueTo(deleteLines);
}
@@ -721,7 +708,7 @@ export function uiIntroLine(context, reveal) {
var wasChanged = false;
var menuCoords = context.map().mouseCoordinates();
- washingtonSegmentId = null;
+ _washingtonSegmentID = null;
revealEditMenu(menuCoords, t('intro.lines.split_intersection',
{ button: icon('#iD-operation-split', 'pre-text'), street: t('intro.graph.name.washington-street') })
@@ -741,10 +728,10 @@ export function uiIntroLine(context, reveal) {
wasChanged = true;
timeout(function() {
if (context.history().undoAnnotation() === t('operations.split.annotation.line')) {
- washingtonSegmentId = changed.created()[0].id;
+ _washingtonSegmentID = changed.created()[0].id;
continueTo(didSplit);
} else {
- washingtonSegmentId = null;
+ _washingtonSegmentID = null;
continueTo(retrySplit);
}
}, 300); // after any transition (e.g. if user deleted intersection)
@@ -785,11 +772,11 @@ export function uiIntroLine(context, reveal) {
function didSplit() {
- if (!washingtonSegmentId ||
- !context.hasEntity(washingtonSegmentId) ||
- !context.hasEntity(washingtonStreetId) ||
- !context.hasEntity(twelfthAvenueId) ||
- !context.hasEntity(eleventhAvenueEndId)) {
+ if (!_washingtonSegmentID ||
+ !context.hasEntity(_washingtonSegmentID) ||
+ !context.hasEntity(washingtonStreetID) ||
+ !context.hasEntity(twelfthAvenueID) ||
+ !context.hasEntity(eleventhAvenueEndID)) {
return continueTo(rightClickIntersection);
}
@@ -819,17 +806,17 @@ export function uiIntroLine(context, reveal) {
context.on('enter.intro', function() {
var ids = context.selectedIDs();
- if (ids.length === 1 && ids[0] === washingtonSegmentId) {
+ if (ids.length === 1 && ids[0] === _washingtonSegmentID) {
continueTo(multiSelect);
}
});
context.history().on('change.intro', function() {
- if (!washingtonSegmentId ||
- !context.hasEntity(washingtonSegmentId) ||
- !context.hasEntity(washingtonStreetId) ||
- !context.hasEntity(twelfthAvenueId) ||
- !context.hasEntity(eleventhAvenueEndId)) {
+ if (!_washingtonSegmentID ||
+ !context.hasEntity(_washingtonSegmentID) ||
+ !context.hasEntity(washingtonStreetID) ||
+ !context.hasEntity(twelfthAvenueID) ||
+ !context.hasEntity(eleventhAvenueEndID)) {
return continueTo(rightClickIntersection);
}
});
@@ -844,17 +831,17 @@ export function uiIntroLine(context, reveal) {
function multiSelect() {
- if (!washingtonSegmentId ||
- !context.hasEntity(washingtonSegmentId) ||
- !context.hasEntity(washingtonStreetId) ||
- !context.hasEntity(twelfthAvenueId) ||
- !context.hasEntity(eleventhAvenueEndId)) {
+ if (!_washingtonSegmentID ||
+ !context.hasEntity(_washingtonSegmentID) ||
+ !context.hasEntity(washingtonStreetID) ||
+ !context.hasEntity(twelfthAvenueID) ||
+ !context.hasEntity(eleventhAvenueEndID)) {
return continueTo(rightClickIntersection);
}
var ids = context.selectedIDs();
- var hasWashington = ids.indexOf(washingtonSegmentId) !== -1;
- var hasTwelfth = ids.indexOf(twelfthAvenueId) !== -1;
+ var hasWashington = ids.indexOf(_washingtonSegmentID) !== -1;
+ var hasTwelfth = ids.indexOf(twelfthAvenueID) !== -1;
if (hasWashington && hasTwelfth) {
return continueTo(multiRightClick);
@@ -910,11 +897,11 @@ export function uiIntroLine(context, reveal) {
});
context.history().on('change.intro', function() {
- if (!washingtonSegmentId ||
- !context.hasEntity(washingtonSegmentId) ||
- !context.hasEntity(washingtonStreetId) ||
- !context.hasEntity(twelfthAvenueId) ||
- !context.hasEntity(eleventhAvenueEndId)) {
+ if (!_washingtonSegmentID ||
+ !context.hasEntity(_washingtonSegmentID) ||
+ !context.hasEntity(washingtonStreetID) ||
+ !context.hasEntity(twelfthAvenueID) ||
+ !context.hasEntity(eleventhAvenueEndID)) {
return continueTo(rightClickIntersection);
}
});
@@ -930,11 +917,11 @@ export function uiIntroLine(context, reveal) {
function multiRightClick() {
- if (!washingtonSegmentId ||
- !context.hasEntity(washingtonSegmentId) ||
- !context.hasEntity(washingtonStreetId) ||
- !context.hasEntity(twelfthAvenueId) ||
- !context.hasEntity(eleventhAvenueEndId)) {
+ if (!_washingtonSegmentID ||
+ !context.hasEntity(_washingtonSegmentID) ||
+ !context.hasEntity(washingtonStreetID) ||
+ !context.hasEntity(twelfthAvenueID) ||
+ !context.hasEntity(eleventhAvenueEndID)) {
return continueTo(rightClickIntersection);
}
@@ -952,13 +939,13 @@ export function uiIntroLine(context, reveal) {
timeout(function() {
var ids = context.selectedIDs();
if (ids.length === 2 &&
- ids.indexOf(twelfthAvenueId) !== -1 &&
- ids.indexOf(washingtonSegmentId) !== -1) {
+ ids.indexOf(twelfthAvenueID) !== -1 &&
+ ids.indexOf(_washingtonSegmentID) !== -1) {
var node = selectMenuItem('delete').node();
if (!node) return;
continueTo(multiDelete);
} else if (ids.length === 1 &&
- ids.indexOf(washingtonSegmentId) !== -1) {
+ ids.indexOf(_washingtonSegmentID) !== -1) {
return continueTo(multiSelect);
} else {
return continueTo(didSplit);
@@ -967,11 +954,11 @@ export function uiIntroLine(context, reveal) {
}, true);
context.history().on('change.intro', function() {
- if (!washingtonSegmentId ||
- !context.hasEntity(washingtonSegmentId) ||
- !context.hasEntity(washingtonStreetId) ||
- !context.hasEntity(twelfthAvenueId) ||
- !context.hasEntity(eleventhAvenueEndId)) {
+ if (!_washingtonSegmentID ||
+ !context.hasEntity(_washingtonSegmentID) ||
+ !context.hasEntity(washingtonStreetID) ||
+ !context.hasEntity(twelfthAvenueID) ||
+ !context.hasEntity(eleventhAvenueEndID)) {
return continueTo(rightClickIntersection);
}
});
@@ -986,11 +973,11 @@ export function uiIntroLine(context, reveal) {
function multiDelete() {
- if (!washingtonSegmentId ||
- !context.hasEntity(washingtonSegmentId) ||
- !context.hasEntity(washingtonStreetId) ||
- !context.hasEntity(twelfthAvenueId) ||
- !context.hasEntity(eleventhAvenueEndId)) {
+ if (!_washingtonSegmentID ||
+ !context.hasEntity(_washingtonSegmentID) ||
+ !context.hasEntity(washingtonStreetID) ||
+ !context.hasEntity(twelfthAvenueID) ||
+ !context.hasEntity(eleventhAvenueEndID)) {
return continueTo(rightClickIntersection);
}
@@ -1010,13 +997,13 @@ export function uiIntroLine(context, reveal) {
});
context.on('exit.intro', function() {
- if (context.hasEntity(washingtonSegmentId) || context.hasEntity(twelfthAvenueId)) {
+ if (context.hasEntity(_washingtonSegmentID) || context.hasEntity(twelfthAvenueID)) {
return continueTo(multiSelect); // left select mode but roads still exist
}
});
context.history().on('change.intro', function() {
- if (context.hasEntity(washingtonSegmentId) || context.hasEntity(twelfthAvenueId)) {
+ if (context.hasEntity(_washingtonSegmentID) || context.hasEntity(twelfthAvenueID)) {
continueTo(retryDelete); // changed something but roads still exist
} else {
continueTo(play);
diff --git a/modules/ui/intro/navigation.js b/modules/ui/intro/navigation.js
index 0b93bbe73..b8f298ee1 100644
--- a/modules/ui/intro/navigation.js
+++ b/modules/ui/intro/navigation.js
@@ -12,15 +12,15 @@ import { icon, pointBox, transitionTime } from './helper';
export function uiIntroNavigation(context, reveal) {
- var dispatch = d3_dispatch('done'),
- timeouts = [],
- hallId = 'n2061',
- townHall = [-85.63591, 41.94285],
- springStreetId = 'w397',
- springStreetEndId = 'n1834',
- springStreet = [-85.63582, 41.94255],
- onewayField = context.presets().field('oneway'),
- maxspeedField = context.presets().field('maxspeed');
+ var dispatch = d3_dispatch('done');
+ var timeouts = [];
+ var hallId = 'n2061';
+ var townHall = [-85.63591, 41.94285];
+ var springStreetId = 'w397';
+ var springStreetEndId = 'n1834';
+ var springStreet = [-85.63582, 41.94255];
+ var onewayField = context.presets().field('oneway');
+ var maxspeedField = context.presets().field('maxspeed');
var chapter = {
@@ -409,9 +409,9 @@ export function uiIntroNavigation(context, reveal) {
function checkSearchResult() {
- var first = d3_select('.feature-list-item:nth-child(0n+2)'), // skip "No Results" item
- firstName = first.select('.entity-name'),
- name = t('intro.graph.name.spring-street');
+ var first = d3_select('.feature-list-item:nth-child(0n+2)'); // skip "No Results" item
+ var firstName = first.select('.entity-name');
+ var name = t('intro.graph.name.spring-street');
if (!firstName.empty() && firstName.text() === name) {
reveal(first.node(),
diff --git a/modules/ui/intro/point.js b/modules/ui/intro/point.js
index 8f6ce0af6..5c58b7188 100644
--- a/modules/ui/intro/point.js
+++ b/modules/ui/intro/point.js
@@ -12,12 +12,12 @@ import { icon, pointBox, pad, selectMenuItem, transitionTime } from './helper';
export function uiIntroPoint(context, reveal) {
- var dispatch = d3_dispatch('done'),
- timeouts = [],
- intersection = [-85.63279, 41.94394],
- building = [-85.632422, 41.944045],
- cafePreset = context.presets().item('amenity/cafe'),
- pointId = null;
+ var dispatch = d3_dispatch('done');
+ var timeouts = [];
+ var intersection = [-85.63279, 41.94394];
+ var building = [-85.632422, 41.944045];
+ var cafePreset = context.presets().item('amenity/cafe');
+ var _pointID = null;
var chapter = {
@@ -66,7 +66,7 @@ export function uiIntroPoint(context, reveal) {
var tooltip = reveal('button.add-point',
t('intro.points.add_point', { button: icon('#iD-icon-point', 'pre-text') }));
- pointId = null;
+ _pointID = null;
tooltip.selectAll('.tooltip-inner')
.insert('svg', 'span')
@@ -102,7 +102,7 @@ export function uiIntroPoint(context, reveal) {
context.on('enter.intro', function(mode) {
if (mode.id !== 'select') return chapter.restart();
- pointId = context.mode().selectedIDs()[0];
+ _pointID = context.mode().selectedIDs()[0];
continueTo(searchPreset);
});
@@ -115,7 +115,7 @@ export function uiIntroPoint(context, reveal) {
function searchPreset() {
- if (context.mode().id !== 'select' || !pointId || !context.hasEntity(pointId)) {
+ if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {
return addPoint();
}
@@ -131,14 +131,14 @@ export function uiIntroPoint(context, reveal) {
);
context.on('enter.intro', function(mode) {
- if (!pointId || !context.hasEntity(pointId)) {
+ if (!_pointID || !context.hasEntity(_pointID)) {
return continueTo(addPoint);
}
var ids = context.selectedIDs();
- if (mode.id !== 'select' || !ids.length || ids[0] !== pointId) {
+ if (mode.id !== 'select' || !ids.length || ids[0] !== _pointID) {
// keep the user's point selected..
- context.enter(modeSelect(context, [pointId]));
+ context.enter(modeSelect(context, [_pointID]));
// disallow scrolling
d3_select('.inspector-wrap').on('wheel.intro', eventCancel);
@@ -186,7 +186,7 @@ export function uiIntroPoint(context, reveal) {
function aboutFeatureEditor() {
- if (context.mode().id !== 'select' || !pointId || !context.hasEntity(pointId)) {
+ if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {
return addPoint();
}
@@ -211,7 +211,7 @@ export function uiIntroPoint(context, reveal) {
function addName() {
- if (context.mode().id !== 'select' || !pointId || !context.hasEntity(pointId)) {
+ if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {
return addPoint();
}
@@ -222,7 +222,7 @@ export function uiIntroPoint(context, reveal) {
// It's possible for the user to add a name in a previous step..
// If so, don't tell them to add the name in this step.
// Give them an OK button instead.
- var entity = context.entity(pointId);
+ var entity = context.entity(_pointID);
if (entity.tags.name) {
var tooltip = reveal('.entity-editor-pane', t('intro.points.add_name'), {
tooltipClass: 'intro-points-describe',
@@ -278,13 +278,13 @@ export function uiIntroPoint(context, reveal) {
function reselectPoint() {
- if (!pointId) return chapter.restart();
- var entity = context.hasEntity(pointId);
+ if (!_pointID) return chapter.restart();
+ var entity = context.hasEntity(_pointID);
if (!entity) return chapter.restart();
// make sure it's still a cafe, in case user somehow changed it..
var oldPreset = context.presets().match(entity, context.graph());
- context.replace(actionChangePreset(pointId, oldPreset, cafePreset));
+ context.replace(actionChangePreset(_pointID, oldPreset, cafePreset));
context.enter(modeBrowse(context));
@@ -298,7 +298,7 @@ export function uiIntroPoint(context, reveal) {
timeout(function() {
context.map().on('move.intro drawn.intro', function() {
- var entity = context.hasEntity(pointId);
+ var entity = context.hasEntity(_pointID);
if (!entity) return chapter.restart();
var box = pointBox(entity.loc, context);
reveal(box, t('intro.points.reselect'), { duration: 0 });
@@ -321,7 +321,7 @@ export function uiIntroPoint(context, reveal) {
function updatePoint() {
- if (context.mode().id !== 'select' || !pointId || !context.hasEntity(pointId)) {
+ if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {
return continueTo(reselectPoint);
}
@@ -351,7 +351,7 @@ export function uiIntroPoint(context, reveal) {
function updateCloseEditor() {
- if (context.mode().id !== 'select' || !pointId || !context.hasEntity(pointId)) {
+ if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {
return continueTo(reselectPoint);
}
@@ -376,8 +376,8 @@ export function uiIntroPoint(context, reveal) {
function rightClickPoint() {
- if (!pointId) return chapter.restart();
- var entity = context.hasEntity(pointId);
+ if (!_pointID) return chapter.restart();
+ var entity = context.hasEntity(_pointID);
if (!entity) return chapter.restart();
context.enter(modeBrowse(context));
@@ -387,7 +387,7 @@ export function uiIntroPoint(context, reveal) {
timeout(function() {
context.map().on('move.intro drawn.intro', function() {
- var entity = context.hasEntity(pointId);
+ var entity = context.hasEntity(_pointID);
if (!entity) return chapter.restart();
var box = pointBox(entity.loc, context);
reveal(box, t('intro.points.rightclick'), { duration: 0 });
@@ -397,7 +397,7 @@ export function uiIntroPoint(context, reveal) {
context.on('enter.intro', function(mode) {
if (mode.id !== 'select') return;
var ids = context.selectedIDs();
- if (ids.length !== 1 || ids[0] !== pointId) return;
+ if (ids.length !== 1 || ids[0] !== _pointID) return;
timeout(function() {
var node = selectMenuItem('delete').node();
@@ -415,8 +415,8 @@ export function uiIntroPoint(context, reveal) {
function enterDelete() {
- if (!pointId) return chapter.restart();
- var entity = context.hasEntity(pointId);
+ if (!_pointID) return chapter.restart();
+ var entity = context.hasEntity(_pointID);
if (!entity) return chapter.restart();
var node = selectMenuItem('delete').node();
@@ -436,8 +436,8 @@ export function uiIntroPoint(context, reveal) {
}, 300); // after menu visible
context.on('exit.intro', function() {
- if (!pointId) return chapter.restart();
- var entity = context.hasEntity(pointId);
+ if (!_pointID) return chapter.restart();
+ var entity = context.hasEntity(_pointID);
if (entity) return continueTo(rightClickPoint); // point still exists
});
diff --git a/modules/ui/intro/welcome.js b/modules/ui/intro/welcome.js
index a33de0e25..ec657d6e2 100644
--- a/modules/ui/intro/welcome.js
+++ b/modules/ui/intro/welcome.js
@@ -9,8 +9,8 @@ import { utilRebind } from '../../util/rebind';
export function uiIntroWelcome(context, reveal) {
- var dispatch = d3_dispatch('done'),
- listener = clickListener();
+ var dispatch = d3_dispatch('done');
+ var listener = clickListener();
var chapter = {
title: 'intro.welcome.title'
@@ -49,8 +49,8 @@ export function uiIntroWelcome(context, reveal) {
function leftClick() {
- var counter = 0,
- times = 5;
+ var counter = 0;
+ var times = 5;
var tooltip = reveal('.intro-nav-wrap .chapter-welcome',
t('intro.welcome.leftclick', { num: times }),
@@ -90,8 +90,8 @@ export function uiIntroWelcome(context, reveal) {
function rightClick() {
- var counter = 0,
- times = 5;
+ var counter = 0;
+ var times = 5;
var tooltip = reveal('.intro-nav-wrap .chapter-welcome',
t('intro.welcome.rightclick', { num: times }),
@@ -163,10 +163,10 @@ export function uiIntroWelcome(context, reveal) {
function clickListener() {
- var dispatch = d3_dispatch('click'),
- minTime = 120,
- tooltip = d3_select(null),
- down = {};
+ var dispatch = d3_dispatch('click');
+ var minTime = 120;
+ var tooltip = d3_select(null);
+ var down = {};
// `down` keeps track of which buttons/keys are down.
// Setting a property in `down` happens immediately.
@@ -187,9 +187,9 @@ function clickListener() {
if (d3_event.keyCode === 93) { // context menu
d3_event.preventDefault();
d3_event.stopPropagation();
- var endTime = d3_event.timeStamp,
- startTime = down.menu || endTime,
- delay = (endTime - startTime < minTime) ? minTime : 0;
+ var endTime = d3_event.timeStamp;
+ var startTime = down.menu || endTime;
+ var delay = (endTime - startTime < minTime) ? minTime : 0;
window.setTimeout(function() {
tooltip.classed('rightclick', false);
@@ -213,10 +213,10 @@ function clickListener() {
function mouseup() {
- var button = d3_event.button,
- endTime = d3_event.timeStamp,
- startTime = down[button] || endTime,
- delay = (endTime - startTime < minTime) ? minTime : 0;
+ var button = d3_event.button;
+ var endTime = d3_event.timeStamp;
+ var startTime = down[button] || endTime;
+ var delay = (endTime - startTime < minTime) ? minTime : 0;
if (button === 0 && !d3_event.ctrlKey) {
window.setTimeout(function() {
diff --git a/modules/ui/map_data.js b/modules/ui/map_data.js
index 149f4693f..369f8554f 100644
--- a/modules/ui/map_data.js
+++ b/modules/ui/map_data.js
@@ -8,9 +8,12 @@ import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js';
import { svgIcon } from '../svg';
import { t, textDirection } from '../util/locale';
import { tooltip } from '../util/tooltip';
+import { geoExtent } from '../geo';
+import { modeBrowse } from '../modes';
import { uiBackground } from './background';
import { uiDisclosure } from './disclosure';
import { uiHelp } from './help';
+import { uiSettingsCustomData } from './settings/custom_data';
import { uiTooltipHtml } from './tooltipHtml';
@@ -20,6 +23,9 @@ export function uiMapData(context) {
var layers = context.layers();
var fills = ['wireframe', 'partial', 'full'];
+ var settingsCustomData = uiSettingsCustomData(context)
+ .on('change', customChanged);
+
var _fillSelected = context.storage('area-fill') || 'partial';
var _shown = false;
var _dataLayerContainer = d3_select(null);
@@ -75,6 +81,11 @@ export function uiMapData(context) {
var layer = layers.layer(which);
if (layer) {
layer.enabled(enabled);
+
+ if (!enabled && (which === 'osm' || which === 'notes')) {
+ context.enter(modeBrowse(context));
+ }
+
update();
}
}
@@ -137,10 +148,8 @@ export function uiMapData(context) {
// Update
- li = li
- .merge(liEnter);
-
li
+ .merge(liEnter)
.classed('active', layerEnabled)
.selectAll('input')
.property('checked', layerEnabled);
@@ -191,24 +200,135 @@ export function uiMapData(context) {
// Update
- li = li
- .merge(liEnter);
-
li
+ .merge(liEnter)
.classed('active', function (d) { return d.layer.enabled(); })
.selectAll('input')
.property('checked', function (d) { return d.layer.enabled(); });
}
- function drawGpxItem(selection) {
- var gpx = layers.layer('gpx');
- var hasGpx = gpx && gpx.hasGpx();
- var showsGpx = hasGpx && gpx.enabled();
+ // Beta feature - sample vector layers to support Detroit Mapping Challenge
+ // https://github.com/osmus/detroit-mapping-challenge
+ function drawVectorItems(selection) {
+ var dataLayer = layers.layer('data');
+ var vtData = [
+ {
+ name: 'Detroit Neighborhoods/Parks',
+ src: 'neighborhoods-parks',
+ tooltip: 'Neighborhood boundaries and parks as compiled by City of Detroit in concert with community groups.',
+ template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmur6x34562qp9iv1u3ksf-54hev,jonahadkins.cjksmqxdx33jj2wp90xd9x2md-4e5y2/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA'
+ }, {
+ name: 'Detroit Composite POIs',
+ src: 'composite-poi',
+ tooltip: 'Fire Inspections, Business Licenses, and other public location data collated from the City of Detroit.',
+ template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmm6a02sli31myxhsr7zf3-2sw8h/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA'
+ }, {
+ name: 'Detroit All-The-Places POIs',
+ src: 'alltheplaces-poi',
+ tooltip: 'Public domain business location data created by web scrapers.',
+ template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmswgk340g2vo06p1w9w0j-8fjjc/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA'
+ }
+ ];
+
+ // Only show this if the map is around Detroit..
+ var detroit = geoExtent([-83.5, 42.1], [-82.8, 42.5]);
+ var showVectorItems = (context.map().zoom() > 9 && detroit.contains(context.map().center()));
+
+ var container = selection.selectAll('.vectortile-container')
+ .data(showVectorItems ? [0] : []);
+
+ container.exit()
+ .remove();
+
+ var containerEnter = container.enter()
+ .append('div')
+ .attr('class', 'vectortile-container');
+
+ containerEnter
+ .append('h4')
+ .attr('class', 'vectortile-header')
+ .text('Detroit Vector Tiles (Beta)');
+
+ containerEnter
+ .append('ul')
+ .attr('class', 'layer-list layer-list-vectortile');
+
+ containerEnter
+ .append('div')
+ .attr('class', 'vectortile-footer')
+ .append('a')
+ .attr('target', '_blank')
+ .attr('tabindex', -1)
+ .call(svgIcon('#iD-icon-out-link', 'inline'))
+ .attr('href', 'https://github.com/osmus/detroit-mapping-challenge')
+ .append('span')
+ .text('About these layers');
+
+ container = container
+ .merge(containerEnter);
+
+
+ var ul = container.selectAll('.layer-list-vectortile');
+
+ var li = ul.selectAll('.list-item')
+ .data(vtData);
+
+ li.exit()
+ .remove();
+
+ var liEnter = li.enter()
+ .append('li')
+ .attr('class', function(d) { return 'list-item list-item-' + d.src; });
+
+ var labelEnter = liEnter
+ .append('label')
+ .each(function(d) {
+ d3_select(this).call(
+ tooltip().title(d.tooltip).placement('top')
+ );
+ });
+
+ labelEnter
+ .append('input')
+ .attr('type', 'radio')
+ .attr('name', 'vectortile')
+ .on('change', selectVTLayer);
+
+ labelEnter
+ .append('span')
+ .text(function(d) { return d.name; });
+
+ // Update
+ li
+ .merge(liEnter)
+ .classed('active', isVTLayerSelected)
+ .selectAll('input')
+ .property('checked', isVTLayerSelected);
+
+
+ function isVTLayerSelected(d) {
+ return dataLayer && dataLayer.template() === d.template;
+ }
+
+ function selectVTLayer(d) {
+ context.storage('settings-custom-data-url', d.template);
+ if (dataLayer) {
+ dataLayer.template(d.template, d.src);
+ dataLayer.enabled(true);
+ }
+ }
+ }
+
+
+ function drawCustomDataItems(selection) {
+ var dataLayer = layers.layer('data');
+ var hasData = dataLayer && dataLayer.hasData();
+ var showsData = hasData && dataLayer.enabled();
var ul = selection
- .selectAll('.layer-list-gpx')
- .data(gpx ? [0] : []);
+ .selectAll('.layer-list-data')
+ .data(dataLayer ? [0] : []);
// Exit
ul.exit()
@@ -217,154 +337,82 @@ export function uiMapData(context) {
// Enter
var ulEnter = ul.enter()
.append('ul')
- .attr('class', 'layer-list layer-list-gpx');
+ .attr('class', 'layer-list layer-list-data');
var liEnter = ulEnter
.append('li')
- .attr('class', 'list-item-gpx');
+ .attr('class', 'list-item-data');
liEnter
.append('button')
- .attr('class', 'list-item-gpx-extent')
.call(tooltip()
- .title(t('gpx.zoom'))
+ .title(t('settings.custom_data.tooltip'))
+ .placement((textDirection === 'rtl') ? 'right' : 'left')
+ )
+ .on('click', editCustom)
+ .call(svgIcon('#iD-icon-more'));
+
+ liEnter
+ .append('button')
+ .call(tooltip()
+ .title(t('map_data.layers.custom.zoom'))
.placement((textDirection === 'rtl') ? 'right' : 'left')
)
.on('click', function() {
d3_event.preventDefault();
d3_event.stopPropagation();
- gpx.fitZoom();
+ dataLayer.fitZoom();
})
.call(svgIcon('#iD-icon-search'));
- liEnter
- .append('button')
- .attr('class', 'list-item-gpx-browse')
- .call(tooltip()
- .title(t('gpx.browse'))
- .placement((textDirection === 'rtl') ? 'right' : 'left')
- )
- .on('click', function() {
- d3_select(document.createElement('input'))
- .attr('type', 'file')
- .on('change', function() {
- gpx.files(d3_event.target.files);
- })
- .node().click();
- })
- .call(svgIcon('#iD-icon-geolocate'));
-
var labelEnter = liEnter
.append('label')
.call(tooltip()
- .title(t('gpx.drag_drop'))
+ .title(t('map_data.layers.custom.tooltip'))
.placement('top')
);
labelEnter
.append('input')
.attr('type', 'checkbox')
- .on('change', function() { toggleLayer('gpx'); });
+ .on('change', function() { toggleLayer('data'); });
labelEnter
.append('span')
- .text(t('gpx.local_layer'));
+ .text(t('map_data.layers.custom.title'));
// Update
ul = ul
.merge(ulEnter);
- ul.selectAll('.list-item-gpx')
- .classed('active', showsGpx)
+ ul.selectAll('.list-item-data')
+ .classed('active', showsData)
.selectAll('label')
- .classed('deemphasize', !hasGpx)
+ .classed('deemphasize', !hasData)
.selectAll('input')
- .property('disabled', !hasGpx)
- .property('checked', showsGpx);
+ .property('disabled', !hasData)
+ .property('checked', showsData);
}
- function drawMvtItem(selection) {
- var mvt = layers.layer('mvt'),
- hasMvt = mvt && mvt.hasMvt(),
- showsMvt = hasMvt && mvt.enabled();
- var ul = selection
- .selectAll('.layer-list-mvt')
- .data(mvt ? [0] : []);
-
- // Exit
- ul.exit()
- .remove();
-
- // Enter
- var ulEnter = ul.enter()
- .append('ul')
- .attr('class', 'layer-list layer-list-mvt');
-
- var liEnter = ulEnter
- .append('li')
- .attr('class', 'list-item-mvt');
-
- liEnter
- .append('button')
- .attr('class', 'list-item-mvt-extent')
- .call(tooltip()
- .title(t('mvt.zoom'))
- .placement((textDirection === 'rtl') ? 'right' : 'left')
- )
- .on('click', function() {
- d3_event.preventDefault();
- d3_event.stopPropagation();
- mvt.fitZoom();
- })
- .call(svgIcon('#iD-icon-search'));
-
- liEnter
- .append('button')
- .attr('class', 'list-item-mvt-browse')
- .call(tooltip()
- .title(t('mvt.browse'))
- .placement((textDirection === 'rtl') ? 'right' : 'left')
- )
- .on('click', function() {
- d3_select(document.createElement('input'))
- .attr('type', 'file')
- .on('change', function() {
- mvt.files(d3_event.target.files);
- })
- .node().click();
- })
- .call(svgIcon('#iD-icon-geolocate'));
-
- var labelEnter = liEnter
- .append('label')
- .call(tooltip()
- .title(t('mvt.drag_drop'))
- .placement('top')
- );
-
- labelEnter
- .append('input')
- .attr('type', 'checkbox')
- .on('change', function() { toggleLayer('mvt'); });
-
- labelEnter
- .append('span')
- .text(t('mvt.local_layer'));
-
- // Update
- ul = ul
- .merge(ulEnter);
-
- ul.selectAll('.list-item-mvt')
- .classed('active', showsMvt)
- .selectAll('label')
- .classed('deemphasize', !hasMvt)
- .selectAll('input')
- .property('disabled', !hasMvt)
- .property('checked', showsMvt);
+ function editCustom() {
+ d3_event.preventDefault();
+ context.container()
+ .call(settingsCustomData);
}
+
+ function customChanged(d) {
+ var dataLayer = layers.layer('data');
+
+ if (d && d.url) {
+ dataLayer.url(d.url);
+ } else if (d && d.fileList) {
+ dataLayer.fileList(d.fileList);
+ }
+ }
+
+
function drawListItems(selection, data, type, name, change, active) {
var items = selection.selectAll('li')
.data(data);
@@ -456,8 +504,8 @@ export function uiMapData(context) {
_dataLayerContainer
.call(drawOsmItems)
.call(drawPhotoItems)
- .call(drawGpxItem);
- // .call(drawMvtItem);
+ .call(drawCustomDataItems)
+ .call(drawVectorItems); // Beta - Detroit mapping challenge
_fillList
.call(drawListItems, fills, 'radio', 'area_fill', setFill, showsFill);
diff --git a/modules/ui/map_in_map.js b/modules/ui/map_in_map.js
index 3e056e4b0..139f41f6c 100644
--- a/modules/ui/map_in_map.js
+++ b/modules/ui/map_in_map.js
@@ -22,7 +22,7 @@ import {
} from '../geo';
import { rendererTileLayer } from '../renderer';
-import { svgDebug, svgGpx } from '../svg';
+import { svgDebug, svgData } from '../svg';
import { utilSetTransform } from '../util';
import { utilGetDimensions } from '../util/dimensions';
@@ -33,7 +33,7 @@ export function uiMapInMap(context) {
var backgroundLayer = rendererTileLayer(context);
var overlayLayers = {};
var projection = geoRawMercator();
- var gpxLayer = svgGpx(projection, context).showLabels(false);
+ var dataLayer = svgData(projection, context).showLabels(false);
var debugLayer = svgDebug(projection, context);
var zoom = d3_zoom()
.scaleExtent([geoZoomToScale(0.5), geoZoomToScale(24)])
@@ -242,7 +242,7 @@ export function uiMapInMap(context) {
.append('svg')
.attr('class', 'map-in-map-data')
.merge(dataLayers)
- .call(gpxLayer)
+ .call(dataLayer)
.call(debugLayer);
diff --git a/modules/ui/modes.js b/modules/ui/modes.js
index d68fa5b72..fb2e35aac 100644
--- a/modules/ui/modes.js
+++ b/modules/ui/modes.js
@@ -1,8 +1,7 @@
import _debounce from 'lodash-es/debounce';
import {
- select as d3_select,
- selectAll as d3_selectAll
+ select as d3_select
} from 'd3-selection';
import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js';
@@ -10,6 +9,7 @@ import {
modeAddArea,
modeAddLine,
modeAddPoint,
+ modeAddNote,
modeBrowse
} from '../modes';
@@ -17,65 +17,32 @@ import { svgIcon } from '../svg';
import { tooltip } from '../util/tooltip';
import { uiTooltipHtml } from './tooltipHtml';
+import _includes from 'lodash-es/includes';
export function uiModes(context) {
var modes = [
modeAddPoint(context),
modeAddLine(context),
- modeAddArea(context)
+ modeAddArea(context),
+ modeAddNote(context)
];
-
function editable() {
var mode = context.mode();
return context.editable() && mode && mode.id !== 'save';
}
- function difference(mode) {
- var match = mode.attr('class');
- var defaultIndex = context.presets().defaultTypes().findIndex(function(d) { return new RegExp(d).test(match); });
- return defaultIndex === -1;
+ function notesEnabled() {
+ var noteLayer = context.layers().layer('notes');
+ return noteLayer && noteLayer.enabled();
}
+ function notesEditable() {
+ var mode = context.mode();
+ return context.map().notesEditable() && mode && mode.id !== 'save';
+ }
return function(selection) {
- var buttons = selection.selectAll('button.add-button')
- .data(modes);
-
- buttons = buttons.enter()
- .append('button')
- .attr('tabindex', -1)
- .attr('class', function(mode) { return mode.id + ' add-button col4'; })
- .on('click.mode-buttons', function(mode) {
- // When drawing, ignore accidental clicks on mode buttons - #4042
- var currMode = context.mode().id;
- if (currMode.match(/^draw/) !== null) return;
-
- if (mode.id === currMode) {
- context.enter(modeBrowse(context));
- } else {
- context.enter(mode);
- }
- })
- .call(tooltip()
- .placement('bottom')
- .html(true)
- .title(function(mode) {
- return uiTooltipHtml(mode.description, mode.key);
- })
- );
-
- buttons
- .each(function(d) {
- d3_select(this)
- .call(svgIcon('#iD-icon-' + d.button, 'pre-text'));
- });
-
- buttons
- .append('span')
- .attr('class', 'label')
- .text(function(mode) { return mode.title; });
-
context
.on('enter.editor', function(entered) {
selection.selectAll('button.add-button')
@@ -94,12 +61,13 @@ export function uiModes(context) {
modes.forEach(function(mode) {
keybinding.on(mode.key, function() {
- if (editable()) {
- if (mode.id === context.mode().id) {
- context.enter(modeBrowse(context));
- } else {
- context.enter(mode);
- }
+ if (mode.id === 'add-note' && !(notesEnabled() && notesEditable())) return;
+ if (mode.id !== 'add-note' && !editable()) return;
+
+ if (mode.id === context.mode().id) {
+ context.enter(modeBrowse(context));
+ } else {
+ context.enter(mode);
}
});
});
@@ -117,15 +85,67 @@ export function uiModes(context) {
context
.on('enter.modes', update);
+ update();
function update() {
- var modes = selection.selectAll('button.add-button');
- modes.property('disabled', !editable());
- d3_selectAll('button.add-button').each(function (d) {
- var mode = d3_select(this);
- mode.property('disabled', difference(mode));
- });
+ var showNotes = notesEnabled();
+ var data = showNotes ? modes : modes.slice(0, 3);
+
+ selection
+ .classed('col3', !showNotes) // 25%
+ .classed('col4', showNotes); // 33%
+
+ var buttons = selection.selectAll('button.add-button')
+ .data(data, function(d) { return d.id; });
+
+ // exit
+ buttons.exit()
+ .remove();
+
+ // enter
+ var buttonsEnter = buttons.enter()
+ .append('button')
+ .attr('tabindex', -1)
+ .attr('class', function(d) { return d.id + ' add-button'; })
+ .on('click.mode-buttons', function(mode) {
+ // When drawing, ignore accidental clicks on mode buttons - #4042
+ var currMode = context.mode().id;
+ if (currMode.match(/^draw/) !== null) return;
+
+ if (mode.id === currMode) {
+ context.enter(modeBrowse(context));
+ } else {
+ context.enter(mode);
+ }
+ })
+ .call(tooltip()
+ .placement('bottom')
+ .html(true)
+ .title(function(mode) {
+ return uiTooltipHtml(mode.description, mode.key);
+ })
+ );
+
+ buttonsEnter
+ .each(function(d) {
+ d3_select(this)
+ .call(svgIcon('#iD-icon-' + d.button, 'pre-text'));
+ });
+
+ buttonsEnter
+ .append('span')
+ .attr('class', 'label')
+ .text(function(mode) { return mode.title; });
+
+ // update
+ buttons = buttons
+ .merge(buttonsEnter)
+ .classed('col3', showNotes) // 25%
+ .classed('col4', !showNotes) // 33%
+ .property('disabled', function(d) {
+ return d.id === 'add-note' ? !notesEditable() : !editable();
+ });
}
};
}
diff --git a/modules/ui/note_comments.js b/modules/ui/note_comments.js
index a9890779c..4534ea10b 100644
--- a/modules/ui/note_comments.js
+++ b/modules/ui/note_comments.js
@@ -11,6 +11,8 @@ export function uiNoteComments() {
function noteComments(selection) {
+ if (_note.isNew()) return; // don't draw .comments-container
+
var comments = selection.selectAll('.comments-container')
.data([0]);
@@ -59,12 +61,14 @@ export function uiNoteComments() {
metadataEnter
.append('div')
.attr('class', 'comment-date')
- .text(function(d) { return d.action + ' ' + localeDateString(d.date); });
+ .text(function(d) {
+ return t('note.status.' + d.action, { when: localeDateString(d.date) });
+ });
mainEnter
.append('div')
.attr('class', 'comment-text')
- .text(function(d) { return d.text; });
+ .html(function(d) { return d.html; });
comments
.call(replaceAvatars);
@@ -99,6 +103,7 @@ export function uiNoteComments() {
if (!s) return null;
var detected = utilDetect();
var options = { day: 'numeric', month: 'short', year: 'numeric' };
+ s = s.replace(/-/g, '/'); // fix browser-specific Date() issues
var d = new Date(s);
if (isNaN(d.getTime())) return null;
return d.toLocaleDateString(detected.locale, options);
diff --git a/modules/ui/note_editor.js b/modules/ui/note_editor.js
index 1d96d80f1..4ebc6e89d 100644
--- a/modules/ui/note_editor.js
+++ b/modules/ui/note_editor.js
@@ -9,6 +9,9 @@ import { services } from '../services';
import { modeBrowse } from '../modes';
import { svgIcon } from '../svg';
+// import { uiField } from './field';
+// import { uiFormFields } from './form_fields';
+
import {
uiNoteComments,
uiNoteHeader,
@@ -26,7 +29,11 @@ export function uiNoteEditor(context) {
var dispatch = d3_dispatch('change');
var noteComments = uiNoteComments();
var noteHeader = uiNoteHeader();
+
+ // var formFields = uiFormFields(context);
+
var _note;
+ // var _fieldsArr;
function noteEditor(selection) {
@@ -40,7 +47,9 @@ export function uiNoteEditor(context) {
headerEnter
.append('button')
.attr('class', 'fr note-editor-close')
- .on('click', function() { context.enter(modeBrowse(context)); })
+ .on('click', function() {
+ context.enter(modeBrowse(context));
+ })
.call(svgIcon('#iD-icon-close'));
headerEnter
@@ -59,7 +68,7 @@ export function uiNoteEditor(context) {
var editor = body.selectAll('.note-editor')
.data([0]);
- editor = editor.enter()
+ editor.enter()
.append('div')
.attr('class', 'modal-section note-editor')
.merge(editor)
@@ -71,7 +80,7 @@ export function uiNoteEditor(context) {
var footer = selection.selectAll('.footer')
.data([0]);
- footer = footer.enter()
+ footer.enter()
.append('div')
.attr('class', 'footer')
.merge(footer)
@@ -103,10 +112,46 @@ export function uiNoteEditor(context) {
.append('div')
.attr('class', 'note-save save-section cf');
+ // // if new note, show categories to pick from
+ // if (_note.isNew()) {
+ // var presets = context.presets();
+
+ // // NOTE: this key isn't a age and therefore there is no documentation (yet)
+ // _fieldsArr = [
+ // uiField(context, presets.field('category'), null, { show: true, revert: false }),
+ // ];
+
+ // _fieldsArr.forEach(function(field) {
+ // field
+ // .on('change', changeCategory);
+ // });
+
+ // noteSaveEnter
+ // .append('div')
+ // .attr('class', 'note-category')
+ // .call(formFields.fieldsArr(_fieldsArr));
+ // }
+
+ // function changeCategory() {
+ // // NOTE: perhaps there is a better way to get value
+ // var val = d3_select('input[name=\'category\']:checked').property('__data__') || undefined;
+
+ // // store the unsaved category with the note itself
+ // _note = _note.update({ newCategory: val });
+ // var osm = services.osm;
+ // if (osm) {
+ // osm.replaceNote(_note); // update note cache
+ // }
+ // noteSave
+ // .call(noteSaveButtons);
+ // }
+
noteSaveEnter
.append('h4')
- .attr('class', 'note-save-header')
- .text(t('note.newComment'));
+ .attr('class', '.note-save-header')
+ .text(function() {
+ return _note.isNew() ? t('note.newDescription') : t('note.newComment');
+ });
noteSaveEnter
.append('textarea')
@@ -115,8 +160,9 @@ export function uiNoteEditor(context) {
.attr('maxlength', 1000)
.property('value', function(d) { return d.newComment; })
.call(utilNoAuto)
- .on('input', change)
- .on('blur', change);
+ .on('keydown.note-input', keydown)
+ .on('input.note-input', changeInput)
+ .on('blur.note-input', changeInput);
// update
noteSave = noteSaveEnter
@@ -125,7 +171,37 @@ export function uiNoteEditor(context) {
.call(noteSaveButtons);
- function change() {
+ // fast submit if user presses cmd+enter
+ function keydown() {
+ if (!(d3_event.keyCode === 13 && d3_event.metaKey)) return;
+
+ var osm = services.osm;
+ if (!osm) return;
+
+ var hasAuth = osm.authenticated();
+ if (!hasAuth) return;
+
+ if (!_note.newComment) return;
+
+ d3_event.preventDefault();
+
+ d3_select(this)
+ .on('keydown.note-input', null);
+
+ // focus on button and submit
+ window.setTimeout(function() {
+ if (_note.isNew()) {
+ noteSave.selectAll('.save-button').node().focus();
+ clickSave(_note);
+ } else {
+ noteSave.selectAll('.comment-button').node().focus();
+ clickComment(_note);
+ }
+ }, 10);
+ }
+
+
+ function changeInput() {
var input = d3_select(this);
var val = input.property('value').trim() || undefined;
@@ -250,23 +326,40 @@ export function uiNoteEditor(context) {
.append('div')
.attr('class', 'buttons');
- buttonEnter
- .append('button')
- .attr('class', 'button status-button action')
- .append('span')
- .attr('class', 'label');
+ if (_note.isNew()) {
+ buttonEnter
+ .append('button')
+ .attr('class', 'button cancel-button secondary-action')
+ .text(t('confirm.cancel'));
+
+ buttonEnter
+ .append('button')
+ .attr('class', 'button save-button action')
+ .text(t('note.save'));
+
+ } else {
+ buttonEnter
+ .append('button')
+ .attr('class', 'button status-button action');
+
+ buttonEnter
+ .append('button')
+ .attr('class', 'button comment-button action')
+ .text(t('note.comment'));
+ }
- buttonEnter
- .append('button')
- .attr('class', 'button comment-button action')
- .append('span')
- .attr('class', 'label')
- .text(t('note.comment'));
// update
buttonSection = buttonSection
.merge(buttonEnter);
+ buttonSection.select('.cancel-button') // select and propagate data
+ .on('click.cancel', clickCancel);
+
+ buttonSection.select('.save-button') // select and propagate data
+ .attr('disabled', isSaveDisabled)
+ .on('click.save', clickSave);
+
buttonSection.select('.status-button') // select and propagate data
.attr('disabled', (hasAuth ? null : true))
.text(function(d) {
@@ -274,30 +367,61 @@ export function uiNoteEditor(context) {
var andComment = (d.newComment ? '_comment' : '');
return t('note.' + action + andComment);
})
- .on('click.status', function(d) {
- this.blur(); // avoid keeping focus on the button - #4641
- var osm = services.osm;
- if (osm) {
- var setStatus = (d.status === 'open' ? 'closed' : 'open');
- osm.postNoteUpdate(d, setStatus, function(err, note) {
- dispatch.call('change', note);
- });
- }
- });
+ .on('click.status', clickStatus);
buttonSection.select('.comment-button') // select and propagate data
- .attr('disabled', function(d) {
- return (hasAuth && d.status === 'open' && d.newComment) ? null : true;
- })
- .on('click.save', function(d) {
- this.blur(); // avoid keeping focus on the button - #4641
- var osm = services.osm;
- if (osm) {
- osm.postNoteUpdate(d, d.status, function(err, note) {
- dispatch.call('change', note);
- });
- }
+ .attr('disabled', isSaveDisabled)
+ .on('click.comment', clickComment);
+
+
+ function isSaveDisabled(d) {
+ return (hasAuth && d.status === 'open' && d.newComment) ? null : true;
+ }
+ }
+
+
+
+ function clickCancel(d) {
+ this.blur(); // avoid keeping focus on the button - #4641
+ var osm = services.osm;
+ if (osm) {
+ osm.removeNote(d);
+ }
+ context.enter(modeBrowse(context));
+ dispatch.call('change');
+ }
+
+
+ function clickSave(d) {
+ this.blur(); // avoid keeping focus on the button - #4641
+ var osm = services.osm;
+ if (osm) {
+ osm.postNoteCreate(d, function(err, note) {
+ dispatch.call('change', note);
});
+ }
+ }
+
+
+ function clickStatus(d) {
+ this.blur(); // avoid keeping focus on the button - #4641
+ var osm = services.osm;
+ if (osm) {
+ var setStatus = (d.status === 'open' ? 'closed' : 'open');
+ osm.postNoteUpdate(d, setStatus, function(err, note) {
+ dispatch.call('change', note);
+ });
+ }
+ }
+
+ function clickComment(d) {
+ this.blur(); // avoid keeping focus on the button - #4641
+ var osm = services.osm;
+ if (osm) {
+ osm.postNoteUpdate(d, d.status, function(err, note) {
+ dispatch.call('change', note);
+ });
+ }
}
diff --git a/modules/ui/note_header.js b/modules/ui/note_header.js
index dcaf82d46..8fc2a0e95 100644
--- a/modules/ui/note_header.js
+++ b/modules/ui/note_header.js
@@ -22,7 +22,8 @@ export function uiNoteHeader() {
var iconEnter = headerEnter
.append('div')
- .attr('class', function(d) { return 'note-header-icon ' + d.status; });
+ .attr('class', function(d) { return 'note-header-icon ' + d.status; })
+ .classed('new', function(d) { return d.id < 0; });
iconEnter
.append('div')
@@ -30,18 +31,18 @@ export function uiNoteHeader() {
.call(svgIcon('#iD-icon-note', 'note-fill'));
iconEnter.each(function(d) {
- if (d.comments.length > 1) {
- iconEnter
- .append('div')
- .attr('class', 'note-icon-annotation')
- .call(svgIcon('#iD-icon-more', 'note-annotation'));
- }
+ var statusIcon = '#iD-icon-' + (d.id < 0 ? 'plus' : (d.status === 'open' ? 'close' : 'apply'));
+ iconEnter
+ .append('div')
+ .attr('class', 'note-icon-annotation')
+ .call(svgIcon(statusIcon, 'note-annotation'));
});
headerEnter
.append('div')
.attr('class', 'note-header-label')
.text(function(d) {
+ if (_note.isNew()) { return t('note.new'); }
return t('note.note') + ' ' + d.id + ' ' +
(d.status === 'closed' ? t('note.closed') : '');
});
diff --git a/modules/ui/panels/history.js b/modules/ui/panels/history.js
index fef7c8804..133955577 100644
--- a/modules/ui/panels/history.js
+++ b/modules/ui/panels/history.js
@@ -8,21 +8,21 @@ import { utilDetect } from '../../util/detect';
export function uiPanelHistory(context) {
var osm;
- function displayTimestamp(entity) {
- if (!entity.timestamp) return t('info_panels.history.unknown');
+ function displayTimestamp(timestamp) {
+ if (!timestamp) return t('info_panels.history.unknown');
var detected = utilDetect();
var options = {
day: 'numeric', month: 'short', year: 'numeric',
hour: 'numeric', minute: 'numeric', second: 'numeric'
};
- var d = new Date(entity.timestamp);
+ var d = new Date(timestamp);
if (isNaN(d.getTime())) return t('info_panels.history.unknown');
return d.toLocaleString(detected.locale, options);
}
- function displayUser(selection, entity) {
- if (!entity.user) {
+ function displayUser(selection, userName) {
+ if (!userName) {
selection
.append('span')
.text(t('info_panels.history.unknown'));
@@ -32,7 +32,7 @@ export function uiPanelHistory(context) {
selection
.append('span')
.attr('class', 'user-name')
- .text(entity.user);
+ .text(userName);
var links = selection
.append('div')
@@ -42,7 +42,7 @@ export function uiPanelHistory(context) {
links
.append('a')
.attr('class', 'user-osm-link')
- .attr('href', osm.userURL(entity.user))
+ .attr('href', osm.userURL(userName))
.attr('target', '_blank')
.attr('tabindex', -1)
.text('OSM');
@@ -51,15 +51,15 @@ export function uiPanelHistory(context) {
links
.append('a')
.attr('class', 'user-hdyc-link')
- .attr('href', 'https://hdyc.neis-one.org/?' + entity.user)
+ .attr('href', 'https://hdyc.neis-one.org/?' + userName)
.attr('target', '_blank')
.attr('tabindex', -1)
.text('HDYC');
}
- function displayChangeset(selection, entity) {
- if (!entity.changeset) {
+ function displayChangeset(selection, changeset) {
+ if (!changeset) {
selection
.append('span')
.text(t('info_panels.history.unknown'));
@@ -69,7 +69,7 @@ export function uiPanelHistory(context) {
selection
.append('span')
.attr('class', 'changeset-id')
- .text(entity.changeset);
+ .text(changeset);
var links = selection
.append('div')
@@ -79,7 +79,7 @@ export function uiPanelHistory(context) {
links
.append('a')
.attr('class', 'changeset-osm-link')
- .attr('href', osm.changesetURL(entity.changeset))
+ .attr('href', osm.changesetURL(changeset))
.attr('target', '_blank')
.attr('tabindex', -1)
.text('OSM');
@@ -88,7 +88,7 @@ export function uiPanelHistory(context) {
links
.append('a')
.attr('class', 'changeset-osmcha-link')
- .attr('href', 'https://osmcha.mapbox.com/changesets/' + entity.changeset)
+ .attr('href', 'https://osmcha.mapbox.com/changesets/' + changeset)
.attr('target', '_blank')
.attr('tabindex', -1)
.text('OSMCha');
@@ -96,11 +96,22 @@ export function uiPanelHistory(context) {
function redraw(selection) {
- var selected = _filter(context.selectedIDs(), function(e) { return context.hasEntity(e); });
- var singular = selected.length === 1 ? selected[0] : null;
-
+ var selectedNoteID = context.selectedNoteID();
osm = context.connection();
+ var selected, note, entity;
+ if (selectedNoteID && osm) { // selected 1 note
+ selected = [ t('note.note') + ' ' + selectedNoteID ];
+ note = osm.getNote(selectedNoteID);
+ } else { // selected 1..n entities
+ selected = _filter(context.selectedIDs(), function(e) { return context.hasEntity(e); });
+ if (selected.length) {
+ entity = context.entity(selected[0]);
+ }
+ }
+
+ var singular = selected.length === 1 ? selected[0] : null;
+
selection.html('');
selection
@@ -110,9 +121,60 @@ export function uiPanelHistory(context) {
if (!singular) return;
- var entity = context.entity(singular);
+ if (entity) {
+ selection.call(redrawEntity, entity);
+ } else if (note) {
+ selection.call(redrawNote, note);
+ }
+ }
- if (!entity.version) {
+
+ function redrawNote(selection, note) {
+ if (!note || note.isNew()) {
+ selection
+ .append('div')
+ .text(t('info_panels.history.note_no_history'));
+ return;
+ }
+
+ var list = selection
+ .append('ul');
+
+ list
+ .append('li')
+ .text(t('info_panels.history.note_comments') + ':')
+ .append('span')
+ .text(note.comments.length);
+
+ if (note.comments.length) {
+ list
+ .append('li')
+ .text(t('info_panels.history.note_created_date') + ':')
+ .append('span')
+ .text(displayTimestamp(note.comments[0].date));
+
+ list
+ .append('li')
+ .text(t('info_panels.history.note_created_user') + ':')
+ .call(displayUser, note.comments[0].user);
+ }
+
+ if (osm) {
+ selection
+ .append('a')
+ .attr('class', 'view-history-on-osm')
+ .attr('target', '_blank')
+ .attr('tabindex', -1)
+ .attr('href', osm.noteURL(note))
+ .call(svgIcon('#iD-icon-out-link', 'inline'))
+ .append('span')
+ .text(t('info_panels.history.note_link_text'));
+ }
+ }
+
+
+ function redrawEntity(selection, entity) {
+ if (!entity || entity.isNew()) {
selection
.append('div')
.text(t('info_panels.history.no_history'));
@@ -132,17 +194,17 @@ export function uiPanelHistory(context) {
.append('li')
.text(t('info_panels.history.last_edit') + ':')
.append('span')
- .text(displayTimestamp(entity));
+ .text(displayTimestamp(entity.timestamp));
list
.append('li')
.text(t('info_panels.history.edited_by') + ':')
- .call(displayUser, entity);
+ .call(displayUser, entity.user);
list
.append('li')
.text(t('info_panels.history.changeset') + ':')
- .call(displayChangeset, entity);
+ .call(displayChangeset, entity.changeset);
if (osm) {
selection
@@ -165,11 +227,16 @@ export function uiPanelHistory(context) {
.on('drawn.info-history', function() {
selection.call(redraw);
});
+
+ context
+ .on('enter.info-history', function() {
+ selection.call(redraw);
+ });
};
panel.off = function() {
- context.map()
- .on('drawn.info-history', null);
+ context.map().on('drawn.info-history', null);
+ context.on('enter.info-history', null);
};
panel.id = 'history';
diff --git a/modules/ui/panels/measurement.js b/modules/ui/panels/measurement.js
index ff7369925..aabdf7641 100644
--- a/modules/ui/panels/measurement.js
+++ b/modules/ui/panels/measurement.js
@@ -11,7 +11,7 @@ import { t } from '../../util/locale';
import { displayArea, displayLength, decimalCoordinatePair, dmsCoordinatePair } from '../../util/units';
import { geoExtent } from '../../geo';
import { utilDetect } from '../../util/detect';
-
+import { services } from '../../services';
export function uiPanelMeasurement(context) {
@@ -43,21 +43,40 @@ export function uiPanelMeasurement(context) {
return result;
}
+
function nodeCount(feature) {
if (feature.type === 'LineString') return feature.coordinates.length;
-
- if (feature.type === 'Polygon') {
- return feature.coordinates[0].length - 1;
- }
+ if (feature.type === 'Polygon') return feature.coordinates[0].length - 1;
}
function redraw(selection) {
var resolver = context.graph();
- var selected = _filter(context.selectedIDs(), function(e) { return context.hasEntity(e); });
+ var selectedNoteID = context.selectedNoteID();
+ var osm = services.osm;
+
+ var selected, center, entity, note, geometry;
+
+ if (selectedNoteID && osm) { // selected 1 note
+ selected = [ t('note.note') + ' ' + selectedNoteID ];
+ note = osm.getNote(selectedNoteID);
+ center = note.loc;
+ geometry = 'note';
+
+ } else { // selected 1..n entities
+ var extent = geoExtent();
+ selected = _filter(context.selectedIDs(), function(e) { return context.hasEntity(e); });
+ if (selected.length) {
+ for (var i = 0; i < selected.length; i++) {
+ entity = context.entity(selected[i]);
+ extent._extend(entity.extent(resolver));
+ }
+ center = extent.center();
+ geometry = entity.geometry(resolver);
+ }
+ }
+
var singular = selected.length === 1 ? selected[0] : null;
- var extent = geoExtent();
- var entity;
selection.html('');
@@ -68,19 +87,12 @@ export function uiPanelMeasurement(context) {
if (!selected.length) return;
- var center;
- for (var i = 0; i < selected.length; i++) {
- entity = context.entity(selected[i]);
- extent._extend(entity.extent(resolver));
- }
- center = extent.center();
-
var list = selection
.append('ul');
var coordItem;
- // multiple features, just display extent center..
+ // multiple selected features, just display extent center..
if (!singular) {
coordItem = list
.append('li')
@@ -92,16 +104,13 @@ export function uiPanelMeasurement(context) {
return;
}
- // single feature, display details..
- if (!entity) return;
- var geometry = entity.geometry(resolver);
-
+ // single selected feature, display details..
if (geometry === 'line' || geometry === 'area') {
- var closed = (entity.type === 'relation') || (entity.isClosed() && !entity.isDegenerate()),
- feature = entity.asGeoJSON(resolver),
- length = radiansToMeters(d3_geoLength(toLineString(feature))),
- lengthLabel = t('info_panels.measurement.' + (closed ? 'perimeter' : 'length')),
- centroid = d3_geoCentroid(feature);
+ var closed = (entity.type === 'relation') || (entity.isClosed() && !entity.isDegenerate());
+ var feature = entity.asGeoJSON(resolver);
+ var length = radiansToMeters(d3_geoLength(toLineString(feature)));
+ var lengthLabel = t('info_panels.measurement.' + (closed ? 'perimeter' : 'length'));
+ var centroid = d3_geoCentroid(feature);
list
.append('li')
@@ -157,7 +166,8 @@ export function uiPanelMeasurement(context) {
});
} else {
- var centerLabel = t('info_panels.measurement.' + (entity.type === 'node' ? 'location' : 'center'));
+ var centerLabel = t('info_panels.measurement.' +
+ (note || entity.type === 'node' ? 'location' : 'center'));
list
.append('li')
@@ -183,11 +193,16 @@ export function uiPanelMeasurement(context) {
.on('drawn.info-measurement', function() {
selection.call(redraw);
});
+
+ context
+ .on('enter.info-measurement', function() {
+ selection.call(redraw);
+ });
};
panel.off = function() {
- context.map()
- .on('drawn.info-measurement', null);
+ context.map().on('drawn.info-measurement', null);
+ context.on('enter.info-measurement', null);
};
panel.id = 'measurement';
diff --git a/modules/ui/preset_editor.js b/modules/ui/preset_editor.js
index 602278507..b6ba50cc1 100644
--- a/modules/ui/preset_editor.js
+++ b/modules/ui/preset_editor.js
@@ -14,13 +14,13 @@ import { utilRebind } from '../util';
export function uiPresetEditor(context) {
- var dispatch = d3_dispatch('change'),
- formFields = uiFormFields(context),
- state,
- fieldsArr,
- preset,
- tags,
- entityId;
+ var dispatch = d3_dispatch('change');
+ var formFields = uiFormFields(context);
+ var _state;
+ var _fieldsArr;
+ var _preset;
+ var _tags;
+ var _entityID;
function presetEditor(selection) {
@@ -32,36 +32,36 @@ export function uiPresetEditor(context) {
function render(selection) {
- if (!fieldsArr) {
- var entity = context.entity(entityId),
- geometry = context.geometry(entityId),
- presets = context.presets();
+ if (!_fieldsArr) {
+ var entity = context.entity(_entityID);
+ var geometry = context.geometry(_entityID);
+ var presets = context.presets();
- fieldsArr = [];
+ _fieldsArr = [];
- preset.fields.forEach(function(field) {
+ _preset.fields.forEach(function(field) {
if (field.matchGeometry(geometry)) {
- fieldsArr.push(
+ _fieldsArr.push(
uiField(context, field, entity)
);
}
});
if (entity.isHighwayIntersection(context.graph()) && presets.field('restrictions')) {
- fieldsArr.push(
+ _fieldsArr.push(
uiField(context, presets.field('restrictions'), entity)
);
}
presets.universal().forEach(function(field) {
- if (preset.fields.indexOf(field) === -1) {
- fieldsArr.push(
+ if (_preset.fields.indexOf(field) === -1) {
+ _fieldsArr.push(
uiField(context, field, entity, { show: false })
);
}
});
- fieldsArr.forEach(function(field) {
+ _fieldsArr.forEach(function(field) {
field
.on('change', function(t, onInput) {
dispatch.call('change', field, t, onInput);
@@ -69,15 +69,15 @@ export function uiPresetEditor(context) {
});
}
- fieldsArr.forEach(function(field) {
+ _fieldsArr.forEach(function(field) {
field
- .state(state)
- .tags(tags);
+ .state(_state)
+ .tags(_tags);
});
selection
- .call(formFields.fieldsArr(fieldsArr), 'inspector-inner fillL3');
+ .call(formFields.fieldsArr(_fieldsArr), 'inspector-inner fillL3');
selection.selectAll('.wrap-form-field input')
@@ -90,35 +90,35 @@ export function uiPresetEditor(context) {
}
- presetEditor.preset = function(_) {
- if (!arguments.length) return preset;
- if (preset && preset.id === _.id) return presetEditor;
- preset = _;
- fieldsArr = null;
+ presetEditor.preset = function(val) {
+ if (!arguments.length) return _preset;
+ if (_preset && _preset.id === val.id) return presetEditor;
+ _preset = val;
+ _fieldsArr = null;
return presetEditor;
};
- presetEditor.state = function(_) {
- if (!arguments.length) return state;
- state = _;
+ presetEditor.state = function(val) {
+ if (!arguments.length) return _state;
+ _state = val;
return presetEditor;
};
- presetEditor.tags = function(_) {
- if (!arguments.length) return tags;
- tags = _;
- // Don't reset fieldsArr here.
+ presetEditor.tags = function(val) {
+ if (!arguments.length) return _tags;
+ _tags = val;
+ // Don't reset _fieldsArr here.
return presetEditor;
};
- presetEditor.entityID = function(_) {
- if (!arguments.length) return entityId;
- if (entityId === _) return presetEditor;
- entityId = _;
- fieldsArr = null;
+ presetEditor.entityID = function(val) {
+ if (!arguments.length) return _entityID;
+ if (_entityID === val) return presetEditor;
+ _entityID = val;
+ _fieldsArr = null;
return presetEditor;
};
diff --git a/modules/ui/raw_tag_editor.js b/modules/ui/raw_tag_editor.js
index 85a7f786e..d5a4fb23a 100644
--- a/modules/ui/raw_tag_editor.js
+++ b/modules/ui/raw_tag_editor.js
@@ -24,17 +24,17 @@ import {
export function uiRawTagEditor(context) {
- var taginfo = services.taginfo,
- dispatch = d3_dispatch('change'),
- _readOnlyTags = [],
- _showBlank = false,
- _updatePreference = true,
- _expanded = false,
- _newRow,
- _state,
- _preset,
- _tags,
- _entityID;
+ var taginfo = services.taginfo;
+ var dispatch = d3_dispatch('change');
+ var _readOnlyTags = [];
+ var _showBlank = false;
+ var _updatePreference = true;
+ var _expanded = false;
+ var _newRow;
+ var _state;
+ var _preset;
+ var _tags;
+ var _entityID;
function rawTagEditor(selection) {
@@ -148,16 +148,16 @@ export function uiRawTagEditor(context) {
items
.each(function(tag) {
- var row = d3_select(this),
- key = row.select('input.key'), // propagate bound data to child
- value = row.select('input.value'); // propagate bound data to child
+ var row = d3_select(this);
+ var key = row.select('input.key'); // propagate bound data to child
+ var value = row.select('input.value'); // propagate bound data to child
if (_entityID && taginfo) {
bindTypeahead(key, value);
}
- var isRelation = (_entityID && context.entity(_entityID).type === 'relation'),
- reference;
+ var isRelation = (_entityID && context.entity(_entityID).type === 'relation');
+ var reference;
if (isRelation && tag.key === 'type') {
reference = uiTagReference({ rtype: tag.value }, context);
@@ -239,8 +239,8 @@ export function uiRawTagEditor(context) {
function sort(value, data) {
- var sameletter = [],
- other = [];
+ var sameletter = [];
+ var other = [];
for (var i = 0; i < data.length; i++) {
if (data[i].value.substring(0, value.length) === value) {
sameletter.push(data[i]);
@@ -265,10 +265,9 @@ export function uiRawTagEditor(context) {
function keyChange(d) {
- var kOld = d.key,
- kNew = this.value.trim(),
- tag = {};
-
+ var kOld = d.key;
+ var kNew = this.value.trim();
+ var tag = {};
if (isReadOnly({ key: kNew })) {
this.value = kOld;
@@ -276,17 +275,17 @@ export function uiRawTagEditor(context) {
}
if (kNew && kNew !== kOld) {
- var match = kNew.match(/^(.*?)(?:_(\d+))?$/),
- base = match[1],
- suffix = +(match[2] || 1);
+ var match = kNew.match(/^(.*?)(?:_(\d+))?$/);
+ var base = match[1];
+ var suffix = +(match[2] || 1);
while (_tags[kNew]) { // rename key if already in use
kNew = base + '_' + suffix++;
}
if (_includes(kNew, '=')) {
- var splitStr = kNew.split('=').map(function(str) { return str.trim(); }),
- key = splitStr[0],
- value = splitStr[1];
+ var splitStr = kNew.split('=').map(function(str) { return str.trim(); });
+ var key = splitStr[0];
+ var value = splitStr[1];
kNew = key;
d.value = value;
@@ -295,9 +294,9 @@ export function uiRawTagEditor(context) {
tag[kOld] = undefined;
tag[kNew] = d.value;
- d.key = kNew; // Maintain DOM identity through the subsequent update.
+ d.key = kNew; // Maintain DOM identity through the subsequent update.
- if (_newRow === kOld) { // see if this row is still a new row
+ if (_newRow === kOld) { // see if this row is still a new row
_newRow = ((d.value === '' || kNew === '') ? kNew : undefined);
}
diff --git a/modules/ui/settings/custom_background.js b/modules/ui/settings/custom_background.js
new file mode 100644
index 000000000..9b2fc4e05
--- /dev/null
+++ b/modules/ui/settings/custom_background.js
@@ -0,0 +1,87 @@
+import _cloneDeep from 'lodash-es/cloneDeep';
+
+import { dispatch as d3_dispatch } from 'd3-dispatch';
+
+import { t } from '../../util/locale';
+import { uiConfirm } from '../confirm';
+import { utilNoAuto, utilRebind } from '../../util';
+
+
+export function uiSettingsCustomBackground(context) {
+ var dispatch = d3_dispatch('change');
+
+ function render(selection) {
+ var _origSettings = {
+ template: context.storage('background-custom-template')
+ };
+ var _currSettings = _cloneDeep(_origSettings);
+ var example = 'https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png';
+ var modal = uiConfirm(selection).okButton();
+
+ modal
+ .classed('settings-modal settings-custom-background', true);
+
+ modal.select('.modal-section.header')
+ .append('h3')
+ .text(t('settings.custom_background.header'));
+
+
+ var textSection = modal.select('.modal-section.message-text');
+
+ textSection
+ .append('pre')
+ .attr('class', 'instructions-template')
+ .text(t('settings.custom_background.instructions', { example: example }));
+
+ textSection
+ .append('textarea')
+ .attr('class', 'field-template')
+ .attr('placeholder', t('settings.custom_background.template.placeholder'))
+ .call(utilNoAuto)
+ .property('value', _currSettings.template);
+
+
+ // insert a cancel button, and adjust the button widths
+ var buttonSection = modal.select('.modal-section.buttons');
+
+ buttonSection
+ .insert('button', '.ok-button')
+ .attr('class', 'button col3 cancel-button secondary-action')
+ .text(t('confirm.cancel'));
+
+
+ buttonSection.select('.cancel-button')
+ .on('click.cancel', clickCancel);
+
+ buttonSection.select('.ok-button')
+ .classed('col3', true)
+ .classed('col4', false)
+ .attr('disabled', isSaveDisabled)
+ .on('click.save', clickSave);
+
+
+ function isSaveDisabled() {
+ return null;
+ }
+
+
+ // restore the original template
+ function clickCancel() {
+ textSection.select('.field-template').property('value', _origSettings.template);
+ context.storage('background-custom-template', _origSettings.template);
+ this.blur();
+ modal.close();
+ }
+
+ // accept the current template
+ function clickSave() {
+ _currSettings.template = textSection.select('.field-template').property('value');
+ context.storage('background-custom-template', _currSettings.template);
+ this.blur();
+ modal.close();
+ dispatch.call('change', this, _currSettings);
+ }
+ }
+
+ return utilRebind(render, dispatch, 'on');
+}
diff --git a/modules/ui/settings/custom_data.js b/modules/ui/settings/custom_data.js
new file mode 100644
index 000000000..090a9ec42
--- /dev/null
+++ b/modules/ui/settings/custom_data.js
@@ -0,0 +1,121 @@
+import _cloneDeep from 'lodash-es/cloneDeep';
+
+import { dispatch as d3_dispatch } from 'd3-dispatch';
+import { event as d3_event } from 'd3-selection';
+
+import { t } from '../../util/locale';
+import { uiConfirm } from '../confirm';
+import { utilNoAuto, utilRebind } from '../../util';
+
+
+export function uiSettingsCustomData(context) {
+ var dispatch = d3_dispatch('change');
+
+ function render(selection) {
+ var dataLayer = context.layers().layer('data');
+ var _origSettings = {
+ fileList: (dataLayer && dataLayer.fileList()) || null,
+ url: context.storage('settings-custom-data-url')
+ };
+ var _currSettings = _cloneDeep(_origSettings);
+
+ // var example = 'https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png';
+ var modal = uiConfirm(selection).okButton();
+
+ modal
+ .classed('settings-modal settings-custom-data', true);
+
+ modal.select('.modal-section.header')
+ .append('h3')
+ .text(t('settings.custom_data.header'));
+
+
+ var textSection = modal.select('.modal-section.message-text');
+
+ textSection
+ .append('pre')
+ .attr('class', 'instructions-file')
+ .text(t('settings.custom_data.file.instructions'));
+
+ textSection
+ .append('input')
+ .attr('class', 'field-file')
+ .attr('type', 'file')
+ .property('files', _currSettings.fileList) // works for all except IE11
+ .on('change', function() {
+ var files = d3_event.target.files;
+ if (files && files.length) {
+ _currSettings.url = '';
+ textSection.select('.field-url').property('value', '');
+ _currSettings.fileList = files;
+ } else {
+ _currSettings.fileList = null;
+ }
+ });
+
+ textSection
+ .append('h4')
+ .text(t('settings.custom_data.or'));
+
+ textSection
+ .append('pre')
+ .attr('class', 'instructions-url')
+ .text(t('settings.custom_data.url.instructions'));
+
+ textSection
+ .append('textarea')
+ .attr('class', 'field-url')
+ .attr('placeholder', t('settings.custom_data.url.placeholder'))
+ .call(utilNoAuto)
+ .property('value', _currSettings.url);
+
+
+ // insert a cancel button, and adjust the button widths
+ var buttonSection = modal.select('.modal-section.buttons');
+
+ buttonSection
+ .insert('button', '.ok-button')
+ .attr('class', 'button col3 cancel-button secondary-action')
+ .text(t('confirm.cancel'));
+
+
+ buttonSection.select('.cancel-button')
+ .on('click.cancel', clickCancel);
+
+ buttonSection.select('.ok-button')
+ .classed('col3', true)
+ .classed('col4', false)
+ .attr('disabled', isSaveDisabled)
+ .on('click.save', clickSave);
+
+
+ function isSaveDisabled() {
+ return null;
+ }
+
+
+ // restore the original url
+ function clickCancel() {
+ textSection.select('.field-url').property('value', _origSettings.url);
+ context.storage('settings-custom-data-url', _origSettings.url);
+ this.blur();
+ modal.close();
+ }
+
+ // accept the current url
+ function clickSave() {
+ _currSettings.url = textSection.select('.field-url').property('value').trim();
+
+ // one or the other but not both
+ if (_currSettings.url) { _currSettings.fileList = null; }
+ if (_currSettings.fileList) { _currSettings.url = ''; }
+
+ context.storage('settings-custom-data-url', _currSettings.url);
+ this.blur();
+ modal.close();
+ dispatch.call('change', this, _currSettings);
+ }
+ }
+
+ return utilRebind(render, dispatch, 'on');
+}
diff --git a/modules/ui/settings/index.js b/modules/ui/settings/index.js
new file mode 100644
index 000000000..b8a047b26
--- /dev/null
+++ b/modules/ui/settings/index.js
@@ -0,0 +1,2 @@
+export { uiSettingsCustomBackground } from './custom_background';
+export { uiSettingsCustomData } from './custom_data';
diff --git a/modules/ui/sidebar.js b/modules/ui/sidebar.js
index 256c775f8..33b3df5c0 100644
--- a/modules/ui/sidebar.js
+++ b/modules/ui/sidebar.js
@@ -2,18 +2,26 @@ import _throttle from 'lodash-es/throttle';
import { selectAll as d3_selectAll } from 'd3-selection';
-import { osmNote } from '../osm';
-import { uiFeatureList } from './feature_list';
-import { uiInspector } from './inspector';
-import { uiNoteEditor } from './note_editor';
+import {
+ osmEntity,
+ osmNote
+} from '../osm';
+
+import {
+ uiDataEditor,
+ uiFeatureList,
+ uiInspector,
+ uiNoteEditor
+} from './index';
export function uiSidebar(context) {
var inspector = uiInspector(context);
+ var dataEditor = uiDataEditor(context);
var noteEditor = uiNoteEditor(context);
var _current;
+ var _wasData = false;
var _wasNote = false;
- // var layer = d3_select(null);
function sidebar(selection) {
@@ -22,25 +30,31 @@ export function uiSidebar(context) {
.attr('class', 'feature-list-pane')
.call(uiFeatureList(context));
-
var inspectorWrap = selection
.append('div')
.attr('class', 'inspector-hidden inspector-wrap fr');
- function hover(what) {
- if ((what instanceof osmNote)) {
- _wasNote = true;
- var notes = d3_selectAll('.note');
- notes
- .classed('hovered', function(d) { return d === what; });
-
- sidebar.show(noteEditor.note(what));
+ function hover(datum) {
+ if (datum && datum.__featurehash__) { // hovering on data
+ _wasData = true;
+ sidebar
+ .show(dataEditor.datum(datum));
selection.selectAll('.sidebar-component')
.classed('inspector-hover', true);
- } else if (!_current && context.hasEntity(what)) {
+ } else if (datum instanceof osmNote) {
+ if (context.mode().id === 'drag-note') return;
+ _wasNote = true;
+
+ sidebar
+ .show(noteEditor.note(datum));
+
+ selection.selectAll('.sidebar-component')
+ .classed('inspector-hover', true);
+
+ } else if (!_current && (datum instanceof osmEntity)) {
featureListWrap
.classed('inspector-hidden', true);
@@ -48,10 +62,10 @@ export function uiSidebar(context) {
.classed('inspector-hidden', false)
.classed('inspector-hover', true);
- if (inspector.entityID() !== what || inspector.state() !== 'hover') {
+ if (inspector.entityID() !== datum.id || inspector.state() !== 'hover') {
inspector
.state('hover')
- .entityID(what);
+ .entityID(datum.id);
inspectorWrap
.call(inspector);
@@ -65,10 +79,10 @@ export function uiSidebar(context) {
inspector
.state('hide');
- } else if (_wasNote) {
+ } else if (_wasData || _wasNote) {
_wasNote = false;
- d3_selectAll('.note')
- .classed('hovered', false);
+ _wasData = false;
+ d3_selectAll('.note').classed('hover', false);
sidebar.hide();
}
}
diff --git a/modules/ui/version.js b/modules/ui/version.js
index d62dfd498..673db5b5e 100644
--- a/modules/ui/version.js
+++ b/modules/ui/version.js
@@ -4,15 +4,15 @@ import { tooltip } from '../util/tooltip';
// these are module variables so they are preserved through a ui.restart()
-var sawVersion = null,
- isNewVersion = false,
- isNewUser = false;
+var sawVersion = null;
+var isNewVersion = false;
+var isNewUser = false;
export function uiVersion(context) {
- var currVersion = context.version,
- matchedVersion = currVersion.match(/\d\.\d\.\d.*/);
+ var currVersion = context.version;
+ var matchedVersion = currVersion.match(/\d+\.\d+\.\d+.*/);
if (sawVersion === null && matchedVersion !== null) {
isNewVersion = (context.storage('sawVersion') !== currVersion);
diff --git a/modules/util/index.js b/modules/util/index.js
index fb05fe83c..e6e6a0b3d 100644
--- a/modules/util/index.js
+++ b/modules/util/index.js
@@ -14,6 +14,7 @@ export { utilFunctor } from './util';
export { utilGetAllNodes } from './util';
export { utilGetPrototypeOf } from './util';
export { utilGetSetValue } from './get_set_value';
+export { utilHashcode } from './util';
export { utilIdleWorker} from './idle_worker';
export { utilMapCSSRule } from './mapcss_rule';
export { utilNoAuto } from './util';
@@ -26,5 +27,6 @@ export { utilSessionMutex } from './session_mutex';
export { utilStringQs } from './util';
export { utilSuggestNames } from './suggest_names';
export { utilTagText } from './util';
+export { utilTiler } from './tiler';
export { utilTriggerEvent } from './trigger_event';
export { utilWrap } from './util';
diff --git a/modules/util/tiler.js b/modules/util/tiler.js
new file mode 100644
index 000000000..80f1989f5
--- /dev/null
+++ b/modules/util/tiler.js
@@ -0,0 +1,163 @@
+import { range as d3_range } from 'd3-array';
+import { geoExtent, geoScaleToZoom } from '../geo';
+
+
+export function utilTiler() {
+ var _size = [256, 256];
+ var _scale = 256;
+ var _tileSize = 256;
+ var _zoomExtent = [0, 20];
+ var _translate = [_size[0] / 2, _size[1] / 2];
+ var _margin = 0;
+ var _skipNullIsland = false;
+
+
+ function bound(val) {
+ return Math.min(_zoomExtent[1], Math.max(_zoomExtent[0], val));
+ }
+
+
+ function nearNullIsland(tile) {
+ var x = tile[0];
+ var y = tile[1];
+ var z = tile[2];
+ if (z >= 7) {
+ var center = Math.pow(2, z - 1);
+ var width = Math.pow(2, z - 6);
+ var min = center - (width / 2);
+ var max = center + (width / 2) - 1;
+ return x >= min && x <= max && y >= min && y <= max;
+ }
+ return false;
+ }
+
+
+ function tiler() {
+ var z = geoScaleToZoom(_scale / (2 * Math.PI), _tileSize);
+ var z0 = bound(Math.round(z));
+ var log2ts = Math.log(_tileSize) * Math.LOG2E;
+ var k = Math.pow(2, z - z0 + log2ts);
+ var origin = [
+ (_translate[0] - _scale / 2) / k,
+ (_translate[1] - _scale / 2) / k
+ ];
+
+ var cols = d3_range(
+ Math.max(0, Math.floor(-origin[0]) - _margin),
+ Math.max(0, Math.ceil(_size[0] / k - origin[0]) + _margin)
+ );
+ var rows = d3_range(
+ Math.max(0, Math.floor(-origin[1]) - _margin),
+ Math.max(0, Math.ceil(_size[1] / k - origin[1]) + _margin)
+ );
+
+ var tiles = [];
+ for (var i = 0; i < rows.length; i++) {
+ var y = rows[i];
+ for (var j = 0; j < cols.length; j++) {
+ var x = cols[j];
+
+ if (i >= _margin && i <= rows.length - _margin &&
+ j >= _margin && j <= cols.length - _margin) {
+ tiles.unshift([x, y, z0]); // tiles in view at beginning
+ } else {
+ tiles.push([x, y, z0]); // tiles in margin at the end
+ }
+ }
+ }
+
+ tiles.translate = origin;
+ tiles.scale = k;
+
+ return tiles;
+ }
+
+
+ /**
+ * getTiles() returns an array of tiles that cover the map view
+ */
+ tiler.getTiles = function(projection) {
+ var origin = [
+ projection.scale() * Math.PI - projection.translate()[0],
+ projection.scale() * Math.PI - projection.translate()[1]
+ ];
+
+ this
+ .size(projection.clipExtent()[1])
+ .scale(projection.scale() * 2 * Math.PI)
+ .translate(projection.translate());
+
+ var tiles = tiler();
+ var ts = tiles.scale;
+
+ return tiles
+ .map(function(tile) {
+ if (_skipNullIsland && nearNullIsland(tile)) {
+ return false;
+ }
+ var x = tile[0] * ts - origin[0];
+ var y = tile[1] * ts - origin[1];
+ return {
+ id: tile.toString(),
+ xyz: tile,
+ extent: geoExtent(
+ projection.invert([x, y + ts]),
+ projection.invert([x + ts, y])
+ )
+ };
+ }).filter(Boolean);
+ };
+
+
+ tiler.tileSize = function(val) {
+ if (!arguments.length) return _tileSize;
+ _tileSize = val;
+ return tiler;
+ };
+
+
+ tiler.zoomExtent = function(val) {
+ if (!arguments.length) return _zoomExtent;
+ _zoomExtent = val;
+ return tiler;
+ };
+
+
+ tiler.size = function(val) {
+ if (!arguments.length) return _size;
+ _size = val;
+ return tiler;
+ };
+
+
+ tiler.scale = function(val) {
+ if (!arguments.length) return _scale;
+ _scale = val;
+ return tiler;
+ };
+
+
+ tiler.translate = function(val) {
+ if (!arguments.length) return _translate;
+ _translate = val;
+ return tiler;
+ };
+
+
+ // number to extend the rows/columns beyond those covering the viewport
+ tiler.margin = function(val) {
+ if (!arguments.length) return _margin;
+ _margin = +val;
+ return tiler;
+ };
+
+
+ tiler.skipNullIsland = function(val) {
+ if (!arguments.length) return _skipNullIsland;
+ _skipNullIsland = val;
+ return tiler;
+ };
+
+
+ return tiler;
+}
diff --git a/modules/util/util.js b/modules/util/util.js
index 60e1436df..b056a23d3 100644
--- a/modules/util/util.js
+++ b/modules/util/util.js
@@ -273,4 +273,19 @@ export function utilExternalPresets() {
export function utilExternalValidationRules() {
return utilStringQs(window.location.hash).hasOwnProperty('validations');
-}
\ No newline at end of file
+}
+
+// https://stackoverflow.com/questions/194846/is-there-any-kind-of-hash-code-function-in-javascript
+// https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
+export function utilHashcode(str) {
+ var hash = 0;
+ if (str.length === 0) {
+ return hash;
+ }
+ for (var i = 0; i < str.length; i++) {
+ var char = str.charCodeAt(i);
+ hash = ((hash << 5) - hash) + char;
+ hash = hash & hash; // Convert to 32bit integer
+ }
+ return hash;
+}
diff --git a/modules/validations/mapcss_checks.js b/modules/validations/mapcss_checks.js
index 5f0d753d4..86255a917 100644
--- a/modules/validations/mapcss_checks.js
+++ b/modules/validations/mapcss_checks.js
@@ -1,17 +1,22 @@
+import { serviceMapRules } from '../services';
+
export function validationMapCSSChecks() {
- var validation = function(changes, graph, rules) {
+ var validation = function(changes, graph) {
+ var rules = serviceMapRules.validationRules();
var warnings = [];
var createdModified = ['created', 'modified'];
- for (var i = 0; i < createdModified.length; i++) {
- var entities = changes[createdModified[i]];
- for (var j = 0; j < entities.length; j++) {
- var entity = entities[j];
- for (var k = 0; k < rules.length; k++) {
- var rule = rules[k];
- rule.findWarnings(entity, graph, warnings);
+
+ for (var i = 0; i < rules.length; i++) {
+ var rule = rules[i];
+ for (var j = 0; j < createdModified.length; j++) {
+ var type = createdModified[j];
+ var entities = changes[type];
+ for (var k = 0; k < entities.length; k++) {
+ rule.findWarnings(entities[k], graph, warnings);
}
}
}
+
return warnings;
};
return validation;
diff --git a/modules/validations/tag_suggests_area.js b/modules/validations/tag_suggests_area.js
index 20b4a402c..a631f4a79 100644
--- a/modules/validations/tag_suggests_area.js
+++ b/modules/validations/tag_suggests_area.js
@@ -12,7 +12,11 @@ export function validationTagSuggestsArea() {
var presence = ['landuse', 'amenities', 'tourism', 'shop'];
for (var i = 0; i < presence.length; i++) {
if (tags[presence[i]] !== undefined) {
- return presence[i] + '=' + tags[presence[i]];
+ if (presence[i] === 'tourism' && tags[presence[i]] === 'artwork') {
+ continue; // exception for tourism=artwork - #5206
+ } else {
+ return presence[i] + '=' + tags[presence[i]];
+ }
}
}
diff --git a/package.json b/package.json
index 3f2f448fc..97fe96919 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "iD",
- "version": "2.9.2",
+ "version": "2.11.1",
"description": "A friendly editor for OpenStreetMap",
"main": "iD.js",
"repository": "openstreetmap/iD",
@@ -34,9 +34,12 @@
"@mapbox/sexagesimal": "1.1.0",
"@mapbox/togeojson": "0.16.0",
"@mapbox/vector-tile": "^1.3.1",
+ "@turf/bbox-clip": "^6.0.0",
"diacritics": "1.3.0",
- "lodash-es": "4.17.10",
- "marked": "0.4.0",
+ "fast-json-stable-stringify": "2.0.0",
+ "lodash-es": "4.17.11",
+ "marked": "0.5.0",
+ "martinez-polygon-clipping": "0.5.0",
"node-diff3": "1.0.0",
"osm-auth": "1.0.2",
"pannellum": "2.4.1",
@@ -46,10 +49,10 @@
"wmf-sitematrix": "0.1.4"
},
"devDependencies": {
- "@fortawesome/fontawesome": "^1.1.5",
- "@fortawesome/fontawesome-free-brands": "^5.0.13",
- "@fortawesome/fontawesome-free-regular": "^5.0.13",
- "@fortawesome/fontawesome-free-solid": "^5.0.13",
+ "@fortawesome/fontawesome-svg-core": "~1.2.4",
+ "@fortawesome/free-brands-svg-icons": "~5.3.1",
+ "@fortawesome/free-regular-svg-icons": "~5.3.1",
+ "@fortawesome/free-solid-svg-icons": "~5.3.1",
"@mapbox/maki": "^4.0.0",
"chai": "^4.1.0",
"colors": "^1.1.2",
@@ -65,17 +68,17 @@
"js-yaml": "^3.9.0",
"json-stringify-pretty-compact": "^1.1.0",
"jsonschema": "^1.1.0",
- "mapillary-js": "2.12.1",
- "mapillary_sprite_source": "^1.4.0",
+ "mapillary-js": "2.13.0",
+ "mapillary_sprite_source": "^1.5.0",
"minimist": "^1.2.0",
"mocha": "^5.0.0",
"mocha-phantomjs-core": "^2.1.0",
"name-suggestion-index": "0.1.6",
"npm-run-all": "^4.0.0",
- "osm-community-index": "0.4.5",
+ "osm-community-index": "0.4.7",
"phantomjs-prebuilt": "~2.1.11",
"request": "^2.85.0",
- "rollup": "~0.60.0",
+ "rollup": "~0.66.0",
"rollup-plugin-commonjs": "^9.0.0",
"rollup-plugin-includepaths": "~0.2.3",
"rollup-plugin-json": "^3.0.0",
@@ -85,7 +88,7 @@
"sinon": "^6.0.0",
"sinon-chai": "^3.1.0",
"smash": "0.0",
- "svg-sprite": "1.4.0",
+ "svg-sprite": "1.5.0",
"temaki": "1.0.0",
"uglify-js": "~3.4.0",
"xml2js": "^0.4.17",
diff --git a/scripts/deploy.sh b/scripts/deploy.sh
index f44f6573f..f95ae09e9 100755
--- a/scripts/deploy.sh
+++ b/scripts/deploy.sh
@@ -1,4 +1,4 @@
-#/bin/bash
+#!/bin/bash
# This is an example script that shows how to pull the latest version
# of iD and replace the version string with a git short hash.
diff --git a/scripts/txpush.sh b/scripts/txpush.sh
index 5807b321b..1fdfaed6d 100755
--- a/scripts/txpush.sh
+++ b/scripts/txpush.sh
@@ -1,4 +1,4 @@
-#/bin/bash
+#!/bin/bash
# This script runs on TravisCI to push new translation strings to transifex
diff --git a/svg/fontawesome/fas-archway.svg b/svg/fontawesome/fas-archway.svg
new file mode 100644
index 000000000..167181b7c
--- /dev/null
+++ b/svg/fontawesome/fas-archway.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/fontawesome/fas-balance-scale.svg b/svg/fontawesome/fas-balance-scale.svg
new file mode 100644
index 000000000..70e5cd4e2
--- /dev/null
+++ b/svg/fontawesome/fas-balance-scale.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/fontawesome/fas-bell.svg b/svg/fontawesome/fas-bell.svg
index 13cf68468..8b6ff39d6 100644
--- a/svg/fontawesome/fas-bell.svg
+++ b/svg/fontawesome/fas-bell.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/svg/fontawesome/fas-blender.svg b/svg/fontawesome/fas-blender.svg
new file mode 100644
index 000000000..0e15eb5c4
--- /dev/null
+++ b/svg/fontawesome/fas-blender.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/fontawesome/fas-charging-station.svg b/svg/fontawesome/fas-charging-station.svg
new file mode 100644
index 000000000..10aae5d96
--- /dev/null
+++ b/svg/fontawesome/fas-charging-station.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/fontawesome/fas-couch.svg b/svg/fontawesome/fas-couch.svg
new file mode 100644
index 000000000..2f2f86549
--- /dev/null
+++ b/svg/fontawesome/fas-couch.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/fontawesome/fas-dumbbell.svg b/svg/fontawesome/fas-dumbbell.svg
new file mode 100644
index 000000000..e7bacfd38
--- /dev/null
+++ b/svg/fontawesome/fas-dumbbell.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/fontawesome/fas-glasses.svg b/svg/fontawesome/fas-glasses.svg
new file mode 100644
index 000000000..76100a994
--- /dev/null
+++ b/svg/fontawesome/fas-glasses.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/fontawesome/fas-paint-roller.svg b/svg/fontawesome/fas-paint-roller.svg
new file mode 100644
index 000000000..0696b018b
--- /dev/null
+++ b/svg/fontawesome/fas-paint-roller.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/fontawesome/fas-people-carry.svg b/svg/fontawesome/fas-people-carry.svg
new file mode 100644
index 000000000..935d1fb50
--- /dev/null
+++ b/svg/fontawesome/fas-people-carry.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/fontawesome/fas-plane-departure.svg b/svg/fontawesome/fas-plane-departure.svg
new file mode 100644
index 000000000..5fed596d4
--- /dev/null
+++ b/svg/fontawesome/fas-plane-departure.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/fontawesome/fas-taxi.svg b/svg/fontawesome/fas-taxi.svg
index 012729fec..d81bc1f28 100644
--- a/svg/fontawesome/fas-taxi.svg
+++ b/svg/fontawesome/fas-taxi.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/svg/fontawesome/fas-volume-up.svg b/svg/fontawesome/fas-volume-up.svg
index 1b79fd302..7588f42d3 100644
--- a/svg/fontawesome/fas-volume-up.svg
+++ b/svg/fontawesome/fas-volume-up.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/svg/fontawesome/fas-wifi.svg b/svg/fontawesome/fas-wifi.svg
new file mode 100644
index 000000000..bbe6efbbb
--- /dev/null
+++ b/svg/fontawesome/fas-wifi.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/iD-sprite/icons/icon-note.svg b/svg/iD-sprite/icons/icon-note.svg
index 11033f707..c531a3212 100644
--- a/svg/iD-sprite/icons/icon-note.svg
+++ b/svg/iD-sprite/icons/icon-note.svg
@@ -1,5 +1,5 @@
diff --git a/svg/iD-sprite/operations/operation-detach-node.svg b/svg/iD-sprite/operations/operation-detach-node.svg
new file mode 100644
index 000000000..a10edfc66
--- /dev/null
+++ b/svg/iD-sprite/operations/operation-detach-node.svg
@@ -0,0 +1,6 @@
+
+
+
diff --git a/svg/iD-sprite/turns/turn-shadow.svg b/svg/iD-sprite/turns/turn-shadow.svg
index c4468ec89..d777a36d7 100644
--- a/svg/iD-sprite/turns/turn-shadow.svg
+++ b/svg/iD-sprite/turns/turn-shadow.svg
@@ -1,7 +1,7 @@