<commit_before>define(["FS","Tonyu","UI","ImageList","Blob","Auth","WebSite"
        ,"ImageDetailEditor","Util","OggConverter","Assets"],
        function (FS, Tonyu, UI,IL,Blob,Auth,WebSite,
                ImageDetailEditor,Util,OggConverter,Assets) {
    var ResEditor=function (prj, mediaType) {
        var mediaInfos={
                image:{name:"画像",exts:["png","gif","jpg"],path:"images/",key:"images",
                    extPattern:/\.(png|gif|jpe?g)$/i,contentType:/image\/(png|gif|jpe?g)/,
                    newItem:function (name) {
                        var r={type:"single"};//pwidth:32,pheight:32};
                        if (name) r.name="$pat_"+name;
                        return r;
                    }
                },
                sound:{name:"音声",exts:["mp3","ogg","mp4","m4a","mid","wav"],path:"sounds/",key:"sounds",
                    extPattern:/\.(mp3|ogg|mp4|m4a|midi?|wav)$/i,contentType:/((audio\/(mp3|ogg|x-m4a|midi?|wav))|(video\/mp4))/,
                    newItem:function (name) {
                        var r={};
                        if (name) r.name="$se_"+name;
                        return r;
                    }
                }
        };
        var mediaInfo=mediaInfos[mediaType||"image"];
        var d=UI("div", {title:mediaInfo.name+"リスト"});
        d.css({height:200+"px", "overflow-v":"scroll"});
        var rsrc=prj.getResource();
        var rsrcDir=prj.getDir().rel(mediaInfo.path);
        var itemUIs=[];
        if (!rsrc) prj.setResource({images:[],sounds:[]});
        function convURL(u) {
            try {
                function cvs(u) {
                    return WebSite.urlAliases[u] || u;
                }
                if (Util.endsWith(u,".ogg")) {
                    u=cvs("images/sound_ogg.png");
                } else if (Util.endsWith(u,".mp3")) {
                    u=cvs("images/sound_mp3.png");
                } else if (Util.endsWith(u,".mp4")) {
                    u=cvs("images/sound_mp4.png");
                } else if (Util.endsWith(u,".m4a")) {
                    u=cvs("images/sound_m4a.png");
                } else if (Util.endsWith(u,".mid") || Util.endsWith(u,".midi")) {
                    u=cvs("images/sound_mid.png");
                } else if (Util.endsWith(u,".wav")) {
                    u=cvs("images/sound_wav.png");
                }
                return Assets.resolve(u,prj);
            }catch(e) {
                return WebSite.urlAliases["images/ecl.png"];
            }
        }
        function reload() {
            d.empty();
            var dragMsg="ここに"+mediaInfo.name+"ファイル("+mediaInfo.exts.join("/")+")をドラッグ＆ドロップして追加";
            var dragPoint=UI("div", {style:"margin:10px; padding:10px; border:solid blue 2px;",
                on:{dragover: s, dragenter: s, drop:dropAdd}},dragMsg
            ).appendTo(d);
            rsrc=prj.getResource();
            var items=rsrc[mediaInfo.key];
            var tempFiles = items.slice();
            itemUIs=[];
            var itemTbl=UI("div").appendTo(d);
            items.forEach(function (item){
                var itemUI=genItemUI(item);
                itemUIs.push(itemUI);
                itemUI.appendTo(itemTbl);
            });
            d.append(UI("div",{style:"clear:left;"},
                         ["button", {on:{click:function (){ add();}}}, "追加"],
                         ["button", {on:{click:function (){ d.dialog("close"); }}}, "完了"]
            ));
            function dropAdd(e) {
                var eo=e.originalEvent;
                var files = eo.dataTransfer.files;
                var readFiles = new Array(files.length);
                var readFileSum = files.length;
                var notReadFiles = [];
                var existsFiles = [];
                for (var i=0; i<files.length; i++) {
                    var file = files[i];
                    var useBlob=WebSite.serverType=="GAE" && (file.size>1000*300);
                    if(!file.type.match(mediaInfo.contentType)) {
                        readFileSum--;
                        notReadFiles.push(file);
                        e.stopPropagation();
                        e.preventDefault();
                        continue;
                    }
                    var itemName=file.name.replace(mediaInfo.extPattern,"").replace(/\W/g,"_");
                    var itemExt="";
                    if (file.name.match(mediaInfo.extPattern)) {
                        itemExt=RegExp.lastMatch.toLowerCase();
                    }
                    var itemFile=rsrcDir.rel(itemName+itemExt);
                    var itemRelPath="ls:"+itemFile.relPath(prj.getDir());
                    var existsFile;
                    var fileExists=tempFiles.some(function(f){
                        existsFile=f;
                        return f.url==itemRelPath;
                    });
                    if (fileExists) {
                        readFileSum--;
                        file.existsFile=existsFile;
                        existsFiles.push(file);
                        e.stopPropagation();
                        e.preventDefault();
                        continue;
                    }

                    var v=mediaInfo.newItem(itemName);
                    renameUnique(v);
                    tempFiles.push(v);
                    if (useBlob) {
                        Auth.assertLogin({
                            showLoginLink:function (u) {
                                dragPoint.css("border","solid red 2px").empty().append(
                                        UI("div","大きい"+mediaInfo.name+"を追加するには，ログインが必要です：",
                                           ["a",{href:u,target:"login",style:"color: blue;"},"ログインする"])
                                );
                            },success:function (u) {
                                dragPoint.text("アップロード中...");
                                var prjN=prj.getName();
                                Blob.upload(u,prjN,file,{success:function (){
                                    dragPoint.text(dragMsg);
                                    v.url="${blobPath}/"+u+"/"+prjN+"/"+file.name;
                                    add(v);
                                }});
                            }
                        });
                    } else {
                        var readCnt = 0;
                        var reader = new FileReader();
                        reader.temp_file=file;
                        reader.temp_itemName=itemName;
                        reader.temp_itemExt=itemExt;
                        reader.temp_v=v;
                        reader.onloadend = function(e) {
                            var target = e.target;
                            var fileContent = target.result;
                            if (target.error==null && fileContent!=null) {
                                var itemFile=rsrcDir.rel(target.temp_itemName+target.temp_itemExt);
                                itemFile.setBytes(fileContent);
                                target.temp_v.url="ls:"+itemFile.relPath(prj.getDir());// fileContent;
                                for (var i=0;i<files.length;i++) {
                                    if (files[i]==target.temp_file) {
                                        readFiles[i]=target.temp_v;
                                        break;
                                    }
                                }
                            }
                            readCnt++;
                            if (readCnt == readFileSum) {
                                addAry(readFiles);
                            }
                        };
                        reader.readAsArrayBuffer(file);
                    }
                }

                if (notReadFiles.length>0) {
                    var mes="このファイルは追加できません：\n";
                    notReadFiles.forEach(function(f){
                        if (f) mes+=f.name+"\n";
                    });
                    alert(mes);
                }
                if (existsFiles.length>0) {
                    var mes="この名前のファイルは既に登録されています：\n";
                    existsFiles.forEach(function(f){
                        if (f) mes+=f.name+" ("+f.existsFile.name+")\n";
                    });
                    alert(mes);
                }

                e.stopPropagation();
                e.preventDefault();
                return false;
            }
            function s(e) {
                e.stopPropagation();
                e.preventDefault();

            }
            function genItemUI(item) {
                function detail() {
                    if (mediaType=="sound") return;
                    ImageDetailEditor.show(item, prj, item.name, {
                        onclose: function () {
                            prj.setResource(rsrc);
                            reload();
                        }
                    });
                }
                function del() {
                    for (var i=items.length-1; i>=0 ; i--) {
                        if (items[i]===item) {
                            var r=Assets.resolve( items[i].url, prj,{asFile:1});
                            if (FS.isFile(r) && rsrcDir.contains(r)) {
                                console.log(r.path()," is removed.");
                                r.rm();
                            }
                            items.splice(i,1);
                            break;
                        }
                    }
                    update();
                }
                function up() {
                    for (var i=items.length-1; i>=1 ; i--) {
                        if (items[i]===item) {
                            items.splice(i,1);
                            items.splice(i-1,0,item);
                            break;
                        }
                    }
                    update();
                }
                function down() {
                    for (var i=items.length-2; i>=0 ; i--) {
                        if (items[i]===item) {
                            items.splice(i,1);
                            items.splice(i+1,0,item);
                            break;
                        }
                    }
                    update();
                }

                var res=UI("div",{style:"float:left;"},
                        ["canvas",{$var:"c",width:100,height:100,"class":"clickable",on:{click: detail}}],
                        ["div",{style:"float:right;"},
                        ["button",{on:{click:del}}, "×"],["br"],
                        ["button",{on:{click:up}}, "←"],["br"],
                        ["button",{on:{click:down}}, "→"]],
                        ["div",
                            ["input", {$var:"name", size:12,value:item.name}]
                            ]
                   );
                draw(convURL(item.url),res.$vars.c[0]);
                var v=res.$vars;
                v.data=item;
                return res;
            }
            function add(v) {
                items.push(v || mediaInfo.newItem());
                update();
            }
            function addAry(vAry) {
                vAry.forEach(function(v){
                    if (v) items.push(v || mediaInfo.newItem());
                });
                update();
            }
            function renameUnique(v) {
                var name=v.name;
                var rename=name;
                var cnt=1;
                for (var i=tempFiles.length-1; i>=0 ; i--) {
                    var o=tempFiles[i];
                    if (o.name==rename) {
                        rename=name+"_"+cnt;
                        cnt++;
                        i=tempFiles.length;
                        continue;
                    }
                }
                v.name=rename;
            }
        }
        function update() {
            itemUIs.forEach(function (itemUI) {
                var v=itemUI.$vars;
                var item=v.data;
                item.name=v.name.val();
            });
            console.log(rsrc);
            prj.setResource(rsrc);
            reload();
        }
        function cleanFiles() {
            var items=rsrc[mediaInfo.key];
            Auth.currentUser(function (u,ct) {
                if (!u) return;
                var rtf=[];
                items.forEach(function (item) {
                    var a,ogg;
                    if (a=Blob.isBlobURL(item.url)) {
                        rtf.push(a.fileName);
                        ogg=a.fileName.replace(/\.(mp3|mp4|m4a)$/,".ogg");
                        if (ogg!=a.fileName) rtf.push(ogg);
                    }
                });
                var data={
                        user:u,
                        project:prj.getName(),
                        mediaType:mediaType,
                        csrfToken:ct,
                        retainFileNames:JSON.stringify(rtf)
                };
                console.log("retainBlobs",data);
                //TODO: urlchange!
                $.ajax({url:WebSite.serverTop+"/retainBlobs",type:"get",
                    data:data
                });
            });
            var cleanFile={};
            if (rsrcDir.exists()) {
                rsrcDir.each(function (f) {
                    cleanFile["ls:"+f.relPath(prj.getDir())]=f;
                });
            }
            rsrc=prj.getResource();
            items.forEach(function (item){
                delete cleanFile[item.url];
                delete cleanFile[item.url.replace(/\.(mp3|mp4|m4a)$/,".ogg")];
            });
            console.log(cleanFile);
            /*for (var ci in cleanFile) {
                var cf=cleanFile[ci];
                console.log(cf+" is removed");
                cf.rm();
            }*/
        }
        function toi(s) {
            if (!s || s=="") return undefined;
            return parseInt(s);
        }
        reload();
        return d.dialog({
            modal:true,
            width: 800,
            height: 500,
            close: function () {
                update();
                cleanFiles();
                if (mediaType=="sound" && rsrcDir.exists()) {
                    OggConverter.convert(rsrcDir);
                }
            }
        });
    };
    function draw(img, canvas) {
        if (typeof img=="string") {
            var i=new Image();
            i.onload=function () {
                draw(i,canvas);
            };
            i.src=img;
            return i;
        }
        var cw=canvas.width;
        var ch=canvas.height;
        var cctx=canvas.getContext("2d");
        var width=img.width;
        var height=img.height;
        var calcw=ch/height*width; // calch=ch
        var calch=cw/width*height; // calcw=cw
        if (calch>ch) calch=ch;
        if (calcw>cw) calcw=cw;
        cctx.clearRect(0,0,cw,ch);
        var marginw=Math.floor((cw-calcw)/2);
        var marginh=Math.floor((ch-calch)/2);
        cctx.drawImage(img,
        0,0,width, height,
        marginw,marginh,calcw, calch );
    }
    return ResEditor;
});
<commit_msg>Change ResEditor specification<commit_after>define(["FS","Tonyu","UI","ImageList","Blob","Auth","WebSite"
        ,"ImageDetailEditor","Util","OggConverter","Assets"],
        function (FS, Tonyu, UI,IL,Blob,Auth,WebSite,
                ImageDetailEditor,Util,OggConverter,Assets) {
    var ResEditor=function (prj, mediaType) {
        var mediaInfos={
                image:{name:"画像",exts:["png","gif","jpg"],path:"images/",key:"images",
                    extPattern:/\.(png|gif|jpe?g)$/i,contentType:/image\/(png|gif|jpe?g)/,
                    newItem:function (name) {
                        var r={type:"single"};//pwidth:32,pheight:32};
                        if (name) r.name="$pat_"+name;
                        return r;
                    }
                },
                sound:{name:"音声",exts:["mp3","ogg","mp4","m4a","mid","wav"],path:"sounds/",key:"sounds",
                    extPattern:/\.(mp3|ogg|mp4|m4a|midi?|wav)$/i,contentType:/((audio\/(mp3|ogg|x-m4a|midi?|wav))|(video\/mp4))/,
                    newItem:function (name) {
                        var r={};
                        if (name) r.name="$se_"+name;
                        return r;
                    }
                }
        };
        var mediaInfo=mediaInfos[mediaType||"image"];
        var d=UI("div", {title:mediaInfo.name+"リスト"});
        d.css({height:200+"px", "overflow-v":"scroll"});
        var rsrc=prj.getResource();
        var rsrcDir=prj.getDir().rel(mediaInfo.path);
        var itemUIs=[];
        if (!rsrc) prj.setResource({images:[],sounds:[]});
        function convURL(u) {
            try {
                function cvs(u) {
                    return WebSite.urlAliases[u] || u;
                }
                if (Util.endsWith(u,".ogg")) {
                    u=cvs("images/sound_ogg.png");
                } else if (Util.endsWith(u,".mp3")) {
                    u=cvs("images/sound_mp3.png");
                } else if (Util.endsWith(u,".mp4")) {
                    u=cvs("images/sound_mp4.png");
                } else if (Util.endsWith(u,".m4a")) {
                    u=cvs("images/sound_m4a.png");
                } else if (Util.endsWith(u,".mid") || Util.endsWith(u,".midi")) {
                    u=cvs("images/sound_mid.png");
                } else if (Util.endsWith(u,".wav")) {
                    u=cvs("images/sound_wav.png");
                }
                return Assets.resolve(u,prj);
            }catch(e) {
                return WebSite.urlAliases["images/ecl.png"];
            }
        }
        function reload() {
            d.empty();
            var dragMsg="ここに"+mediaInfo.name+"ファイル("+mediaInfo.exts.join("/")+")をドラッグ＆ドロップして追加";
            var dragPoint=UI("div", {style:"margin:10px; padding:10px; border:solid blue 2px;",
                on:{dragover: s, dragenter: s, drop:dropAdd}},dragMsg
            ).appendTo(d);
            rsrc=prj.getResource();
            var items=rsrc[mediaInfo.key];
            var tempFiles = items.slice();
            itemUIs=[];
            var itemTbl=UI("div").appendTo(d);
            items.forEach(function (item){
                var itemUI=genItemUI(item);
                itemUIs.push(itemUI);
                itemUI.appendTo(itemTbl);
            });
            d.append(UI("div",{style:"clear:left;"},
                         ["button", {on:{click:function (){ add();}}}, "追加"],
                         ["button", {on:{click:function (){ d.dialog("close"); }}}, "完了"]
            ));
            function dropAdd(e) {
                e.stopPropagation();
                e.preventDefault();
                var eo=e.originalEvent;
                var files = eo.dataTransfer.files;
                var readFiles = new Array(files.length);
                var readFileSum = files.length;
                var notReadFiles = [];
                var existsFiles = [];
                for (var i=0; i<files.length; i++) {
                    var file = files[i];
                    var useBlob=WebSite.serverType=="GAE" && (file.size>1000*300);
                    if(!file.type.match(mediaInfo.contentType)) {
                        readFileSum--;
                        notReadFiles.push(file);
                        continue;
                    }
                    var itemName=file.name.replace(mediaInfo.extPattern,"").replace(/\W/g,"_");
                    var itemExt="";
                    if (file.name.match(mediaInfo.extPattern)) {
                        itemExt=RegExp.lastMatch.toLowerCase();
                    }
                    var itemFile=rsrcDir.rel(itemName+itemExt);
                    var itemRelPath="ls:"+itemFile.relPath(prj.getDir());
                    var existsFile;
                    var fileExists=tempFiles.some(function(f){
                        existsFile=f;
                        var fNameTemp=f.url.replace(mediaInfo.extPattern,"");
                        var fName=fNameTemp.substring(fNameTemp.lastIndexOf("/")+1,fNameTemp.length);
                        return fName==itemName;
                    });
                    if (fileExists) {
                        readFileSum--;
                        file.existsFile=existsFile;
                        existsFiles.push(file);
                        continue;
                    }

                    var v=mediaInfo.newItem(itemName);
                    v.url=itemRelPath;
                    renameUnique(v);
                    tempFiles.push(v);
                    if (useBlob) {
                        Auth.assertLogin({
                            showLoginLink:function (u) {
                                dragPoint.css("border","solid red 2px").empty().append(
                                        UI("div","大きい"+mediaInfo.name+"を追加するには，ログインが必要です：",
                                           ["a",{href:u,target:"login",style:"color: blue;"},"ログインする"])
                                );
                            },success:function (u) {
                                dragPoint.text("アップロード中...");
                                var prjN=prj.getName();
                                Blob.upload(u,prjN,file,{success:function (){
                                    dragPoint.text(dragMsg);
                                    v.url="${blobPath}/"+u+"/"+prjN+"/"+file.name;
                                    add(v);
                                }});
                            }
                        });
                    } else {
                        var readCnt = 0;
                        var reader = new FileReader();
                        reader.temp_file=file;
                        reader.temp_itemName=itemName;
                        reader.temp_itemExt=itemExt;
                        reader.temp_v=v;
                        reader.onloadend = function(e) {
                            var target = e.target;
                            var fileContent = target.result;
                            if (target.error==null && fileContent!=null) {
                                var itemFile=rsrcDir.rel(target.temp_itemName+target.temp_itemExt);
                                itemFile.setBytes(fileContent);
                                target.temp_v.url="ls:"+itemFile.relPath(prj.getDir());// fileContent;
                                for (var i=0;i<files.length;i++) {
                                    if (files[i]==target.temp_file) {
                                        readFiles[i]=target.temp_v;
                                        break;
                                    }
                                }
                            }
                            readCnt++;
                            if (readCnt == readFileSum) {
                                addAry(readFiles);
                            }
                        };
                        reader.readAsArrayBuffer(file);
                    }
                }

                if (notReadFiles.length>0) {
                    var mes="このファイルは追加できません：\n";
                    notReadFiles.forEach(function(f){
                        if (f) mes+=f.name+"\n";
                    });
                    alert(mes);
                }
                if (existsFiles.length>0) {
                    var mes="同じ名前のファイルが既に登録されています：\n";
                    existsFiles.forEach(function(f){
                        if (f) {
                            var fNameTemp=f.existsFile.url;
                            var fName=fNameTemp.substring(fNameTemp.lastIndexOf("/")+1,fNameTemp.length);
                            mes+=f.name+" ⇒ "+fName+"("+f.existsFile.name+") と重複\n";
                            //mes+=f.name+" ("+f.existsFile.name+")\n";
                        }
                    });
                    alert(mes);
                }
                return false;
            }
            function s(e) {
                e.stopPropagation();
                e.preventDefault();

            }
            function genItemUI(item) {
                function detail() {
                    if (mediaType=="sound") return;
                    ImageDetailEditor.show(item, prj, item.name, {
                        onclose: function () {
                            prj.setResource(rsrc);
                            reload();
                        }
                    });
                }
                function del() {
                    for (var i=items.length-1; i>=0 ; i--) {
                        if (items[i]===item) {
                            var r=Assets.resolve( items[i].url, prj,{asFile:1});
                            if (FS.isFile(r) && rsrcDir.contains(r)) {
                                console.log(r.path()," is removed.");
                                r.rm();
                            }
                            items.splice(i,1);
                            break;
                        }
                    }
                    update();
                }
                function up() {
                    for (var i=items.length-1; i>=1 ; i--) {
                        if (items[i]===item) {
                            items.splice(i,1);
                            items.splice(i-1,0,item);
                            break;
                        }
                    }
                    update();
                }
                function down() {
                    for (var i=items.length-2; i>=0 ; i--) {
                        if (items[i]===item) {
                            items.splice(i,1);
                            items.splice(i+1,0,item);
                            break;
                        }
                    }
                    update();
                }

                var res=UI("div",{style:"float:left;"},
                        ["canvas",{$var:"c",width:100,height:100,"class":"clickable",on:{click: detail}}],
                        ["div",{style:"float:right;"},
                        ["button",{on:{click:del}}, "×"],["br"],
                        ["button",{on:{click:up}}, "←"],["br"],
                        ["button",{on:{click:down}}, "→"]],
                        ["div",
                            ["input", {$var:"name", size:12,value:item.name}]
                            ]
                   );
                draw(convURL(item.url),res.$vars.c[0]);
                var v=res.$vars;
                v.data=item;
                return res;
            }
            function add(v) {
                items.push(v || mediaInfo.newItem());
                update();
            }
            function addAry(vAry) {
                vAry.forEach(function(v){
                    if (v) items.push(v || mediaInfo.newItem());
                });
                update();
            }
            function renameUnique(v) {
                var name=v.name;
                var rename=name;
                var cnt=1;
                for (var i=tempFiles.length-1; i>=0 ; i--) {
                    var o=tempFiles[i];
                    if (o.name==rename) {
                        rename=name+"_"+cnt;
                        cnt++;
                        i=tempFiles.length;
                        continue;
                    }
                }
                v.name=rename;
            }
        }
        function update() {
            itemUIs.forEach(function (itemUI) {
                var v=itemUI.$vars;
                var item=v.data;
                item.name=v.name.val();
            });
            console.log(rsrc);
            prj.setResource(rsrc);
            reload();
        }
        function cleanFiles() {
            var items=rsrc[mediaInfo.key];
            Auth.currentUser(function (u,ct) {
                if (!u) return;
                var rtf=[];
                items.forEach(function (item) {
                    var a,ogg;
                    if (a=Blob.isBlobURL(item.url)) {
                        rtf.push(a.fileName);
                        ogg=a.fileName.replace(/\.(mp3|mp4|m4a)$/,".ogg");
                        if (ogg!=a.fileName) rtf.push(ogg);
                    }
                });
                var data={
                        user:u,
                        project:prj.getName(),
                        mediaType:mediaType,
                        csrfToken:ct,
                        retainFileNames:JSON.stringify(rtf)
                };
                console.log("retainBlobs",data);
                //TODO: urlchange!
                $.ajax({url:WebSite.serverTop+"/retainBlobs",type:"get",
                    data:data
                });
            });
            var cleanFile={};
            if (rsrcDir.exists()) {
                rsrcDir.each(function (f) {
                    cleanFile["ls:"+f.relPath(prj.getDir())]=f;
                });
            }
            rsrc=prj.getResource();
            items.forEach(function (item){
                delete cleanFile[item.url];
                delete cleanFile[item.url.replace(/\.(mp3|mp4|m4a)$/,".ogg")];
            });
            console.log(cleanFile);
            /*for (var ci in cleanFile) {
                var cf=cleanFile[ci];
                console.log(cf+" is removed");
                cf.rm();
            }*/
        }
        function toi(s) {
            if (!s || s=="") return undefined;
            return parseInt(s);
        }
        reload();
        return d.dialog({
            modal:true,
            width: 800,
            height: 500,
            close: function () {
                update();
                cleanFiles();
                if (mediaType=="sound" && rsrcDir.exists()) {
                    OggConverter.convert(rsrcDir);
                }
            }
        });
    };
    function draw(img, canvas) {
        if (typeof img=="string") {
            var i=new Image();
            i.onload=function () {
                draw(i,canvas);
            };
            i.src=img;
            return i;
        }
        var cw=canvas.width;
        var ch=canvas.height;
        var cctx=canvas.getContext("2d");
        var width=img.width;
        var height=img.height;
        var calcw=ch/height*width; // calch=ch
        var calch=cw/width*height; // calcw=cw
        if (calch>ch) calch=ch;
        if (calcw>cw) calcw=cw;
        cctx.clearRect(0,0,cw,ch);
        var marginw=Math.floor((cw-calcw)/2);
        var marginh=Math.floor((ch-calch)/2);
        cctx.drawImage(img,
        0,0,width, height,
        marginw,marginh,calcw, calch );
    }
    return ResEditor;
});
<|endoftext|>


