新浪博客

在线视力检查表的制作和分享——技术和算法篇

2015-08-27 08:52阅读:
这篇详细讲解下在线E视力检查表开发过程中所使用的技术以及详细的算法和代码。
视力检查程序的网址是:http://www.aieyedoctor.cn/default.html 或者 http://qiangye.somee.com/default.asp
首先这是一个单页面网页程序,整个程序就一个页面,目前流行的网页技术几乎都有涉及,其编程语言使用的是javascript,界面的美观得益于JEasyUI库(包括大量js和css,当然也有JQuery)。本篇着重讲解和视力检查表有关的绘图技术的运用、流程控制和相关算法。
一、Html5中canvas绘图标签的使用
我们定义了一个canvas标签来绘图,绘图函数使用的是HTML5下的标签。canvas是一个好东西,提供了和许多其他绘图库类似的绘图函数。一开始选用这个标签也是看中这点,但用起来发现他的很多函数是名字差不多、功能差不多,但最后的效果却差很多,这可能是因为其语言是解释执行而不是编译执行的原因把。关于canvas标签的详细使用方法请参考:http://www.w3school.com.cn/tags/tag_canvas.asp 。讲讲对于Canvas标签使用的体会。
canvas在使用是要设定一个长宽区域,为了便于对其位置的控制,最好把它放在一个div里
给canvas标签一个id,本例中使用“myCanvas'。对于canvas绘图的使用是在javascript代码里。
首先要获取该标签的绘图上下文:
v
ar c = document.getElementByIdx_x_x_x_x_x_x('myCanvas');
cxt = c.getContext('2d');
获取绘图区域的大小,方便后续计算:
drawAreaWidth = c.width;
darwAreaHeight = c.height;
设置绘图坐标原点,由于我们绘制的E字符是需要旋转的,因此坐标原点设定在绘图区中央,使用下面这个函数可以达到这个效果:
cxt.translate(drawAreaWidth / 2, drawAreaHeight / 2);
然后就可以在需要的位置进行相关的绘图工作了。
//清除绘图区域
function ClearDrawArea() {
if (cxt != null) {
cxt.clearRect(-drawAreaWidth / 2, -drawAreaHeight/2, drawAreaWidth, drawAreaHeight);
}
}
E字符是如何绘制出来的呢,请看下面的代码:
//直接利用线宽来绘制E字符
function DrawEChar()
{
cxt.strokeStyle = '#000000';//使用黑色绘制,背景事先已设置为白色。
cxt.lineWidth = eChar.linewidth;//将绘制的宽度设定为指定的宽度,eChar是一个自定义的对象
cxt.rotate(GetRotationValue()); //设置旋转参数,后续的绘制将在这个旋转的影响下绘制。
cxt.beginPath();//开始描述绘制路径
var unit = eChar.linewidth / 2;//设定最小绘制单位,这是因为当线条较粗是,其位置的描述是线条宽度的中央像素代表的位置。
//下面6行就是在绘制E字符,绘制的是一个开口向上像“山”字的E字符。
cxt.moveTo(-4 * unit, -5 * unit);//首先将画笔移至“山”字最左上角
cxt.lineTo(-4 * unit, 4 * unit);//自上而下绘制第一竖笔
cxt.lineTo(4 * unit, 4 * unit);//自作而又接续绘制最底下的横线
cxt.lineTo(4 * unit, -5 * unit);//自下而上接续绘制最右边的竖线
cxt.moveTo(0, -5 * unit);//将画笔转移至中间竖线的上端
cxt.lineTo(0, 4 * unit);//绘制最中间的竖线
cxt.stroke();//前面的知识描述绘制路径,这一步是确实的绘制出来了。
}