<commit_before>// MAP DATA REQUEST CALCULATORS //////////////////////////////////////
// Ingress Intel splits up requests for map data (portals, links,
// fields) into tiles. To get data for the current viewport (i.e. what
// is currently visible) it first calculates which tiles intersect.
// For all those tiles, it then calculates the lat/lng bounds of that
// tile and a quadkey. Both the bounds and the quadkey are “somewhat”
// required to get complete data.
//
// Conversion functions courtesy of
// http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames

window.setupDataTileParams = function() {
  // default values - used to fall back to if we can't detect those used in stock intel
  var DEFAULT_ZOOM_TO_TILES_PER_EDGE = [1,1,1,40,40,80,80,320,1000,2000,2000,4000,8000,16000,16000,32000];
  var DEFAULT_ZOOM_TO_LEVEL = [8,8,8,8,7,7,7,6,6,5,4,4,3,2,2,1,1];

  // stock intel doesn't have this array (they use a switch statement instead), but this is far neater
  var DEFAULT_ZOOM_TO_LINK_LENGTH = [200000,200000,200000,200000,200000,60000,60000,10000,5000,2500,2500,800,300,0,0];

  window.TILE_PARAMS = {};

  // not in stock to detect - we'll have to assume the above values...
  window.TILE_PARAMS.ZOOM_TO_LINK_LENGTH = DEFAULT_ZOOM_TO_LINK_LENGTH;

  if (niantic_params.ZOOM_TO_LEVEL && niantic_params.TILES_PER_EDGE) {
    window.TILE_PARAMS.ZOOM_TO_LEVEL = niantic_params.ZOOM_TO_LEVEL;
    window.TILE_PARAMS.TILES_PER_EDGE = niantic_params.TILES_PER_EDGE;

    // lazy numerical array comparison
    if ( JSON.stringify(niantic_params.ZOOM_TO_LEVEL) != JSON.stringify(DEFAULT_ZOOM_TO_LEVEL)) {
      console.warn('Tile parameter ZOOM_TO_LEVEL have changed in stock intel. Detected correct values, but code should be updated');
      debugger;
    }
    if ( JSON.stringify(niantic_params.TILES_PER_EDGE) != JSON.stringify(DEFAULT_ZOOM_TO_TILES_PER_EDGE)) {
      console.warn('Tile parameter TILES_PER_EDGE have changed in stock intel. Detected correct values, but code should be updated');
      debugger;
    }

  } else {
    dialog({
      title: 'IITC Warning',
      html: "
IITC failed to detect the ZOOM_TO_LEVEL and/or TILES_PER_EDGE settings from the stock intel site.
"
           +"
IITC is now using fallback default values. However, if detection has failed it's likely the values have changed."
           +" IITC may not load the map if these default values are wrong.
",
    });

    window.TILE_PARAMS.ZOOM_TO_LEVEL = DEFAULT_ZOOM_TO_LEVEL;
    window.TILE_PARAMS.TILES_PER_EDGE = DEFAULT_ZOOM_TO_TILES_PER_EDGE;
  }

}

window.debugMapZoomParameters = function() {

  //for debug purposes, log the tile params used for each zoom level
  console.log('DEBUG: Map Zoom Parameters');
  var doneZooms = {};
  for (var z=MIN_ZOOM; z<=21; z++) {
    var ourZoom = getDataZoomForMapZoom(z);
    console.log('DEBUG: map zoom '+z+': IITC requests '+ourZoom+(ourZoom!=z?' instead':''));
    if (!doneZooms[ourZoom]) {
      var params = getMapZoomTileParameters(ourZoom);
      var msg = 'DEBUG: data zoom '+ourZoom;
      if (params.hasPortals) {
        msg += ' has portals, L'+params.level+'+';
      } else {
        msg += ' NO portals (was L'+params.level+'+)';
      }
      msg += ', minLinkLength='+params.minLinkLength;
      msg += ', tiles per edge='+params.tilesPerEdge;
      console.log(msg);
      doneZooms[ourZoom] = true;
    }
  }
}

window.getMapZoomTileParameters = function(zoom) {

  // the current API allows the client to request a minimum portal level. the window.TILE_PARAMS.ZOOM_TO_LEVEL list are minimums
  // however, in my view, this can return excessive numbers of portals in many cases. let's try an optional reduction
  // of detail level at some zoom levels

  var level = window.TILE_PARAMS.ZOOM_TO_LEVEL[zoom] || 0;  // default to level 0 (all portals) if not in array

//  if (window.CONFIG_ZOOM_SHOW_LESS_PORTALS_ZOOMED_OUT) {
//    if (level <= 7 && level >= 4) {
//      // reduce portal detail level by one - helps reduce clutter
//      level = level+1;
//    }
//  }

  var maxTilesPerEdge = window.TILE_PARAMS.TILES_PER_EDGE[window.TILE_PARAMS.TILES_PER_EDGE.length-1];

  return {
    level: level,
    maxLevel: window.TILE_PARAMS.ZOOM_TO_LEVEL[zoom] || 0,  // for reference, for log purposes, etc
    tilesPerEdge: window.TILE_PARAMS.TILES_PER_EDGE[zoom] || maxTilesPerEdge,
    minLinkLength: window.TILE_PARAMS.ZOOM_TO_LINK_LENGTH[zoom] || 0,
    hasPortals: zoom >= window.TILE_PARAMS.ZOOM_TO_LINK_LENGTH.length,  // no portals returned at all when link length limits things
    zoom: zoom  // include the zoom level, for reference
  };
}