这里用到了一个rotate函数,rotate函数是旋转图形,使用一次你不会觉得有问题,但是连续的使用就产生了问题。调试发现后一次rotate是在前一次rotate的基础上增加特定度数的。由于其它朝向的E字符都是通过旋转开头朝向的E字符得到的。所以我们每次绘制一定朝向的E字符时,需要考虑两点:一是在绘制开口朝向的E字符之前要知道当前的画布旋转了多少,其次要知道我们绘制的特定朝向的E字符本身需要旋转多少。这是一个容易出问题的地方。
二、对朝向的抽象
由于该程序要经常考虑四个朝向问题,为了使得代码简洁,我们针对朝向进行了一些抽象,Javascript不支持类和枚举的定义,我们变通了一下:
//设定一个朝向枚举对象:四个确定的朝向,1个不确定
if (typeof Orientation == 'undefined') {
var Orientation = {};
Orientation.up = 0;
Orientation.right = 1;
Orientation.down = 2;
Orientation.left = 3;
Orientation.unknown = -1;
}
//产生一个随机数介于min和max之间
function Random(min, max) {
return rand = min + (Math.round(Math.random() * 1000)) % (max - min);
}
//获取一个和参数方向不同的随机方向值,因为我们不希望看到后面呈现的E字符恰好和前面的E字符相同的朝向
//我们希望的是每一次出来一个和之前不同朝向的E字符。
function getNextDifferentOrientation(o) {
var newo;
do{
newo=RandomOrientation();
}while(o==newo);
return newo;
}
//这个函数仅仅产生一个随机朝向的方向。
function RandomOrientation() {
var rand = Random(0, 4);
switch (rand) {
case 0: return Orientation.up;
case 1: return Orientation.right;
case 2: return Orientation.down;
case 3: return Orientation.left;
default: return Orientation.unknown;
}
}
三、对E字符的抽象
同样我们对于E字符也进行了抽象,定义了一个全局对象,每一个E字符拥有一个线宽、一个朝向:
var eChar = new Object();
eChar.orient = Orientation.up;//初始的朝向是向上的。
eChar.linewidth = 20;//初始的线宽是20,随便设定的,正式绘制前会给其新的值。
如何控制旋转的呢?我们只要知道下一个E字符的朝向和当前E字符的朝向之间的差,然后旋转这个差值,就可以了。我们设计一个函数取得这个差值:
//获取不同的E字符方向对应的旋转角度(以弧度计算)
function GetRotationValue() {
var dif = eChar.orient - oldOrient;//将要绘制的和已经绘制的E字符朝向的差,枚举的好处体现在这里,既有良好的阅读性,代码也会比较简单。
totalRotated = (totalRotated + dif) % 4;//一共旋转的角度,取4的余数忽略360度的整数倍。
return dif * Math.PI / 2;//dif记录的角度是以90度为单位的,要换算成rotate函数认定的弧度单位。
}
为什么要设定一个totalRotated呢?其实可以不设计这个变量。这个变量存在初衷是为了能记住经过多次旋转后,目前的状态和最初状态的差别,这样好可以比较容易回到最初状态中去,以执行其他一些工作,比如绘制校准区域啊。如果不是在无旋转状态下,那么可能想要绘制一个横着的卡片,却显示了一个竖着的卡片。
注:在早期的开发校准过程中,我们也是通过绘制以及缩放矩形来实现的,后来发现随着不同朝向E字符的不断绘制,回到校准界面时会经常看到横着的矩形立起来了。通过分析才发现问题出在了rotate这里。所以设置了这个变量,使得我们在回到绘制校准矩形前先把画布rotate回初始的状态。
同样rotate函数并不会对已经绘制的图形产生影响,它只会影响后续的绘制动作,所以我们绘制不同朝向的E字符不能仅仅通过旋转画布,而必须要擦掉当前的E字符,然后旋转画布,然后在重新绘制。擦除的函数是:
function ClearDrawArea() {
if (cxt != null) {//cxt还是之前的绘制上下文全局变量。
cxt.clearRect(-drawAreaWidth / 2, -drawAreaHeight/2, drawAreaWidth, drawAreaHeight);
}
}
四、核心流程的控制
绘制旋转的问题解决了,还有一个重点是如何应对检查时使用者的响应,控制整个检查流程呢。这个工作之前使用swift语言已经完成过,这次翻译成javascript省事了不少,但通过翻译调试还是发现两种语言之间有一个很不起眼却产生完全不同结果的差别,结合代码逐行解释:
首先需要设定一些变量,记录检查过程中的一些结果参数等,这些参数体现了我们的控制思路。
//使用者当前答错次数
var wrongtimes = 0;
//连续答对次数
var correcttimes = 0;
//最大允许答错次数
var maxwrongtimes = 3;
//最大连续答对次数,达到该次数认为通过此级别检查
var maxcorrecttimes = 5;
//当前E字符的朝向
var curgivencharorient = Orientation.unknown;
//用户给出的回答朝向
var userorient = Orientation.unknown;
//设定一个视力检查方向的变量,当该变量为0时,表示检查方向未确定,表示系统还不知道在该行检查结束后应该提供更小的E字符还是提供较大的E字符;当=1时,表示患者可以看清当前视力代表的字符大小,并倾向于提供更高的视力字符;当=-1时表示患者无法看清当前视力登记代表的字符,检查方向转为提供较低视力的字符。检查方向一旦确定下来,则在检查过程中就不再变化,否则检查就不会终止。
var examdirection = 0;
//用户回答是否正确,初始设定为不正确
var isAnswerRight = false;
//检查是否结束
var isExamFinished = false;
//是否正在分析本次应答,锁变量,防止用户两次应答间隔过小导致程序死锁。
var isAnalysising = false;
其中在每次检查前都需要对一些参数进行重新设定,这些设定集中写在一个方法里:
function initPreExamData() {
examrecord.examingEye = Eyes.unknown;
examrecord.distance = examDis;
examrecord.vaindex = initExamVAIndex;
examrecord.va = vagrades[initExamVAIndex].value;
wrongtimes = 0;
correcttimes = 0;
maxwrongtimes = 3;
maxcorrecttimes = 5;
curgivencharorient = Orientation.unknown;
userorient = Orientation.unknown;
examdirection = 0;
isAnswerRight = false;
isExamFinished = false;
isAnalysising = false;
} 同样,在每次检查前,只设定数据还不行,还要准备场景,准备E字符,更新面板的信息显示等等。这些工作集中包含在下面这个方法里(里面具体一些小方法就不再详细描述了):
//在正式检查视力时应调用该函数对检查进行设置,此后检查过程中均只对examrecord进行修改。
function preExamSetting() {
$('#fbImage').fadeTo(0, 0);//这是一个JQuery语句,什么功能留给读者去猜。
//根据参数设定初始检查记录
initPreExamData();
UpdateExamingVA();//更新面板上的视力显示
UpdateExamingDis();//更新面板上的距离显示
//准备画布
ClearDrawArea();
//准备新字符
CreateNewEChar();
//绘制字符
DrawEChar();
}
当用户点击回答正确或错误的按钮,我们需要在屏幕上显示相应的图片,这个功能由下面这个方法实现:
//以图片的方式显示用户回答的结果是否正确,在场景内放置图片
function displayResultImage(isanswerright) {
var c=document.getElementByIdx_x_x_x_x_x_x('fbImage')
if(isanswerright)
{
c.src = 'Images/right.png';
}
else {
c.src = 'Images/wrong.png';
}
$('#fbImage').fadeTo('fast', 1);//先将图片快速显示出来
$('#fbImage').fadeTo('fast',0);//在快速将图片淡出。
}
下面这个方法是最核心的控制方法,过程比较多,解释也比较多:
//分析处理用户应答的核心程序,当用户给出一个应答后调用
function ProcessAfterUserAnswer(){
//如果检查已经结束,则不运行后续代码,直接返回跳出本函数
if(isExamFinished==true){
//println('检查结束')
return
}
//如果用户的朝向与E字符实际朝向一致
if(userorient == eChar.orient){
//表示用户答对了
isAnswerRight=true;
//正确回答的次数增加1次
correcttimes++;
//当此行视力检查连续正确回答次数达到一定数量,并且总的检查方向是朝着差的视力进行的,或者已达到最高视力
//检查结束,当前视力为最佳视力
if((correcttimes >= maxcorrecttimes && examdirection == -1) ||
(correcttimes >= maxcorrecttimes && examrecord.va >= 1.0)){
isExamFinished=true;
}
//当检查方向不确定或者是朝着更好的视力等级进行时
else if ( correcttimes >= maxcorrecttimes && examdirection != -1 ){
if(examdirection==0){
//设定检查朝向更好的视力等级进行
examdirection = 1;
}
//打算提高一个视力等级继续检查,并设定在该行的一些判断数据,检查并未结束
examrecord.vaindex++;
if (examrecord.vaindex >= vagrades.length - 1)
{ examrecord.vaindex = vagrades.length - 1; }
examrecord.va = vagrades[examrecord.vaindex].value;
correcttimes=0;
wrongtimes=0;
isExamFinished=false;
}
}
//用户指出的方向与实际方向不一致
else{
//回答错了
isAnswerRight=false;
wrongtimes++;//增加回答错误的次数
//一旦回答错误,则先前在此行的正确回答次数被清零,表明正确回答次数是连续正确回答次数
correcttimes = 0;
//如果此行回答错误次数达到设定值,并且是朝着视力较好的等级方向检查或者已经到达最低检查视力,表明检查应该结束
if((wrongtimes >= maxwrongtimes && examdirection == 1) ||
(wrongtimes >= maxwrongtimes && examrecord.va <= 0.1)){
//此时的实际视力应比当前低一级
if (examrecord.vaindex > 0)
{ examrecord.vaindex--; }
examrecord.va=vagrades[examrecord.vaindex].value;
isExamFinished=true;
//return;
}
//如果虽然错误回答此处达到设定次数,但检查的方向不确定或者是一直提供较高视力等级给用户(也就是说用户还没能有一个确定的视力级别)检查
//则表明检查方向应设定为朝着更低等级的视力进行
else if ( wrongtimes >= maxwrongtimes && examdirection != 1 ){
if(examdirection==0){
examdirection = -1;
}
//同时下调视力等级继续检查
if (examrecord.vaindex > 0)
{ examrecord.vaindex--; }
examrecord.va = vagrades[examrecord.vaindex].value;
isExamFinished=false;
wrongtimes=0;
}
}
//将当前用户回答情况以图片的形式在界面上反映出来
displayResultImage(isAnswerRight)
//根据检查是否结束决定是否在场景内布置新的E字符
if (isExamFinished == false) {
CreateNewEChar();
UpdateExamingVA();
ClearDrawArea();
DrawEChar2();
//重置用户的判断为unknow,表明用户还没有判断新的E字符朝向
userorient=Orientation.unknown
//更新下界面数据
}
//如果检查结束
else{
//则不让用户继续点击“看不清”按钮
//uiBtnWrong.enabled=false
//显示检查结束的一些信息(显示最终得到的视力)
displayExamResult()
//用户可以点击导航条右上角的按钮返回上一个视图
//nextBarButtonItem.enabled=true
//uiBtnCorrect.enabled=false
}
}
那么如何响应用户的点击按钮呢,在网页中注册按钮的onclick事件给下面这两个方法:
//用户点击了看不清或回答错误的按钮
function wrongBtnClicked() {
if(isAnalysising==true){
return;
}
else{
isAnalysising = true;
//为了简便判断,直接将用户的朝向设定为“不知道”。用户给出的朝向还可以通过其他渠道获得,比如在iphone上通过手势获得,或者在网页上专门涉及4个朝向的按钮等等。这里有点偷懒了,但是代码的设计考虑了今后功能的扩展。
userorient=Orientation.unknown;
ProcessAfterUserAnswer();
isAnalysising = false;
}
}
//用户点击了能看清按钮
function rightBtnClicked(){
if(isAnalysising==true){
return;
}
else{
isAnalysising = true;
//直接将用户给出的朝向设定为E字符的朝向。
userorient=eChar.orient;
ProcessAfterUserAnswer();
isAnalysising = false;
}
}
五、辅助数据结构:
核心代码就基本结束了,本例中一些和前端网页打交道的html代码没有重点解释。另外使用了两个JSON技术描述的数组数据,分别是页面控制和视力等级。先贴一下视力等级数据,这样设计完全是为了程序方便而已。
var vagrades = [
{ 'index': '0', 'value': '<0.1', 'logvalue': '<4.0', 'text': '低于 0.10(4.0)' },
{ 'index':'1','value': '0.1', 'logvalue': '4.0', 'text': '0.10 - 4.0' },
{ 'index':'2','value': '0.12', 'logvalue': '4.1', 'text': '0.12 - 4.1' },
{ 'index':'3', 'value': '0.15', 'logvalue': '4.2', 'text': '0.15 - 4.2' },
{ 'index':'4','value': '0.2', 'logvalue': '4.3', 'text': '0.20 - 4.3' },
{ 'index':'5','value': '0.25', 'logvalue': '4.4', 'text': '0.25 - 4.4' },
{ 'index':'6','value': '0.3', 'logvalue': '4.5', 'text': '0.30 - 4.5' },
{ 'index':'7','value': '0.4', 'logvalue': '4.6', 'text': '0.40 - 4.6' },
{ 'index':'8','value': '0.5', 'logvalue': '4.7', 'text': '0.50 - 4.7' },
{ 'index':'9','value': '0.6', 'logvalue': '4.8', 'text': '0.60 - 4.8' },
{ 'index':'10','value': '0.8', 'logvalue': '4.8', 'text': '0.80 - 4.9' },
{ 'index':'11','value': '1.0', 'logvalue': '5.0', 'text': '1.00 - 5.0' }
];
//页面控制
var stepPages = [
{ 'id': 'Introduction', 'title': '视力检查-介绍' },
{ 'id': 'Correction', 'title': '视力检查-尺寸校准' },
{ 'id': 'Setting', 'title': '视力检查-参数设置' },
{ 'id': 'PreExamInfo', 'title': '视力检查-检查前提示' },
{ 'id': 'Examing', 'title': '视力检查-进行中' }//,
// { 'id': 'Result', 'title': '视力检查-结果' }
];
这两个个比较简单,就不再细述了。
真诚感谢能有耐心看到这句话的朋友们!感谢你们!
六、swift和javascript差别的一点体会
关于从这个程序中体会到的swift语言和javascript语言的差别:
虽然swift也使用var定义变量,但它还是有着严格使得数据类型的。但是javascript则没有。调试过程中出现一个滑稽的错误:在javascript中:a +=1 这句代码可以用这不同的运行结果哦,当a=1时,a+=1的结果可能是11而不是我们希望得到的2,原因就在于a在这里被认为是字符串。发现这一问题是因为在程序中当进行完0.1(加入索引是1)级别的视力检查后,我们希望进行下一级别的检查(索引为2),而当运行了examrecord.vaindex+=1时,里面告知检查完毕,同时提示视力为1.0。就在于vaindex一下子从1变成了11,超出了允许检查的视力范围,因而直接告知检查结束,视力是最好视力。
可见不同的语言之间即使代码一样,表达的意思可能差别很多,还是强数据类型的语言出错的概率低一些。

我的更多文章

下载客户端阅读体验更佳

APP专享