window.getDataZoomForMapZoom = function(zoom) {
  // we can fetch data at a zoom level different to the map zoom.

  //NOTE: the specifics of this are tightly coupled with the above ZOOM_TO_LEVEL and TILES_PER_EDGE arrays

  // firstly, some of IITCs zoom levels, depending on base map layer, can be higher than stock. limit zoom level
  // (stock site max zoom may vary depending on google maps detail in the area - 20 or 21 max is common)
  if (zoom > 21) {
    zoom = 21;
  }

  if (window.CONFIG_ZOOM_SHOW_MORE_PORTALS) {
    // as of 2015-06-25 stock site update, all zoom levels that retrieve portals (15+) use the same tile size
    // therefore, it's no more load on the servers to fake it always to show unclaimed rather than L1+
    if (zoom >= 15 && zoom <= 16) {
      zoom = 17;
    }
  }

  if (!window.CONFIG_ZOOM_DEFAULT_DETAIL_LEVEL) {

    // to improve the cacheing performance, we try and limit the number of zoom levels we retrieve data for
    // to avoid impacting server load, we keep ourselves restricted to a zoom level with the sane numbre
    // of tilesPerEdge and portal levels visible

    var origTileParams = getMapZoomTileParameters(zoom);

    while (zoom > MIN_ZOOM) {
      var newTileParams = getMapZoomTileParameters(zoom-1);

      if ( newTileParams.tilesPerEdge != origTileParams.tilesPerEdge
        || newTileParams.hasPortals != origTileParams.hasPortals
        || newTileParams.level*newTileParams.hasPortals != origTileParams.level*origTileParams.hasPortals  // multiply by 'hasPortals' bool - so comparison does not matter when no portals available
      ) {
        // switching to zoom-1 would result in a different detail level - so we abort changing things
        break;
      } else {
        // changing to zoom = zoom-1 results in identical tile parameters - so we can safely step back
        // with no increase in either server load or number of requests
        zoom = zoom-1;
      }
    }

  }

  return zoom;
}

window.lngToTile = function(lng, params) {
  return Math.floor((lng + 180) / 360 * params.tilesPerEdge);
}

window.latToTile = function(lat, params) {
  return Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) +
    1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * params.tilesPerEdge);
}

window.tileToLng = function(x, params) {
  return x / params.tilesPerEdge * 360 - 180;
}

window.tileToLat = function(y, params) {
  var n = Math.PI - 2 * Math.PI * y / params.tilesPerEdge;
  return 180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)));
}

window.pointToTileId = function(params, x, y) {
//change to quadkey construction
//as of 2014-05-06: zoom_x_y_minlvl_maxlvl_maxhealth

  return params.zoom + "_" + x + "_" + y + "_" + params.level + "_8_100";
}

window.getResonatorLatLng = function(dist, slot, portalLatLng) {
  // offset in meters
  var dn = dist*SLOT_TO_LAT[slot];
  var de = dist*SLOT_TO_LNG[slot];

  // Coordinate offset in radians
  var dLat = dn/EARTH_RADIUS;
  var dLon = de/(EARTH_RADIUS*Math.cos(Math.PI/180*portalLatLng[0]));

  // OffsetPosition, decimal degrees
  var lat0 = portalLatLng[0] + dLat * 180/Math.PI;
  var lon0 = portalLatLng[1] + dLon * 180/Math.PI;

  return [lat0, lon0];
}
<commit_msg>remove 'show-more-portals' config case - not currently needed, so not messing with zoom level is more stock-like<commit_after>// MAP DATA REQUEST CALCULATORS //////////////////////////////////////
// Ingress Intel splits up requests for map data (portals, links,
// fields) into tiles. To get data for the current viewport (i.e. what
// is currently visible) it first calculates which tiles intersect.
// For all those tiles, it then calculates the lat/lng bounds of that
// tile and a quadkey. Both the bounds and the quadkey are “somewhat”
// required to get complete data.
//
// Conversion functions courtesy of
// http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames

window.setupDataTileParams = function() {
  // default values - used to fall back to if we can't detect those used in stock intel
  var DEFAULT_ZOOM_TO_TILES_PER_EDGE = [1,1,1,40,40,80,80,320,1000,2000,2000,4000,8000,16000,16000,32000];
  var DEFAULT_ZOOM_TO_LEVEL = [8,8,8,8,7,7,7,6,6,5,4,4,3,2,2,1,1];

  // stock intel doesn't have this array (they use a switch statement instead), but this is far neater
  var DEFAULT_ZOOM_TO_LINK_LENGTH = [200000,200000,200000,200000,200000,60000,60000,10000,5000,2500,2500,800,300,0,0];

  window.TILE_PARAMS = {};

  // not in stock to detect - we'll have to assume the above values...
  window.TILE_PARAMS.ZOOM_TO_LINK_LENGTH = DEFAULT_ZOOM_TO_LINK_LENGTH;

  if (niantic_params.ZOOM_TO_LEVEL && niantic_params.TILES_PER_EDGE) {
    window.TILE_PARAMS.ZOOM_TO_LEVEL = niantic_params.ZOOM_TO_LEVEL;
    window.TILE_PARAMS.TILES_PER_EDGE = niantic_params.TILES_PER_EDGE;

    // lazy numerical array comparison
    if ( JSON.stringify(niantic_params.ZOOM_TO_LEVEL) != JSON.stringify(DEFAULT_ZOOM_TO_LEVEL)) {
      console.warn('Tile parameter ZOOM_TO_LEVEL have changed in stock intel. Detected correct values, but code should be updated');
      debugger;
    }
    if ( JSON.stringify(niantic_params.TILES_PER_EDGE) != JSON.stringify(DEFAULT_ZOOM_TO_TILES_PER_EDGE)) {
      console.warn('Tile parameter TILES_PER_EDGE have changed in stock intel. Detected correct values, but code should be updated');
      debugger;
    }

  } else {
    dialog({
      title: 'IITC Warning',
      html: "
IITC failed to detect the ZOOM_TO_LEVEL and/or TILES_PER_EDGE settings from the stock intel site.
"
           +"
IITC is now using fallback default values. However, if detection has failed it's likely the values have changed."
           +" IITC may not load the map if these default values are wrong.
",
    });

    window.TILE_PARAMS.ZOOM_TO_LEVEL = DEFAULT_ZOOM_TO_LEVEL;
    window.TILE_PARAMS.TILES_PER_EDGE = DEFAULT_ZOOM_TO_TILES_PER_EDGE;
  }

}

window.debugMapZoomParameters = function() {

  //for debug purposes, log the tile params used for each zoom level
  console.log('DEBUG: Map Zoom Parameters');
  var doneZooms = {};
  for (var z=MIN_ZOOM; z<=21; z++) {
    var ourZoom = getDataZoomForMapZoom(z);
    console.log('DEBUG: map zoom '+z+': IITC requests '+ourZoom+(ourZoom!=z?' instead':''));
    if (!doneZooms[ourZoom]) {
      var params = getMapZoomTileParameters(ourZoom);
      var msg = 'DEBUG: data zoom '+ourZoom;
      if (params.hasPortals) {
        msg += ' has portals, L'+params.level+'+';
      } else {
        msg += ' NO portals (was L'+params.level+'+)';
      }
      msg += ', minLinkLength='+params.minLinkLength;
      msg += ', tiles per edge='+params.tilesPerEdge;
      console.log(msg);
      doneZooms[ourZoom] = true;
    }
  }
}

window.getMapZoomTileParameters = function(zoom) {

  // the current API allows the client to request a minimum portal level. the window.TILE_PARAMS.ZOOM_TO_LEVEL list are minimums
  // however, in my view, this can return excessive numbers of portals in many cases. let's try an optional reduction
  // of detail level at some zoom levels

  var level = window.TILE_PARAMS.ZOOM_TO_LEVEL[zoom] || 0;  // default to level 0 (all portals) if not in array

//  if (window.CONFIG_ZOOM_SHOW_LESS_PORTALS_ZOOMED_OUT) {
//    if (level <= 7 && level >= 4) {
//      // reduce portal detail level by one - helps reduce clutter
//      level = level+1;
//    }
//  }

  var maxTilesPerEdge = window.TILE_PARAMS.TILES_PER_EDGE[window.TILE_PARAMS.TILES_PER_EDGE.length-1];

  return {
    level: level,
    maxLevel: window.TILE_PARAMS.ZOOM_TO_LEVEL[zoom] || 0,  // for reference, for log purposes, etc
    tilesPerEdge: window.TILE_PARAMS.TILES_PER_EDGE[zoom] || maxTilesPerEdge,
    minLinkLength: window.TILE_PARAMS.ZOOM_TO_LINK_LENGTH[zoom] || 0,
    hasPortals: zoom >= window.TILE_PARAMS.ZOOM_TO_LINK_LENGTH.length,  // no portals returned at all when link length limits things
    zoom: zoom  // include the zoom level, for reference
  };
}

window.getDataZoomForMapZoom = function(zoom) {
  // we can fetch data at a zoom level different to the map zoom.

  //NOTE: the specifics of this are tightly coupled with the above ZOOM_TO_LEVEL and TILES_PER_EDGE arrays

  // firstly, some of IITCs zoom levels, depending on base map layer, can be higher than stock. limit zoom level
  // (stock site max zoom may vary depending on google maps detail in the area - 20 or 21 max is common)
  if (zoom > 21) {
    zoom = 21;
  }

// as of the 2015-06-26 niantic update, unclaimed portals are returned when we ask for L1+ - so this
// is not needed at all
//  if (window.CONFIG_ZOOM_SHOW_MORE_PORTALS) {
//    // as of 2015-06-25 stock site update, all zoom levels that retrieve portals (15+) use the same tile size
//    // therefore, it's no more load on the servers to fake it always to show unclaimed rather than L1+
//    if (zoom >= 15 && zoom <= 16) {
//      zoom = 17;
//    }
//  }

  if (!window.CONFIG_ZOOM_DEFAULT_DETAIL_LEVEL) {

    // to improve the cacheing performance, we try and limit the number of zoom levels we retrieve data for
    // to avoid impacting server load, we keep ourselves restricted to a zoom level with the sane numbre
    // of tilesPerEdge and portal levels visible

    var origTileParams = getMapZoomTileParameters(zoom);

    while (zoom > MIN_ZOOM) {
      var newTileParams = getMapZoomTileParameters(zoom-1);

      if ( newTileParams.tilesPerEdge != origTileParams.tilesPerEdge
        || newTileParams.hasPortals != origTileParams.hasPortals
        || newTileParams.level*newTileParams.hasPortals != origTileParams.level*origTileParams.hasPortals  // multiply by 'hasPortals' bool - so comparison does not matter when no portals available
      ) {
        // switching to zoom-1 would result in a different detail level - so we abort changing things
        break;
      } else {
        // changing to zoom = zoom-1 results in identical tile parameters - so we can safely step back
        // with no increase in either server load or number of requests
        zoom = zoom-1;
      }
    }

  }

  return zoom;
}

window.lngToTile = function(lng, params) {
  return Math.floor((lng + 180) / 360 * params.tilesPerEdge);
}

window.latToTile = function(lat, params) {
  return Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) +
    1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * params.tilesPerEdge);
}

window.tileToLng = function(x, params) {
  return x / params.tilesPerEdge * 360 - 180;
}

window.tileToLat = function(y, params) {
  var n = Math.PI - 2 * Math.PI * y / params.tilesPerEdge;
  return 180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)));
}

window.pointToTileId = function(params, x, y) {
//change to quadkey construction
//as of 2014-05-06: zoom_x_y_minlvl_maxlvl_maxhealth

  return params.zoom + "_" + x + "_" + y + "_" + params.level + "_8_100";
}

window.getResonatorLatLng = function(dist, slot, portalLatLng) {
  // offset in meters
  var dn = dist*SLOT_TO_LAT[slot];
  var de = dist*SLOT_TO_LNG[slot];

  // Coordinate offset in radians
  var dLat = dn/EARTH_RADIUS;
  var dLon = de/(EARTH_RADIUS*Math.cos(Math.PI/180*portalLatLng[0]));

  // OffsetPosition, decimal degrees
  var lat0 = portalLatLng[0] + dLat * 180/Math.PI;
  var lon0 = portalLatLng[1] + dLon * 180/Math.PI;

  return [lat0, lon0];
}
<|endoftext|>


<commit_before>/*
 * responsive-carousel dynamic containers extension
 * https://github.com/filamentgroup/responsive-carousel
 *
 * Copyright (c) 2012 Filament Group, Inc.
 * Licensed under the MIT, GPL licenses.
 */

(function($) {

	var pluginName = "carousel",
		initSelector = "." + pluginName,
		itemClass = pluginName + "-item",
		activeClass = pluginName + "-active",
		rowAttr = "data-" + pluginName + "-slide",
		dynamicContainers = {
			_assessContainers: function(){
				var $self = $( this ),
					$rows = $self.find( "[" + rowAttr + "]" ),
					$activeItem = $rows.filter( "." + activeClass ).children( 0 ),
					$kids = $rows.children(),
					sets = [];

				if( !$rows.length ){
					$kids = $( this ).find( "." + itemClass );
				}
				else{
					$kids.appendTo( $self );
					$rows.remove();
				}

				$kids
					.removeClass( itemClass + " " + activeClass )
					.each(function(){
						var prev = $( this ).prev();

						if( !prev.length || $( this ).offset().top !== prev.offset().top ){
							sets.push([]);
						}

						sets[ sets.length -1 ].push( $( this ) );
					});

				for( var i = 0; i < sets.length; i++ ){
					var $row = $( "

" );
					for( var j = 0; j < sets[ i ].length; j++ ){
						$row.append( sets[ i ][ j ] );
					}

					$row.prependTo( $self );
				}

				$self[ pluginName ]( "update" );

				$self.find( "." + itemClass ).eq( 0 ).addClass( activeClass );
			},

			_dynamicContainerEvents: function(){
				var $self = $( this ),
					timeout;

				// on init
				$self[ pluginName ]( "_assessContainers" );

				// and on resize
				$( window )
					.bind( "resize", function( e ){
						if( timeout ){
							clearTimeout( timeout );
						}

						timeout = setTimeout( function(){
							$self[ pluginName ]( "_assessContainers" );
						}, 200 );
					} );
			}
		};

	// add methods
	$.extend( $.fn[ pluginName ].prototype, dynamicContainers );

	// DOM-ready auto-init
	$( initSelector ).live( "create." + pluginName, function(){
		$( this )[ pluginName ]( "_dynamicContainerEvents" );
	} );

}(jQuery));<commit_msg>insert before the nav rather than prepend<commit_after>/*
 * responsive-carousel dynamic containers extension
 * https://github.com/filamentgroup/responsive-carousel
 *
 * Copyright (c) 2012 Filament Group, Inc.
 * Licensed under the MIT, GPL licenses.
 */

(function($) {

	var pluginName = "carousel",
		initSelector = "." + pluginName,
		itemClass = pluginName + "-item",
		activeClass = pluginName + "-active",
		rowAttr = "data-" + pluginName + "-slide",
		dynamicContainers = {
			_assessContainers: function(){
				var $self = $( this ),
					$rows = $self.find( "[" + rowAttr + "]" ),
					$activeItem = $rows.filter( "." + activeClass ).children( 0 ),
					$kids = $rows.children(),
					$nav = $self.find( "." + pluginName + "-nav" ),
					sets = [];

				if( !$rows.length ){
					$kids = $( this ).find( "." + itemClass );
				}
				else{
					$kids.appendTo( $self );
					$rows.remove();
				}

				$kids
					.removeClass( itemClass + " " + activeClass )
					.each(function(){
						var prev = $( this ).prev();

						if( !prev.length || $( this ).offset().top !== prev.offset().top ){
							sets.push([]);
						}

						sets[ sets.length -1 ].push( $( this ) );
					});

				for( var i = 0; i < sets.length; i++ ){
					var $row = $( "

" );
					for( var j = 0; j < sets[ i ].length; j++ ){
						$row.append( sets[ i ][ j ] );
					}

					$row.insertBefore( $nav );
				}

				$self[ pluginName ]( "update" );

				$self.find( "." + itemClass ).eq( 0 ).addClass( activeClass );
			},

			_dynamicContainerEvents: function(){
				var $self = $( this ),
					timeout;

				// on init
				$self[ pluginName ]( "_assessContainers" );

				// and on resize
				$( window )
					.bind( "resize", function( e ){
						if( timeout ){
							clearTimeout( timeout );
						}

						timeout = setTimeout( function(){
							$self[ pluginName ]( "_assessContainers" );
						}, 200 );
					} );
			}
		};

	// add methods
	$.extend( $.fn[ pluginName ].prototype, dynamicContainers );

	// DOM-ready auto-init
	$( initSelector ).live( "create." + pluginName, function(){
		$( this )[ pluginName ]( "_dynamicContainerEvents" );
	} );

}(jQuery));<|endoftext|>


<commit_before>function Cell(nodeId, owner, position, mass, gameServer) {
    this.nodeId = nodeId;
    this.owner = owner; // playerTracker that owns this cell
    this.gameServer = gameServer;

    this.tickOfBirth = 0;
    this.color = { r: 0, g: 0, b: 0 };
    this.position = { x: 0, y: 0 };
    this._size = 0;
    this._mass = 0;
    this._squareSize = 0;
    this.cellType = -1;     // 0 = Player Cell, 1 = Food, 2 = Virus, 3 = Ejected Mass
    this.isSpiked = false;  // If true, then this cell has spikes around it
    this.isAgitated = false;// If true, then this cell has waves on it's outline
    this.killedBy = null;   // Cell that ate this cell

    this.boostDistance = 0;
    this.boostDirection = { x: 1, y: 0, angle: 0 };
    this.ejector = null;

    if (gameServer != null)
        this.tickOfBirth = gameServer.getTick();
    if (mass != null)
        this.setMass(mass);
    if (position != null)
        this.setPosition(position);
}

module.exports = Cell;

// Fields not defined by the constructor are considered private and need a getter/setter to access from a different class

Cell.prototype.getName = function() {
    if (this.owner)
        return this.owner.name;
    return "";
};

Cell.prototype.getSkin = function () {
    if (this.owner)
        return this.owner.skin;
    return "";
};

Cell.prototype.setColor = function(color) {
    this.color.r = color.r;
    this.color.g = color.g;
    this.color.b = color.b;
};

Cell.prototype.getColor = function() {
    return this.color;
};

Cell.prototype.getType = function() {
    return this.cellType;
};

Cell.prototype.getSize = function() {
    return this._size;
};

Cell.prototype.getMass = function () {
    return this._mass;
};

Cell.prototype.getSquareSize = function () {
    return this._squareSize;
};

Cell.prototype.setMass = function (mass) {
    this._size = Math.sqrt(mass * 100);
    this._squareSize = this._size * this._size;
    this._mass = this._squareSize / 100;
    if (this.owner)
        this.owner.massChanged();
};

Cell.prototype.addMass = function (n) {
    // Check if the cell needs to autosplit before adding mass
    var newMass = this.getMass() + n;
    this.setMass(newMass);
    if (this.getMass() <= this.gameServer.config.playerMaxMass)
        return;
    if (this.owner.cells.length >= this.gameServer.config.playerMaxCells)
        return;
    var splitMass = this.getMass() / 2;
    var randomAngle = Math.random() * 6.28; // Get random angle
    this.gameServer.splitPlayerCell(this.owner, this, randomAngle, splitMass);
};

Cell.prototype.getSpeed = function() {
    var speed = 2.1106 / Math.pow(this.getSize(), 0.449);
    // tickStep=40ms
    return speed * 40 * this.gameServer.config.playerSpeed;
};

Cell.prototype.setAngle = function(angle) {
    this.boostDirection = {
        x: Math.sin(angle),
        y: Math.cos(angle),
        angle: angle
    };
};

Cell.prototype.getAngle = function() {
    return this.boostDirection.angle;
};

// Returns cell age in ticks for specified game tick
Cell.prototype.getAge = function (tick) {
    if (this.tickOfBirth == null) return 0;
    return Math.max(0, tick - this.tickOfBirth);
}

Cell.prototype.getKiller = function() {
    return this.killedBy;
};

Cell.prototype.setKiller = function(cell) {
    this.killedBy = cell;
};

Cell.prototype.setPosition = function (pos) {
    if (isNaN(pos.x) || isNaN(pos.y)) {
        console.log("[ERROR] Cell.setPosition: NaN");
        return;
    }
    this.position.x = pos.x;
    this.position.y = pos.y;
};

// Functions

Cell.prototype.setBoost = function (distance, angle) {
    if (isNaN(angle)) angle = 0;

    this.boostDistance = distance;
    this.setAngle(angle);
};

Cell.prototype.move = function (border) {
    if (this.boostDistance <= 0) {
        this.boostDistance = 0;
        return;
    }

    var speed = Math.sqrt(this.boostDistance * this.boostDistance / 100);
    var speed = Math.min(speed, 78);                // limit max speed with sqrt(780*780/100)
    speed = Math.min(speed, this.boostDistance);    // avoid overlap 0
    this.boostDistance -= speed;
    if (this.boostDistance < 1) this.boostDistance = 0;

    var v = this.clipVelocity(
        { x: this.boostDirection.x * speed, y: this.boostDirection.y * speed },
        border);
    this.position.x += v.x;
    this.position.y += v.y;
    this.checkBorder(border);
}

Cell.prototype.clipVelocity = function (v, border) {
    if (isNaN(v.x) || isNaN(v.y)) {
        throw new TypeError("Cell.clipVelocity: NaN");
    };
    if (v.x == 0 && v.y == 0)
        return v; // zero move, no calculations :)
    var r = this.getSize() / 2;
    var bound = {
        minx: border.minx + r,
        miny: border.miny + r,
        maxx: border.maxx - r,
        maxy: border.maxy - r
    };
    var x = this.position.x + v.x;
    var y = this.position.y + v.y;
    // border check
    var pleft = x >= bound.minx ? null : this.getLineIntersection(
        this.position.x, this.position.y, x, y,
        bound.minx, bound.miny, bound.minx, bound.maxy);
    var pright = x <= bound.maxx ? null : this.getLineIntersection(
        this.position.x, this.position.y, x, y,
        bound.maxx, bound.miny, bound.maxx, bound.maxy);
    var ptop = y >= bound.miny ? null : this.getLineIntersection(
        this.position.x, this.position.y, x, y,
        bound.minx, bound.miny, bound.maxx, bound.miny);
    var pbottom = y <= bound.maxy ? null : this.getLineIntersection(
        this.position.x, this.position.y, x, y,
        bound.minx, bound.maxy, bound.maxx, bound.maxy);
    var ph = pleft != null ? pleft : pright;
    var pv = ptop != null ? ptop : pbottom;
    var p = ph != null ? ph : pv;
    if (p == null) {
        // inside border
        return v;
    }
    if (ph && pv) {
        // two border lines intersection => get nearest point
        var hdx = ph.x - this.position.x;
        var hdy = ph.y - this.position.y;
        var vdx = pv.x - this.position.x;
        var vdy = pv.y - this.position.y;
        if (hdx * hdx + hdy * hdy < vdx * vdx + vdy * vdy)
            p = ph;
        else
            p = pv;
    }
    // p - stop point on the border

    // reflect angle
    var angle = this.getAngle();
    if (p == ph) {
        // left/right border reflection
        angle = 2 * Math.PI - angle;
    } else {
        // top/bottom border reflection
        angle = angle <= Math.PI ? Math.PI - angle : 3 * Math.PI - angle;
    }
    this.setAngle(angle);
    // new velocity
    var lx = p.x - this.position.x;
    var ly = p.y - this.position.y;
    // calculate rest of velocity
    var ldx = v.x - lx;
    var ldy = v.y - ly;
    // update velocity and add rest to the boostDistance
    v.x = lx;
    v.y = ly;
    this.boostDistance += Math.sqrt(ldx * ldx + ldy * ldy);
    if (this.boostDistance < 1) this.boostDistance = 0;
    return v;
};

Cell.prototype.checkBorder = function (border) {
    var r = this.getSize() / 2;
    var x = this.position.x;
    var y = this.position.y;
    x = Math.max(x, border.minx + r);
    y = Math.max(y, border.miny + r);
    x = Math.min(x, border.maxx - r);
    y = Math.min(y, border.maxy - r);
    if (x != this.position.x || y != this.position.y) {
        this.setPosition({ x: x, y: y });
    }
};

// Override these

Cell.prototype.sendUpdate = function() {
    // Whether or not to include this cell in the update packet
    return true;
};

Cell.prototype.canEat = function (cell) {
    // by default cell cannot eat anyone
    return false;
};

Cell.prototype.onConsume = function(consumer, gameServer) {
    // Called when the cell is consumed
};

Cell.prototype.onAdd = function(gameServer) {
    // Called when this cell is added to the world
};

Cell.prototype.onRemove = function(gameServer) {
    // Called when this cell is removed
};

// Lib

Cell.prototype.getLineIntersection = function (p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
    var z1 = p1x - p0x;
    var z2 = p3x - p2x;
    var w1 = p1y - p0y;
    var w2 = p3y - p2y;
    var k1 = w1 * z2 - z1 * w2;
    if (k1 == 0) return null;
    var k2 = (z1 * (p2y - p0y) + w1 * (p0x - p2x)) / k1;
    var px = p2x + z2 * k2;
    var py = p2y + w2 * k2;
    if (isNaN(px) || isNaN(py)) return null;
    return { x: px, y: py };
}

Cell.prototype.abs = function(x) {
    return x < 0 ? -x : x;
};

Cell.prototype.getDist = function(x1, y1, x2, y2) {
    var vx = x2 - x1;
    var vy = y2 - x1;
    return Math.sqrt(vx * vx + vy * vy);
};
<commit_msg>fix mergeOverride split loop<commit_after>function Cell(nodeId, owner, position, mass, gameServer) {
    this.nodeId = nodeId;
    this.owner = owner; // playerTracker that owns this cell
    this.gameServer = gameServer;

    this.tickOfBirth = 0;
    this.color = { r: 0, g: 0, b: 0 };
    this.position = { x: 0, y: 0 };
    this._size = 0;
    this._mass = 0;
    this._squareSize = 0;
    this.cellType = -1;     // 0 = Player Cell, 1 = Food, 2 = Virus, 3 = Ejected Mass
    this.isSpiked = false;  // If true, then this cell has spikes around it
    this.isAgitated = false;// If true, then this cell has waves on it's outline
    this.killedBy = null;   // Cell that ate this cell

    this.boostDistance = 0;
    this.boostDirection = { x: 1, y: 0, angle: 0 };
    this.ejector = null;

    if (gameServer != null)
        this.tickOfBirth = gameServer.getTick();
    if (mass != null)
        this.setMass(mass);
    if (position != null)
        this.setPosition(position);
}

module.exports = Cell;

// Fields not defined by the constructor are considered private and need a getter/setter to access from a different class

Cell.prototype.getName = function() {
    if (this.owner)
        return this.owner.name;
    return "";
};

Cell.prototype.getSkin = function () {
    if (this.owner)
        return this.owner.skin;
    return "";
};

Cell.prototype.setColor = function(color) {
    this.color.r = color.r;
    this.color.g = color.g;
    this.color.b = color.b;
};

Cell.prototype.getColor = function() {
    return this.color;
};

Cell.prototype.getType = function() {
    return this.cellType;
};

Cell.prototype.getSize = function() {
    return this._size;
};

Cell.prototype.getMass = function () {
    return this._mass;
};

Cell.prototype.getSquareSize = function () {
    return this._squareSize;
};

Cell.prototype.setMass = function (mass) {
    this._size = Math.sqrt(mass * 100);
    this._squareSize = this._size * this._size;
    this._mass = this._squareSize / 100;
    if (this.owner)
        this.owner.massChanged();
};

Cell.prototype.addMass = function (n) {
    // Check if the cell needs to autosplit before adding mass
    var newMass = this.getMass() + n;
    this.setMass(newMass);
    if (!this.owner || this.owner.mergeOverride)
        return;
    if (this.owner.cells.length >= this.gameServer.config.playerMaxCells)
        return;
    if (this.getMass() <= this.gameServer.config.playerMaxMass)
        return;
    var splitMass = this.getMass() / 2;
    var randomAngle = Math.random() * 6.28; // Get random angle
    this.gameServer.splitPlayerCell(this.owner, this, randomAngle, splitMass);
};

Cell.prototype.getSpeed = function() {
    var speed = 2.1106 / Math.pow(this.getSize(), 0.449);
    // tickStep=40ms
    return speed * 40 * this.gameServer.config.playerSpeed;
};

Cell.prototype.setAngle = function(angle) {
    this.boostDirection = {
        x: Math.sin(angle),
        y: Math.cos(angle),
        angle: angle
    };
};

Cell.prototype.getAngle = function() {
    return this.boostDirection.angle;
};

// Returns cell age in ticks for specified game tick
Cell.prototype.getAge = function (tick) {
    if (this.tickOfBirth == null) return 0;
    return Math.max(0, tick - this.tickOfBirth);
}

Cell.prototype.getKiller = function() {
    return this.killedBy;
};

Cell.prototype.setKiller = function(cell) {
    this.killedBy = cell;
};

Cell.prototype.setPosition = function (pos) {
    if (isNaN(pos.x) || isNaN(pos.y)) {
        console.log("[ERROR] Cell.setPosition: NaN");
        return;
    }
    this.position.x = pos.x;
    this.position.y = pos.y;
};

// Functions

Cell.prototype.setBoost = function (distance, angle) {
    if (isNaN(angle)) angle = 0;

    this.boostDistance = distance;
    this.setAngle(angle);
};

Cell.prototype.move = function (border) {
    if (this.boostDistance <= 0) {
        this.boostDistance = 0;
        return;
    }

    var speed = Math.sqrt(this.boostDistance * this.boostDistance / 100);
    var speed = Math.min(speed, 78);                // limit max speed with sqrt(780*780/100)
    speed = Math.min(speed, this.boostDistance);    // avoid overlap 0
    this.boostDistance -= speed;
    if (this.boostDistance < 1) this.boostDistance = 0;

    var v = this.clipVelocity(
        { x: this.boostDirection.x * speed, y: this.boostDirection.y * speed },
        border);
    this.position.x += v.x;
    this.position.y += v.y;
    this.checkBorder(border);
}

Cell.prototype.clipVelocity = function (v, border) {
    if (isNaN(v.x) || isNaN(v.y)) {
        throw new TypeError("Cell.clipVelocity: NaN");
    };
    if (v.x == 0 && v.y == 0)
        return v; // zero move, no calculations :)
    var r = this.getSize() / 2;
    var bound = {
        minx: border.minx + r,
        miny: border.miny + r,
        maxx: border.maxx - r,
        maxy: border.maxy - r
    };
    var x = this.position.x + v.x;
    var y = this.position.y + v.y;
    // border check
    var pleft = x >= bound.minx ? null : this.getLineIntersection(
        this.position.x, this.position.y, x, y,
        bound.minx, bound.miny, bound.minx, bound.maxy);
    var pright = x <= bound.maxx ? null : this.getLineIntersection(
        this.position.x, this.position.y, x, y,
        bound.maxx, bound.miny, bound.maxx, bound.maxy);
    var ptop = y >= bound.miny ? null : this.getLineIntersection(
        this.position.x, this.position.y, x, y,
        bound.minx, bound.miny, bound.maxx, bound.miny);
    var pbottom = y <= bound.maxy ? null : this.getLineIntersection(
        this.position.x, this.position.y, x, y,
        bound.minx, bound.maxy, bound.maxx, bound.maxy);
    var ph = pleft != null ? pleft : pright;
    var pv = ptop != null ? ptop : pbottom;
    var p = ph != null ? ph : pv;
    if (p == null) {
        // inside border
        return v;
    }
    if (ph && pv) {
        // two border lines intersection => get nearest point
        var hdx = ph.x - this.position.x;
        var hdy = ph.y - this.position.y;
        var vdx = pv.x - this.position.x;
        var vdy = pv.y - this.position.y;
        if (hdx * hdx + hdy * hdy < vdx * vdx + vdy * vdy)
            p = ph;
        else
            p = pv;
    }
    // p - stop point on the border

    // reflect angle
    var angle = this.getAngle();
    if (p == ph) {
        // left/right border reflection
        angle = 2 * Math.PI - angle;
    } else {
        // top/bottom border reflection
        angle = angle <= Math.PI ? Math.PI - angle : 3 * Math.PI - angle;
    }
    this.setAngle(angle);
    // new velocity
    var lx = p.x - this.position.x;
    var ly = p.y - this.position.y;
    // calculate rest of velocity
    var ldx = v.x - lx;
    var ldy = v.y - ly;
    // update velocity and add rest to the boostDistance
    v.x = lx;
    v.y = ly;
    this.boostDistance += Math.sqrt(ldx * ldx + ldy * ldy);
    if (this.boostDistance < 1) this.boostDistance = 0;
    return v;
};

Cell.prototype.checkBorder = function (border) {
    var r = this.getSize() / 2;
    var x = this.position.x;
    var y = this.position.y;
    x = Math.max(x, border.minx + r);
    y = Math.max(y, border.miny + r);
    x = Math.min(x, border.maxx - r);
    y = Math.min(y, border.maxy - r);
    if (x != this.position.x || y != this.position.y) {
        this.setPosition({ x: x, y: y });
    }
};

// Override these

Cell.prototype.sendUpdate = function() {
    // Whether or not to include this cell in the update packet
    return true;
};

Cell.prototype.canEat = function (cell) {
    // by default cell cannot eat anyone
    return false;
};

Cell.prototype.onConsume = function(consumer, gameServer) {
    // Called when the cell is consumed
};

Cell.prototype.onAdd = function(gameServer) {
    // Called when this cell is added to the world
};

Cell.prototype.onRemove = function(gameServer) {
    // Called when this cell is removed
};

// Lib

Cell.prototype.getLineIntersection = function (p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
    var z1 = p1x - p0x;
    var z2 = p3x - p2x;
    var w1 = p1y - p0y;
    var w2 = p3y - p2y;
    var k1 = w1 * z2 - z1 * w2;
    if (k1 == 0) return null;
    var k2 = (z1 * (p2y - p0y) + w1 * (p0x - p2x)) / k1;
    var px = p2x + z2 * k2;
    var py = p2y + w2 * k2;
    if (isNaN(px) || isNaN(py)) return null;
    return { x: px, y: py };
}

Cell.prototype.abs = function(x) {
    return x < 0 ? -x : x;
};

Cell.prototype.getDist = function(x1, y1, x2, y2) {
    var vx = x2 - x1;
    var vy = y2 - x1;
    return Math.sqrt(vx * vx + vy * vy);
};
<|endoftext|>


<commit_before>import { is } from './utils';
import * as errors from './errors';
import { EventEmitter } from 'events';
import fs from 'fs';
import path from 'path';
import yaml from 'js-yaml';
import converter from 'sass-convert';

export default class Environment extends EventEmitter {

  /**
   * @param {Logger} logger
   * @param {Boolean} strict
   */
  constructor(logger, strict = false) {
    super();

    this.logger = logger;
    this.strict = strict;

    this.on('error', error => {
      let friendlyErrors = [
        errors.SassDocError,
        converter.BinaryError,
        converter.VersionError,
      ];

      if (Array.find(friendlyErrors, c => error instanceof c)) {
        logger.error(error.message);
      } else {
        if (is.error(error) && 'stack' in error) {
          logger.error(error.stack);
        } else {
          logger.error(error);
        }
      }
    });

    if (strict) {
      this.on('warning', warning => this.emit('error', warning));
    } else {
      this.on('warning', warning => logger.warn(warning.message));
    }
  }

  /**
   * @param {Object|String} config
   */
  load(config) {
    if (!config) {
      return this.loadDefaultFile();
    }

    if (is.string(config)) {
      return this.loadFile(config);
    }

    if (is.plainObject(config)) {
      return this.loadObject(config);
    }

    this.emit('error', new errors.SassDocError(
      'Invalid `config` argument, expected string, object or undefined.'
    ));
  }

  /**
   * Merge given configuration object, excluding reserved keys.
   *
   * @param {Object} config
   */
  loadObject(config) {
    if (this.file) {
      this.file = path.resolve(this.file);
      this.dir = path.dirname(this.file);
    }

    for (let k of Object.keys(config)) {
      if (k in this) {
        return this.emit('error', new Error(
          `Reserved configuration key \`${k}\`.`
        ));
      }

      this[k] = config[k];
    }
  }

  /**
   * Get the configuration object from given file.
   *
   * If the file is not found, emit a warning and fallback to default.
   *
   * The `dir` property will be the directory of the given file or the CWD
   * if no file is given. The configuration paths should be relative to
   * it.
   *
   * The given logger will be injected in the configuration object for
   * further usage.
   *
   * @param {String} file
   */
  loadFile(file) {
    this.file = file;

    if (!this.tryLoadCurrentFile()) {
      this.emit('warning', new errors.Warning(`Config file \`${file}\` not found.`));
      this.logger.warn('Falling back to `.sassdocrc`');
      this.loadDefaultFile();
    }
  }

  /**
   * Try to load default `.sassdocrc` configuration file, or fallback
   * to an empty object.
   */
  loadDefaultFile() {
    this.file = '.sassdocrc';
    this.tryLoadCurrentFile();
  }

  /**
   * Post process the configuration to ensure `package` and `theme`
   * have uniform values.
   *
   * The `package` key is ensured to be an object. If it's a string, it's
   * required as JSON, relative to the configuration file directory.
   *
   * The `theme` key, if present and not already a function, will be
   * resolved to the actual theme function.
   */
  postProcess() {
    if (!this.dir) {
      this.dir = process.cwd();
    }

    if (!this.dest) {
      this.dest = 'sassdoc';
    }

    this.dest = this.resolve(this.dest, this.destCwd);
    this.displayDest = path.relative(process.cwd(), this.dest);

    if (!this.package) {
      this.defaultPackage();
    }

    if (typeof this.package !== 'object') {
      this.loadPackage();
    }

    if (typeof this.theme !== 'function') {
      this.loadTheme();
    }
  }

  /**
   * Process `this.package`.
   */
  loadPackage() {
    let file = this.resolve(this.package);
    this.package = this.tryParseFile(file);

    if (this.package) {
      return;
    }

    this.emit('warning', new errors.Warning(`Package file \`${file}\` not found.`));
    this.logger.warn('Falling back to `package.json`.');

    this.defaultPackage();
  }

  /**
   * Load `package.json`.
   */
  defaultPackage() {
    let file = this.resolve('package.json');
    this.package = this.tryParseFile(file);

    if (this.package) {
      return;
    }

    this.logger.warn('No package information.');
    this.package = {};
  }

  /**
   * Process `this.theme`.
   */
  loadTheme() {
    this.themeName = this.theme || 'default';

    if (this.theme === undefined) {
      return this.defaultTheme();
    }

    if (this.theme.indexOf('/') === -1) {
      return this.tryTheme(`sassdoc-theme-${this.theme}`);
    }

    let theme = this.resolve(this.theme, this.themeCwd);
    this.displayTheme = path.relative(process.cwd(), theme);

    return this.tryTheme(theme);
  }

  /**
   * Try to load given theme module, or fallback to default theme.
   *
   * @param {String} module
   */
  tryTheme(module) {
    try {
      require.resolve(module);
    } catch (err) {
      this.emit('warning', new errors.Warning(`Theme \`${this.theme}\` not found.`));
      this.logger.warn('Falling back to default theme.');
      return this.defaultTheme();
    }

    this.theme = require(module);
    let str = Object.prototype.toString;

    if (typeof this.theme !== 'function') {
      this.emit('error', new errors.SassDocError(
        `Given theme is ${str(this.theme)}, expected ${str(str)}.` // eslint-disable-line comma-spacing
      ));

      return this.defaultTheme();
    }

    if (this.theme.length !== 2) {
      this.logger.warn(
        `Given theme takes ${this.theme.length} arguments, expected 2.`
      );
    }
  }

  /**
   * Load `sassdoc-theme-default`.
   */
  defaultTheme() {
    try {
      require.resolve('sassdoc-theme-default');
    } catch (err) {
      this.emit('error', new errors.SassDocError(
        'Holy shit, the default theme was not found!'
      ));
    }

    this.theme = require('sassdoc-theme-default');
    this.themeName = 'default';
  }

  /**
   * Try to load `this.file`, and if not found, return `false`.
   *
   * @return {Boolean}
   */
  tryLoadCurrentFile() {
    let config = this.tryParseFile(this.file);

    if (!config) {
      return false;
    }

    this.load(config);

    return true;
  }

  /**
   * Try `this.parseFile` and return `false` if an `ENOENT` error
   * is thrown.
   *
   * Other exceptions are passed to the `error` event.
   *
   * @param {String} file
   * @return {*}
   */
  tryParseFile(file) {
    try {
      return this.parseFile(file);
    } catch (e) {
      if (e.code !== 'ENOENT') {
        return this.emit('error', e);
      }
    }

    return false;
  }

  /**
   * Load YAML or JSON from given file.
   *
   * @param {String} file
   * @return {*}
   */
  parseFile(file) {
    return yaml.safeLoad(fs.readFileSync(file, 'utf-8'));
  }

  /**
   * Resolve given file from `this.dir`.
   *
   * @param {String} file
   * @param {Boolean} cwd - whether it's relative to CWD (like when
   *                        defined in CLI).
   * @return {String}
   */
  resolve(file, cwd = false) {
    return path.resolve(cwd ? process.cwd() : this.dir, file);
  }
}
<commit_msg>Ensure `default` theme name is properly logged  - Refs #391<commit_after>import { is } from './utils';
import * as errors from './errors';
import { EventEmitter } from 'events';
import fs from 'fs';
import path from 'path';
import yaml from 'js-yaml';
import converter from 'sass-convert';

export default class Environment extends EventEmitter {

  /**
   * @param {Logger} logger
   * @param {Boolean} strict
   */
  constructor(logger, strict = false) {
    super();

    this.logger = logger;
    this.strict = strict;

    this.on('error', error => {
      let friendlyErrors = [
        errors.SassDocError,
        converter.BinaryError,
        converter.VersionError,
      ];

      if (Array.find(friendlyErrors, c => error instanceof c)) {
        logger.error(error.message);
      } else {
        if (is.error(error) && 'stack' in error) {
          logger.error(error.stack);
        } else {
          logger.error(error);
        }
      }
    });

    if (strict) {
      this.on('warning', warning => this.emit('error', warning));
    } else {
      this.on('warning', warning => logger.warn(warning.message));
    }
  }

  /**
   * @param {Object|String} config
   */
  load(config) {
    if (!config) {
      return this.loadDefaultFile();
    }

    if (is.string(config)) {
      return this.loadFile(config);
    }

    if (is.plainObject(config)) {
      return this.loadObject(config);
    }

    this.emit('error', new errors.SassDocError(
      'Invalid `config` argument, expected string, object or undefined.'
    ));
  }

  /**
   * Merge given configuration object, excluding reserved keys.
   *
   * @param {Object} config
   */
  loadObject(config) {
    if (this.file) {
      this.file = path.resolve(this.file);
      this.dir = path.dirname(this.file);
    }

    for (let k of Object.keys(config)) {
      if (k in this) {
        return this.emit('error', new Error(
          `Reserved configuration key \`${k}\`.`
        ));
      }

      this[k] = config[k];
    }
  }

  /**
   * Get the configuration object from given file.
   *
   * If the file is not found, emit a warning and fallback to default.
   *
   * The `dir` property will be the directory of the given file or the CWD
   * if no file is given. The configuration paths should be relative to
   * it.
   *
   * The given logger will be injected in the configuration object for
   * further usage.
   *
   * @param {String} file
   */
  loadFile(file) {
    this.file = file;

    if (!this.tryLoadCurrentFile()) {
      this.emit('warning', new errors.Warning(`Config file \`${file}\` not found.`));
      this.logger.warn('Falling back to `.sassdocrc`');
      this.loadDefaultFile();
    }
  }

  /**
   * Try to load default `.sassdocrc` configuration file, or fallback
   * to an empty object.
   */
  loadDefaultFile() {
    this.file = '.sassdocrc';
    this.tryLoadCurrentFile();
  }

  /**
   * Post process the configuration to ensure `package` and `theme`
   * have uniform values.
   *
   * The `package` key is ensured to be an object. If it's a string, it's
   * required as JSON, relative to the configuration file directory.
   *
   * The `theme` key, if present and not already a function, will be
   * resolved to the actual theme function.
   */
  postProcess() {
    if (!this.dir) {
      this.dir = process.cwd();
    }

    if (!this.dest) {
      this.dest = 'sassdoc';
    }

    this.dest = this.resolve(this.dest, this.destCwd);
    this.displayDest = path.relative(process.cwd(), this.dest);

    if (!this.package) {
      this.defaultPackage();
    }

    if (typeof this.package !== 'object') {
      this.loadPackage();
    }

    if (typeof this.theme !== 'function') {
      this.loadTheme();
    }
  }

  /**
   * Process `this.package`.
   */
  loadPackage() {
    let file = this.resolve(this.package);
    this.package = this.tryParseFile(file);

    if (this.package) {
      return;
    }

    this.emit('warning', new errors.Warning(`Package file \`${file}\` not found.`));
    this.logger.warn('Falling back to `package.json`.');

    this.defaultPackage();
  }

  /**
   * Load `package.json`.
   */
  defaultPackage() {
    let file = this.resolve('package.json');
    this.package = this.tryParseFile(file);

    if (this.package) {
      return;
    }

    this.logger.warn('No package information.');
    this.package = {};
  }

  /**
   * Process `this.theme`.
   */
  loadTheme() {
    if (this.theme === undefined) {
      return this.defaultTheme();
    }

    if (this.theme.indexOf('/') === -1) {
      return this.tryTheme(`sassdoc-theme-${this.theme}`);
    }

    let theme = this.resolve(this.theme, this.themeCwd);
    this.themeName = this.theme;
    this.displayTheme = path.relative(process.cwd(), theme);

    return this.tryTheme(theme);
  }

  /**
   * Try to load given theme module, or fallback to default theme.
   *
   * @param {String} module
   */
  tryTheme(module) {
    try {
      require.resolve(module);
    } catch (err) {
      this.emit('warning', new errors.Warning(`Theme \`${this.theme}\` not found.`));
      this.logger.warn('Falling back to default theme.');
      return this.defaultTheme();
    }

    this.theme = require(module);
    let str = Object.prototype.toString;

    if (typeof this.theme !== 'function') {
      this.emit('error', new errors.SassDocError(
        `Given theme is ${str(this.theme)}, expected ${str(str)}.` // eslint-disable-line comma-spacing
      ));

      return this.defaultTheme();
    }

    if (this.theme.length !== 2) {
      this.logger.warn(
        `Given theme takes ${this.theme.length} arguments, expected 2.`
      );
    }
  }

  /**
   * Load `sassdoc-theme-default`.
   */
  defaultTheme() {
    try {
      require.resolve('sassdoc-theme-default');
    } catch (err) {
      this.emit('error', new errors.SassDocError(
        'Holy shit, the default theme was not found!'
      ));
    }

    this.theme = require('sassdoc-theme-default');
    this.themeName = this.displayTheme = 'default';
  }

  /**
   * Try to load `this.file`, and if not found, return `false`.
   *
   * @return {Boolean}
   */
  tryLoadCurrentFile() {
    let config = this.tryParseFile(this.file);

    if (!config) {
      return false;
    }

    this.load(config);

    return true;
  }

  /**
   * Try `this.parseFile` and return `false` if an `ENOENT` error
   * is thrown.
   *
   * Other exceptions are passed to the `error` event.
   *
   * @param {String} file
   * @return {*}
   */
  tryParseFile(file) {
    try {
      return this.parseFile(file);
    } catch (e) {
      if (e.code !== 'ENOENT') {
        return this.emit('error', e);
      }
    }

    return false;
  }

  /**
   * Load YAML or JSON from given file.
   *
   * @param {String} file
   * @return {*}
   */
  parseFile(file) {
    return yaml.safeLoad(fs.readFileSync(file, 'utf-8'));
  }

  /**
   * Resolve given file from `this.dir`.
   *
   * @param {String} file
   * @param {Boolean} cwd - whether it's relative to CWD (like when
   *                        defined in CLI).
   * @return {String}
   */
  resolve(file, cwd = false) {
    return path.resolve(cwd ? process.cwd() : this.dir, file);
  }
}
<|endoftext|>