First prototype of the new Patient Portal

You can test it out here: https://clear.dental/test/test.php

Here is the source code that I hereby release under GPL v3:

 

<!doctype html>
<html>

<head>
	<title>Welcome New Patient</title>
	<meta name="viewport" content="width=device-width, initial-scale=1">
	

	<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script> 
	
	<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jquerymobile/1.4.5/jquery.mobile.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquerymobile/1.4.5/jquery.mobile.min.js"></script>
    
    <script src="https://cdn.jsdelivr.net/npm/signature_pad@2.3.2/dist/signature_pad.min.js"></script>
    
</head>


    <body>
        <form>
<?php

include "stateList.php";

$json = file_get_contents('newPatQuestions.json');
$json_data = json_decode($json,true);

//echo var_dump($json_data);
$pageCounter =1;

foreach($json_data as $index => $page) {
    echo "<div data-role=\"page\" id=\"page{$pageCounter}\" data-theme=\"a\"> <div role=\"main\" class=\"ui-content\">\n";
    $hasCut = false;
    $cutName = ""; //is init here in order to get a high span...
    
    foreach($page as $index1 => $line) {
        //echo "Line: ".$line["name"]."\n";
        
        if($line["type"] == "header") { 
            if($pageCounter > 1) {
                echo "\t<div data-role=\"header\" data-add-back-btn=\"true\"><h1>{$line["prompt"]}:</h1></div>\n";
            } else {
                echo "\t<div data-role=\"header\"><h1>{$line["prompt"]}:</h1></div>\n";
            }
        } else if($line["type"] == "lineText") {
            echo "\t<div class=\"ui-field-contain\"><label for=\"".$line["name"]."\">".$line["prompt"].":</label>";
            echo "\t<input type=\"text\" name=\"".$line["name"]."\" value=\"\"  data-clear-btn=\"true\"></div>\n";
        }else if($line["type"] == "date") {
            echo "\t<div class=\"ui-field-contain\"><label for=\"".$line["name"]."\">".$line["prompt"].":</label>";
            echo "\t<input type=\"date\" name=\"".$line["name"]."\" value=\"\"></div>\n";        
        } else if($line["type"] == "choose") { 
            echo "\t<fieldset class=\"ui-field-contain\" data-role=\"controlgroup\">\n";
            echo "\t\t<legend>".$line["prompt"].":</legend>\n";
            foreach($line["options"] as $option) {
                echo "\t\t<label for=\"radio-choice-".$line["name"].$option."\">".$option."</label>\n";
                echo "\t\t<input type=\"radio\" name=\"radio-choice-".$line["name"]."\" id=\"radio-choice-".$line["name"].$option."\">\n";
            }
            echo "\t</fieldset>\n";
        } else if($line["type"] == "chooseLine") { 
            echo "\t<div class=\"ui-field-contain\">";
            echo "\t\t<label for=\"".$line["name"]."\">".$line["prompt"]."</label>";
            echo "\t\t<select name=\"".$line["name"]."\" id=\"".$line["name"]."\">";
            foreach($line["options"] as $option) {
                echo "\t\t\t<option value=\"".$option."\">".$option."</option>";
            }
            echo "\t</select></div>";
        } else if($line["type"] == "usState") {
            echo "\t<div class=\"ui-field-contain\">";
            echo "\t\t<label for=\"".$line["name"]."\">".$line["prompt"]."</label>";
            echo "\t\t<select name=\"".$line["name"]."\" id=\"".$line["name"]."\">";
            foreach($state_list as $code => $statename) {
                if($code == "NH")
                    echo "\t\t\t<option selected=\"selected\" value=\"".$code."\">".$statename."</option>";
                else
                    echo "\t\t\t<option value=\"".$code."\">".$statename."</option>";
            }
            echo "\t</select></div>";
        } else if($line["type"] == "number") {
            echo "\t<div class=\"ui-field-contain\"><label for=\"".$line["name"]."\">".$line["prompt"].":</label>";
            echo "\t<input type=\"number\" name=\"".$line["name"]."\" value=\"\"  data-clear-btn=\"true\"></div>\n";
        } else if($line["type"] == "phone") {
            echo "\t<div class=\"ui-field-contain\"><label for=\"".$line["name"]."\">".$line["prompt"].":</label>";
            echo "\t<input type=\"tel\" name=\"".$line["name"]."\" value=\"\"  data-clear-btn=\"true\"></div>\n";
        } else if($line["type"] == "cutoffQuestion") {
            $hasCut = true;
            $cutName = $line["name"];
            echo "\t<h3>".$line["prompt"]."</h3>\n";
            echo '<div data-role="controlgroup" data-type="horizontal">
                <a href="#" class="ui-btn ui-corner-all ui-icon-check ui-btn-icon-left" id="Yes'.$line["name"].'">Yes</a>
                <a href="#" class="ui-btn ui-corner-all ui-icon-delete ui-btn-icon-left" id="No'.$line["name"].'">No</a></div><div id="hideMe'.$line["name"].'">';
            echo '<script>            
                $(document).ready(function(){
                    $("#hideMe'.$line["name"].'").hide();
                    $("#nextBut'.$line["name"].'").hide();
                    $("#Yes'.$line["name"].'").click(function(){
                        $("#hideMe'.$line["name"].'").show(1000);
                        $("#nextBut'.$line["name"].'").show(1000);
                    });
                    $("#No'.$line["name"].'").click(function(){
                        $("#hideMe'.$line["name"].'").hide(1000);
                        $("#nextBut'.$line["name"].'").show(200);
                    });
                });
                    </script>';
        } else if($line["type"] == "signature") {
            echo "\t<label>".$line["prompt"]."</label>";
            
            echo '<canvas id="sig'.$line["name"].'" style="border:1px solid #000000;" ></canvas></div>';
            echo '<script>
                var canvas = document.querySelector("#sig'.$line["name"].'");
                canvas.width  = window.innerWidth * .9;
                canvas.height = window.innerHeight * .6;

                var signaturePad = new SignaturePad(canvas);

                function resizeCanvas() {
                    canvas.width  = window.innerWidth * .9;
                    canvas.height = window.innerHeight * .6;
                    signaturePad.clear(); // otherwise isEmpty() might return incorrect value
                }

                window.addEventListener("resize", resizeCanvas);
                resizeCanvas();
                </script>';
        } else if($line["type"] == "conditionLine") {
            echo "\t<div class=\"ui-field-contain\">";
            echo "\t<label for=\"".$line["name"]."\">".$line["prompt"]."<input id=\"".$line["name"]."\" type=\"checkbox\" name=\"".$line["name"]."\" value=\"\" ></label></div>\n";
            
            echo '<div id="hideMeExp'.$line["name"].'">';
            echo "\t<div class=\"ui-field-contain\"><label for=\"exp".$line["name"]."\">Please explain:</label>";
            echo "\t<input type=\"text\" id=\"\" name=\"exp".$line["name"]."\" value=\"\"  data-clear-btn=\"true\"></div>\n";
            echo '</div>';
            
            echo '<script>            
                $(document).ready(function(){
                    $("#hideMeExp'.$line["name"].'").hide();
                   
                    $("#'.$line["name"].'").click(function(){
                    
                        if( $("#'.$line["name"].'").is(\':checked\')) {
                            $("#hideMeExp'.$line["name"].'").show(500);
                        } else {
                            $("#hideMeExp'.$line["name"].'").hide(500);
                        }                        
                    });
                });
                    </script>';
        } else if($line["type"] == "askConditions") {
            echo "<hr><h4>Please check if you have any of the following conditions:</h4>";
        } else if($line["type"] == "dayChoose") {
            echo "\t<div class=\"ui-field-contain\"><label for=\"".$line["name"]."\">".$line["prompt"].":</label>";
            
            echo '<fieldset data-role="controlgroup" data-type="horizontal">
                    <input type="checkbox" name="mon'.$line["name"].'" id="mon'.$line["name"].'"><label for="mon'.$line["name"].'">Monday</label>
                    <input type="checkbox" name="tue'.$line["name"].'" id="tue'.$line["name"].'"><label for="tue'.$line["name"].'">Tuesday</label>
                    <input type="checkbox" name="wed'.$line["name"].'" id="wed'.$line["name"].'"><label for="wed'.$line["name"].'">Wednesday</label>
                    <input type="checkbox" name="thurs'.$line["name"].'" id="thurs'.$line["name"].'"><label for="thurs'.$line["name"].'">Thursday</label>
                    <input type="checkbox" name="fri'.$line["name"].'" id="fri'.$line["name"].'"><label for="fri'.$line["name"].'">Friday</label>
                    <input type="checkbox" name="sat'.$line["name"].'" id="sat'.$line["name"].'"><label for="sat'.$line["name"].'">Saturday</label>
                    
                    </fieldset></div>';
            
        }
    }
    
    if($hasCut) {
        $hasCut = false;
        echo "</div>"; //end of "hideme" for the rest of the page div
    }
    
    echo "<a href=\"#page".($pageCounter+1)."\" data-role=\"button\" data-inline=\"true\" data-transition=\"flow\" id=\"nextBut".$cutName."\" data-icon=\"carat-r\" data-iconpos=\"right\">Next Page</a>";
    echo "</div></div>\n";
    $pageCounter++;
}
?>



    
        </form>
    </body>
</html>

And here is the current JSON file:

[  
  [
    {"name":"intro", "type":"header","prompt":"Basic information"},
    {"name":"firstName", "type":"lineText","prompt":"First Name"},
    {"name":"middleName", "type":"lineText","prompt":"Middle Name"},
    {"name":"lastName", "type":"lineText","prompt":"Last Name"},
    {"name":"dob", "type":"date","prompt":"Date of Birth"},
    {"name":"sex", "type":"choose","prompt":"Sex", "options":["Male","Female"]},
    {"name":"gender", "type":"lineText","prompt":"Gender (if different)"},
    {"name":"race", "type":"chooseLine","prompt":"Race (optional)", "options":["I choose not to answer","American Indian or Alaska native","Asian", "Black or African American", "Polynesian","White","Other"]},
    {"name":"ethnicity", "type":"chooseLine","prompt":"Ethnicity (optional)", "options":["I choose not to answer","Hispanic Or Latino", "Not Hispanic or Latino"]},
    {"name":"martialStatus", "type":"choose","prompt":"Martial Status", "options":["Single", "Married"]},
    {"name":"addrStreet", "type":"lineText","prompt":"Address"},
    {"name":"addrCity", "type":"lineText","prompt":"City"},
    {"name":"addrState", "type":"usState","prompt":"State"},
    {"name":"addrZip", "type":"number","prompt":"Zip code"},
    {"name":"homePhone", "type":"phone","prompt":"Home Phone number"},
    {"name":"cellPhone", "type":"phone","prompt":"Cell Phone Number"},
    {"name":"email", "type":"lineText","prompt":"Email Address"},
    {"name":"workLoc", "type":"lineText","prompt":"Employer or School"},
    {"name":"workPhone", "type":"phone","prompt":"Work Phone number"},
    {"name":"referral", "type":"lineText","prompt":"How were you referred to our practice?"},
    {"name":"contactPref", "type":"choose","prompt":"How would you prefer we to contact you?", "options":["Home Phone","Call cell phone", "Text Phone", "Email"]}
  ],
  
  [
    {"name":"intro", "type":"header","prompt":"Financial Responsibility"},
    {"name":"isOtherRes", "type":"cutoffQuestion","prompt":"Is somebody other than the patient listed responsible for payment?"},
    {"name":"otherResName", "type":"lineText","prompt":"Name of person responsible"},
    {"name":"otherResAddr", "type":"lineText","prompt":"Address of person responsible"},
    {"name":"otherResCity", "type":"lineText","prompt":"City of person responsible"},
    {"name":"otherResState", "type":"usState","prompt":"State of person responsible"}
  ],
  
  [
    {"name":"intro", "type":"header","prompt":"Dental Plan Information"},
    {"name":"has1ins", "type":"cutoffQuestion","prompt":"Do you have a dental plan or dental insurance?"},
    {"name":"insCompany", "type":"lineText","prompt":"Dental Plan/Insurance Company"},
    {"name":"insPhone", "type":"phone","prompt":"Phone Number for Plan"},
    {"name":"subName", "type":"lineText","prompt":"Subscriber's Name"},
    {"name":"insDob", "type":"date","prompt":"Date of Birth"},
    {"name":"insSex", "type":"choose","prompt":"Sex", "options":["Male","Female"]}
  ],
  
  [
    {"name":"intro", "type":"header","prompt":"Secondary Dental Plan Information"},
    {"name":"has2ins", "type":"cutoffQuestion","prompt":"Do you have a secondary dental plan or dental insurance?"},
    {"name":"insCompany2", "type":"lineText","prompt":"Secondary Dental Plan/Insurance Company"},
    {"name":"insPhone2", "type":"phone","prompt":"Phone Number for Secondary  Plan"},
    {"name":"subName2", "type":"lineText","prompt":"Subscriber's Name"},
    {"name":"insDob2", "type":"date","prompt":"Date of Birth"},
    {"name":"insSex2", "type":"choose","prompt":"Sex", "options":["Male","Female"]}
  ],
  
  [
    {"name":"intro", "type":"header","prompt":"Please sign the following"},
    {"name":"voluntaryCome", "type":"signature","prompt":"I have voluntarily come to the Tamworth Dental Center seeking dental services including examinations, diagnostic tests and treatment"}
  ],
  
  [
    {"name":"intro", "type":"header","prompt":"Please sign the following"},
    {"name":"alltrue", "type":"signature","prompt":"This is to certify that all information on this form is true and complete. I understand that if I deliberately give false information related to my sitation, now or in the future that I am liable for prosecution for fraud."}
  ],
  
  [
    {"name":"intro", "type":"header","prompt":"Please sign the following"},
    {"name":"finanicallyResp", "type":"signature","prompt":"I understand that I am finanically responsible for all charges incurred that are not covered by my insurance company."}
  ],
  
  [
    {"name":"intro", "type":"header","prompt":"Please sign the following"},
    {"name":"privHealthInformation", "type":"signature","prompt":"We are required by law to maintain the privacy of your health information and to provide you with the Notice of Privacy Practices."}
  ],
  
  [
    {"name":"intro", "type":"header","prompt":"Please sign the following"},
    {"name":"shareTriCap", "type":"signature","prompt":"If applicable, I understand that the Tamworth Dental Center and the Fuel / Electric Assistance Program are both projects of Tri-County CAP. I hereby authorize Tamworth Dental Center staff to review and verify my household income as previously disclosed to the Fuel / Electric Assistance Program."}
  ],
  
  [
    {"name":"intro", "type":"header","prompt":"Cardiovascular Conditions"},
    {"name":"anyHeartConditions", "type":"cutoffQuestion","prompt":"Do you have any heart or cardiovascular conditions?"},
    {"name":"na", "type":"askConditions","prompt":"na"},
    {"name":"hypertension", "type":"conditionLine","prompt":"Hypertension (high blood pressure)"},
    {"name":"hypotension", "type":"conditionLine","prompt":"Hypotension (low blood pressure)"},
    {"name":"endocarditis", "type":"conditionLine","prompt":"History of endocarditis"}
  ],
  
  [
    {"name":"intro", "type":"header","prompt":"Preferences"},
    {"name":"appointPref", "type":"dayChoose","prompt":"Which day(s) of the week works best for you?"}
  
  ]
  
  
]

Reading a 16-bit greyscale tiff file and saving it as a .png for later

TIFF* tif = TIFFOpen("11111.tif", "r");


    if (tif) {
        uint32 imagelength;
        tsize_t scanline;
        tdata_t buf;
        uint32 row;
        uint32 col;

        ulong grandTotal=0;
        int pixelCount =0;

        QFile dataset("data.dat");
        dataset.open(QIODevice::WriteOnly | QIODevice::Text);
        QTextStream textStream( &dataset );
        QList<int> intData;


        TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &imagelength);
        scanline = TIFFScanlineSize(tif);
        buf = _TIFFmalloc(scanline);
        for (row = 0; row < imagelength; row++)
        {
            TIFFReadScanline(tif, buf, row);
            uchar *vals = static_cast<uchar*>(buf);
            for (col = 0; col < (scanline/2); col++) {
                int first = vals[col*2];
                int second = vals[(col*2)+1];
                int combine = first + (second * 256);

                textStream<<QString::number(combine)<<"\n";

                grandTotal+= combine;
                pixelCount++;

                intData.append(combine);
            }
            //qDebug()<<"Scanline Size: "<<scanline;


        }
        dataset.close();;
        _TIFFfree(buf);
        TIFFClose(tif);


        uint average = grandTotal / pixelCount;
        ulong variance=0;
        for(int i: intData) {
            int diff = average - i;
            variance += (diff*diff);
        }
        qreal stdev = sqrt( variance / pixelCount);

        int low = (int) (average - (1.5 * stdev));
        int high = (int) (average + (1.5 * stdev));
        qreal range = high-low;
        qDebug()<<"Averge: "<<average;
        qDebug()<<"variance: "<<variance;
        qDebug()<<"stdev: "<<stdev;
        qDebug()<<"low: "<<low;
        qDebug()<<"high: "<<high;
        qDebug()<<"range: "<<range;


        int i=0;

        QImage makeMe(scanline/2,imagelength,QImage::Format_RGB32);
        qDebug()<<makeMe.height() * makeMe.width() <<"="<< intData.length();

        for(int y=0;y<makeMe.height();y++) {
            for(int x=0;x<makeMe.width();x++) {
                int value = intData.at(i++);
                if(value< low) {
                    value = 0;
                }
                else if(value > high) {
                    value = 255;
                }
                else {
                    qreal fraction = (value - low)/range;
                    value =(int) (fraction * 255);
                    //qDebug()<<value;
                }
                makeMe.setPixelColor(x,y,QColor(value,value,value));
            }
        }

        makeMe.invertPixels(QImage::InvertRgb);
        makeMe.save("makeMe.png");

    }

Hamamatsu Dental CMOS Sensor documentation: Part 1: the Serial Number handshake

After working on the Hamamatsu sensors for some time, I was able to figure out the USB protocol.

 

USB Sensor information

For the sensor connection, you first have to know where to connect to. Here is the basic information:

Vendor code: 0661 (hex); 1633 (dec)
Product code: 4400 (hex); 17408 (dec)

It appears, that every time you want to send data to the sensor, you need to send the endpoint to endpoint #2. To get data, you need to listen in to endpoint #6.

 

Handshake request

Send the following 12 bytes of code to the sensor (hexadecimal little endian) using endpoint 2:

c8:06:01:b8:01:b5:00:00:75:30:ab:cd

Of course, make sure all 12 bytes were sent to the device.

 

Data Back

Expect to get back 16 bytes of data. Assuming we get a byte array (zero indexed) called “dataBack”, here is what appears to be the results of the data:

 

dataBack[15] = Firmware version
dataBack[14] = Sensor Type
dataBack[10] + (256 * dataBack[11]) = Sensor name

 

To make it match the serial number that your sensor came back with, you simply convert both the sensor type and the name to a hexadecimal number and then combine it to a single string of “[sensorType][sensorName]”.

The tmj.ini file

Motion based exam

The data is recorded with the assumption that the TMJ exam is done with a motion based exam.

Opening and closing

Here are the attributes for opening and closing:

[Opening and Closing]
PainOnOpening=[true | false]
LeftClickOnOpen=[true | false]
RightClickOnOpen=[true | false]
DeviationOnOpen=["center" | "left" | "right"]
OpenAmount=[int (in mm)]
PainOnClosing=[true | false]
LeftClickOnClosing=[true | false]
RightClickOnClosing=[true | false]
DeviationOnClosing=["center" | "left" | "right"]

 

Protrusive movement

Here are the attributes for protrusive movement:

[Protrusive]
ProtrusiveAmount=[int (in mm)]
PainOnProtrusion=[true | false]

 

Lateral Movements

Here are the attributes for lateral movement:

[Lateral]
LeftLateralAmount=[int (in mm)]
PainOnLeftLateral[true | false]
RightLateralAmount=[int (in mm)]
PainOnRightLateral=[true | false]

 

Static Bite

Records for when the bite is at centric occlusion.

[Static]
OcclusionClass=["I" | "II div 1" | "II div 2" | "III"]
Overjet=[int (in mm)]
Overbite=[int (in mm)]
AnteriorCrossbite=[true | false]
PosteriorCrossbite=[true | false]

 
Let me know in the comments if I am missing anything

The cpcf file

Basics and header

Now that we went over tooth surface charting, lets go over periodontal charting.

Just like the cdcf file, we start with the line: “SpecVersion=1”. And also like the cdcf file, we denote each tooth by the American system followed by the “|” symbol and then will have periodontal attributes associated with each tooth.

Periodontal probing

After the keyword “probing”, six numbers are expected which are associated with the probing depths. Where the probing depths are located depends on the quadrant we are working in. The best way to remember the order is to look at the tooth from the occlusal position and “up” being towards the facial or bucal, then then work clockwise starting with the buccal side “left” and then all the way around.

Maxillary right quadrant: [buccal/facial distal] [buccal/facial center] [buccal/facial mesial] [lingual mesial] [lingual center] [lingual distal]
Maxillary left quadrant: [buccal/facial mesial] [buccal/facial center] [buccal/facial distal] [lingual distal] [lingual center] [lingual mesial]
Mandibular left quadrant: [buccal/facial distal] [buccal/facial center] [buccal/facial mesial] [lingual mesial] [lingual center] [lingual distal]
Mandibular right quadrant: [buccal/facial distal] [buccal/facial center] [buccal/facial mesial] [lingual mesial] [lingual center] [lingual distal]

For example, lets say we have the following line:

...
3| probing 2 2 3 3 3 4
...

This would mean for tooth #3, the distal buccal probing depth is 2mm, the center buccal probing depth is 2mm, the mesial buccal probing depth is 3mm, the mesial lingual probing depth is 3mm, the center lingual probing depth is 3mm, and the distal lingual probing depth is 4mm.

Periodontal recession

Recession works the same way as probing but with the keyword “recession”.  So, looking at the following example:

...
4| probing 3 3 3 2 2 2; recession 0 0 1 2 0 0
...

Tooth #4 has 1mm recession in the mesial buccal aspect and 2mm recession in the mesial lingual aspect and no recession anywhere else.

Bleeding

Bleeding starts off with they attribute keyword “bleeding” but then expects either a “b” or “-“. The “b” meaning bleeding was present and “-” meaning there was no bleeding present. In the following example:

...
5| probing 3 3 3 2 2 2; recession 0 0 1 2 0 0; bleeding b b b - - b
...

In this case, tooth #5 had bleeding all along the buccal aspect and the distal lingual aspect.

Mobility

Mobility applies for the whole tooth, so there is only one number expected (which is the grade) after the mobility keyword. For example:

...
12| probing 6 6 7 8 4 7; recession 2 2 1 2 3 2; mobility 2
...

Which means tooth #12 has a mobility of grade II.

Furcation

Just like mobility, furcation applies for the whole tooth. For example:

...
30| probing 6 6 7 8 4 7; recession 5 5 5 2 3 2; furcation 2
...

Which means that tooth #30 has furcation grade II.

Other keyword attributes

The following are just keyword attributes that do not expect any additional information

  • Keyword dehiscence for Dehiscence
  • Keyword fenestration for Fenestration

 

And that’s it!

Let me know in the comments if anything is missing.

The cdcf file

Why make a chart format?

Because it hasn’t been done before. Apparently nobody has ever decided to create a file format and spec for dental charting. This will be the first.

Fundamentals

First of all, all tooth numbers are going by the American / Universal Standard. Just to review, here is the basic chart (taken from the Wikipedia article):

Starting with patient’s upper right, the patient’s maxillary right 3rd molar is tooth #1. As you move to the patient’s left, you increase in number with the patient’s maxillary right 2nd molar being tooth #2. Then, with the maxillary left 3rd molar being #16, then, still staying on the left side, the left mandibular 3rd molar is tooth #17, and working towards the right, the mandibular right 3rd molar is #32.

As for primary (baby) teeth, the standard system (using tooth #A-T) will be used in a similar fashion.

What about European Standards?

Having a UI convert from American to the FDI World Dental Federation Notation is actually very simple. Therefore, whichever system is actually used in the file does not matter as much. But still, why go with American? There is no real good answer outside of the fact this software will be released in the US first and the rest of the world later.

The basic tenants we want in a charting file format

As mentioned in previous posts, we want this file format to be human readable. That way, even if the UI is broken, somebody can at least read the cdcf file using a simple text editor. Therefore, it will use basic text in a way that most dentist can learn in a single day. The side effect of this is that using a system like git, we can be able to push changes via a simple diff rather than uploading an entire binary file.

We also want to use terms that most people can understand. However, things like the CDT codes complicate things. Most people do not know on top of their head what code is assigned to which procedure. Therefore, we need to minimize any kind of CDT code in the format itself. However, there are indeed cases where normal terminology is not sufficient and a code needs to be used to explain what is existing and what kind of procedure was done with the patient. Eventually, as Clear Dental goes international, all the terms will be properly explained in the CDT codes will be unnecessary.

As mentioned before, this will be released in the US first. Therefore, all terms will be in English. Even when Clear Dental makes its way to other countries, the terms will be kept in English.

One real important thing about this format is that when you are reading the file, you are reading the current status of the patient. In order to get a history of what was done you need to use git to see the logs of the changes done to the file and you can use diff to find the difference between the two versions. Of course, in the UI, this will all be integrated so the user does not have to type in command lines to check the history (but that option is always there).

What belongs in the file and does not

Because you want to get a comprehensive view of the patient’s status, it will include all of the following:

  • Existing / missing teeth
  • Restorations (amalgams, composites, crowns, etc.)
  • Implants
  • Existing endodontic treatment (including post/core) and endodontic diagnosis
  • Caries, chip, fractures, abrasions, abfractions, malpositions, braces, diastemas, incipient lesions, open margins, watches

What will not be included:

  • Periodontal probing, recession, or anything else related to a typical periodontal exam. Not because it is not important or wouldn’t be integrated in a UI, but because it would be too much for a simple cdcf file to handle.
  • The billed out procedure (including exams). If an exam is done, it will be part of the comments of the commit log. For example, of a re-care exam is done, the fact a re-care exam was done will not be in the file (the only way you can find it was done would be looking at the git history). However, you would still see the updates to the chart (if any was done) after the exam was done.
  • Non carious lesions (hard or soft). For now, it will be part of the commit log but eventually there will a file and image directory dedicated just for lesions. Things like ulcerations, Morsicatio buccarum and tori are not charted in this file. Please see softTissue.ini and hardTissue.ini along with tmj.ini for this kind of information
  • Treatment plan. That, oddly enough, is done via git branching what treatment you want to do and then merging the actual treatment when you do it. That way, you can have multiple treatment plans, mix treatment plans and even have treatment plan templates for patients.
  • Chief complaint (will also be in the git log)

File format basics

The file will start off with the line “SpecVersion=X” where X is the current version of the spec. In theory, the spec should be backwards and forwards compatible, but this is added in there just in case. For now, each file will start with “SpecVersion=1”. Then, the file will list off each tooth that is existing by its number and then have the “|” to then specify what attributes it has.

So lets start with a patient who has all 32 teeth, all are erupted and not a single filling or cavity. The file would look like this:

SpecVersion=1
1|
2|
3|
4|
5|
6|
7|
8|
9|
10|
11|
12|
13|
14|
15|
16|
17|
18|
19|
20|
21|
22|
23|
24|
25|
26|
27|
28|
29|
30|
31|
32|

 

Now lets go with the other extreme. You have a fully edentulous patient. The file would look like this:

SpecVersion=1

That’s it, mostly a blank file with the “SpecVersion=1” there just to informed the reader that the whole file isn’t empty, just the chart.

Now supernumerary teeth are strange because neither tooth is the “real” one. Therefore, to designate which tooth is being charted, the tooth will show up twice. For example, lets say the patient has a supernumerary tooth #7, the chart would have something like this:

 

...
6|
7|
7|
8|
...

Now you know that there are two teeth that are maxillary right lateral incisors. This would even apply with there are 3 or even more supernumerary teeth. Also, the teeth would be in the order that they are charted. For example:

 

...
6|
7| DL composite
7|
8|
...

The tooth with the DL composite would be the distal tooth #7, not the mesial. Likewise:

...
9|
10| DL composite
10|
11|
...

The tooth with the DL composite would be the mesial tooth #10, not the distal.

Attribute(s) of the teeth

The general format for each tooth will be:

Tooth Number| attribute 1; attribute 2; ... ; attribute n 

Each attribute will be separated by a semicolon. The last attribute does not require a semicolon but does require a newline to end the tooth

Surfaces of the teeth

The following abbreviations will be used in the chart:

  • M = Mesial
  • O = Occlusal
  • D = Distal
  • B = Buccal (valid only for posterior teeth)
  • L = Lingual (for maxillary teeth, lingual and palatal will mean the same thing)
  • F = Facial (valid only for anterior teeth)
  • I = Incisal (valid only for anterior teeth)

Fillings

All fillings must have a surface and material. All fillings are basically inlays regardless if it was direct or indirect. Lets say there is a MOD amalgam on tooth #30, the line would be:

30| MOD amalgam

There must be a space between the surfaces and the material. The surfaces can come in any order. Repeat surfaces are ignored. A Class V restoration is not charted in this system as it would be an B or F anyway.

The material type can be anything, but it is recommended to use only “amalgam”, “composite”, “irm”, “gold”, or “porcelain”. Using a something like Fuji IX (resin modified glass ionomer) should be charted as composite for the sake of a simple UI but adding in “RMGI” would still be valid; but not recommended.

An onlay is really a crown, just to a full crown. Please see the crown section for more information.

Decay

Similar to fillings, decay must have a surface. For example, if #12 has distal caries, you would chart:

12| D caries

This is independent if you wish to do a DO restoration or if there is existing restorations. If #8 has a ML composite with recurrent decay, you would see:

8| ML composite; ML caries

If the decay is incipient, then it is marked as “incipient decay” like:

3| M incipient decay

The UI at this point would assume you want to watch the surface.

Crowns

Crowns are marked with the format:

tooth number| [material] 

For example, if #9 as a porcelain fused to metal crown, then:

9| [PFM] 

It is recommended to only use the terms “PFM”, “gold”, “ceramic”, “titanium”, or “zirconia”. Technically, any free text can be valid but then the UI may not be able to convert the term to whatever color/scheme it is using.

If no surfaces are mentioned, then it is assumed you are talking about a full coverage crown. If there is even a single surface, then it is assumed you are talking about an onlay. For example:

3| [DL gold] 

Is a distal lingual gold onlay on tooth #3. Without the “[” and “]”, it is assumed we are talking about a filling (or inlay) rather than an onlay.

You can still combine a filling with a full coverage crown. For example, lets say #30 was endodontically treated with a gold restoration, and then had recurrent infection and had to be retreated with a composite to cover up the access. It would be charted as:

30| RCT; post; composite core; [gold]; O composite

Endodontic treatment

If the tooth had a root canal treatment, then the attribute would be “RCT”. No surfaces. It is implied that if a tooth has the “RCT” attribute, then root canal treatment is done. For example, if #11 had a root canal and it was finished off with the lingual access closed via composite, it would be:

11| RCT; L composite

Implant

If a tooth has an implant, it will have the “implant” attribute. If there are no words after the term “implant”, then it means we don’t know the details. For example, if we know that #18 has an implant, and a PFM implant crown, but we don’t know what brand of implant, then we would say:

18| implant; [PFM]

It is implied with this system that any crown with an implant is an implant crown. Let say we know #30 is a T3 model BOPS6515, then we say:

30| implant T3 BOPS6515; abutment custom; [PFM]

Note that first it was the vendor name, and then a space, and then the model name.

Posts

Posts do not have a surface, but they have a material. If you place a parapost on #13, then you would have:

13| RCT; post parapost; core amalgam; [PFM]

If is unclear which type of post was placed, then the attribute would just be “post” and that’s it.

Core

Similar to posts, there are no surfaces but there is a material. Is there is a

Additional attributes

So, now that we talked about a lot of examples, lets just go one by one and talk about the remaining attributes. It is assumed you will know what the attribute itself means. Just like the previous examples, if there is a surface for the attribute, it would come first.

  • erosion (surface required)
  • chip (surface required)
  • fracture (surface required)
  • watch (surface required)
  • heavy wear (no surface required)
  • discoloration (no surface required)
  • white spot lesion (surface required)
  • stain (surface required)

Free text comments

All comments start with the “#”. The comment is over with either a newline or semicolon. For example:

19| heavy wear; #patient says he uses that tooth to chew on everything

Anything I am missing?

I am sure I am missing out on some more attributes, please leave a comment or e-mail me for suggestions. Thanks.

The .ini file and personal.ini

As mentioned in the previous post, one of the files that will be in the patient’s chart will be “contacts.ini”. So lets break it down:

The .ini file format

Rather than re-inventing the wheel, we can just use a format that many toolkits and other APIs already use: the .ini file. You can read the details of how a .ini file looks like by reading the Wikipedia article on it.

Why go with .ini files?

First of all, a .ini file is human readable. If all else fails, somebody can read the file using a text editor and see the data. This also makes is much easier to debug.

Secondly, it does allow features like sections, being able to assign clear labels for each value, and adding comments.

The third (but not final) reason is that Qt (which Clear Dental will use as the front end) has a built in .ini parser that is fast and light weight. It can also store and serialize strings, integers, etc. all transparently without any issues.

What is wrong with all the other file formats?

XML is not a good choice because although it can be human readable, it can be overly verbose and unnecessarily complex. Also, the lack of hierachy in .ini is more a feature than a bug in this case because we actually want to discourage a fully complex dataset when all we need is just simple data about the patient. This is why JSON probably would not be a good idea either despite being much better than XML.

A .CSV file would not work well either because we normally store things in a simple “key” and “value”. Therefore, the entire file would just be two columns and we also wouldn’t get other features like groups.

YAML could work but would require 3rd party libraries which have their own maintenance required and may not be supported with Qt.

However, in the future, we could move from a .ini file to something else like a .yaml by simply converting the files from one format to another which would be simple because we are working with the file system, not a complex SQL database.

How .ini files will work in this system

The front end software should work even if all fields are missing. Although the presentation would be nonsensical, it should not crash due to lack of data. Therefore, all fields are either “recommended” or “optional”.

The term “recommended” simply means the person entering the data should put it in. If the field is blank, the UI should warn the user that it is blank but the user can still override the system and allow it to move on.

The term “optional” means that there are no warning if the field is missing. This means the UI has to be able to present the data without the field. From here on out, all optional fields will be in italics.

Back to our regularly scheduled program: personal.ini

The file will have the following groups:

    • Name
    • Personal
    • Address
    • Phones
    • Emails
    • Work
    • Emergency Contact
    • Preferences
  • Others

Lets now go in to each group in detail:

Name

Holds everything related to the patient’s name. The name group will have the following keys:

FirstName: The patients first name
NamePhonetic: How to say the patient’s first name (or preferred name) phonetically
MiddleName: The patient’s middle name or middle initial
LastName: The patient’s last name
PreferredName: What the patient would preferred to be called
PreviousName: In case of a name change, what they used to be named (in case insurance has the old name)

Example:

[Name]
FirstName=Jacqueline
NamePhonetic=Say "Jack" with an "e" at the end
MiddleName=Betty
Lastname=Smith
PreferredName=Jackie
PreviousName=Jacqueline Doe

 

Personal

Holds everything related to the patient’s personal data. Please note that things like family relationship is stored as symbolic links in the “family” folder. Things like social security number are stored in the “finance” folder. The personal group will have the following keys:
DateOfBirth: The date of birth of the patient, in the format “MM/dd/yyyy” (American format).
Sex: The sex of the patient (“male” or “female”)
Gender: The gender of the patient (which is independent of sex). This field can include “Male”, “Female”, “Transman”, Transwoman”, “Genderqueer” or “Other”. The patient’s gender will override the sex field when it comes to “he” vs. “she” vs. “they”.
Race: In case you are working in a public health facility and need to kept records of race. This is using the CDREC race codes (the concept field)
Ethnicity: In case you are working in a public health facility and need to kept records of ethnicity. This is using the CDREC ethnicity codes (the concept field)

Example:

[Personal]
DateOfBirth=07/12/1972
Sex=Female
Gender=Female
Race=White
Ethnicity=Not Hispanic or Latino

 

Address

This will hold all the basic information about the physical address. In theory, these fields will be “localized” to the region of the practicing doctor (which is why .ini files are nice because they can be extendable and still valid). The address has the following keys:
StreetAddr: The full address including the street number (like “123 Main Street”). In situations where additional lines are needed (like “Apartment 23E”), the escape character  “\n” will be used to append additional lines.
City: The city, town or village
State: The state, commonwealth, prefecture, etc. Use the normal postage prefix like “MA” for Massachusetts or “CA” for California.
PostalCode: Zip or postal code
Country: Nation or country using the ISO 3166-1 Alpha-2 code

Example:

[Address]
StreetAddr=123 Main Street
City=Burlington
State=VT
Zip=05401
Country=US

 

Phones

Basic phone numbers. This whole section is optional. It will have the following keys:
HomePhone: Telephone number of the land line. Recommended to have area code but country code is optional. No dashes but a “+” will be used for country codes.
CellPhone:Cellphone number. Recommended to have area code but country code is optional. No dashes but a “+” will be used for country codes.

Example:

[Phones]
HomePhone=1234567890
CellPhone=+442079460625

 

Emails

Emails can include any form including traditional email or other social networking sites (assuming they have a messaging service). This whole section is optional. The Emails group has the following keys:
Email: This is the traditional email in the format “somebody@example.com”
Facebook: Facebook id of the patient
Instagram: Instagram username of the patient
WhatsApp: WhatsApp phone number

Example:

[Emails]
Email=someone@example.com
Facebook=JoneDoneh
Instagram=daviddobrik
WhatsApp=1234567890

 

Work

Everything related to work. This whole section is optional. The Work group has the following keys:
WorkAddr: Work address. As with personal address, the “\n” is used for additional lines needed
WorkCity: Work city
WorkState: State in which the work location is
WorkPhone: Phone number to contact the patient during work
WorkEmail: Email that the patient uses while at work

Example:

[Work]
WorkAddr=The ACME Company\n123 Main Street
WorkCity=Burlington
WorkState=VT
WorkPhone=1224567890
WorkEmail=work@example.com

 

Emergency Contact

This section relates to how to get in touch with the patient’s emergency contact. The Emergency Contact group has the following keys:
EmergencyName: Full name of the emergency contact
EmergencyPhone: Phone number of the emergency contact
EmergencyRelation: The relationship with the person and the emergency contact

Example:

[Work]
EmergencyName=Joe Smith
EmergencyPhone=1234567890
EmergencyRelation=Father

 

Preferences

This section is what the patient has preferences for which can make their experience better. This entire section is optional. The preferences group has the following keys:
PreferredLanguage: The language the patient prefers to communicate in (in ISO 639-1 two letter code)
AvailableDays: Which type of days the patient is available for. The days of the week are abbreviated as “Su M Tu W Th F Sa”. For example, if the patient is only available on Mondays and Thursdays, the value would be “M Th”. If the patient can come in any day of the week, then you would see all “Su M Tu W Th F Sa”.
AvailableTime: Which time of day the patient prefers to come in. These are in a series of ranges which are in 24H format. For example, lets say the patient can come in anytime before noon, then it will have the value “0-12” (the zero is for midnight and 12 hours is noon time). Lets say the patient can come in anytime after 3PM. Then it would have a value of “15-23” (15 being 3PM and 23 being 11PM). Lets say the patient can only come in before 8AM but anytime after 4PM, then it would be “0-8,16-23” with the “,” to separate the ranges.
PreferredContact: How the patient likes to be contacted (appointment reminders, etc.). It can be “homephone”, “cellphone”, “email”, “text”, “workphone”, “workemail”, “facebook”, “instagram”, “whatsapp” or some other text value a human can understand.
PreferredDoctor: Which doctor the patient would like to see. If the patient likes to see Dr. Smith, then the value would be “Dr. Smith”. If the patient likes all the doctors except for Dr. Joe, it would be “-Dr. Joe” (the “-” prefix means patient does not like this particular doctor).

Example:

[Preferences]
PreferredLanguage=en
AvailableDays=Th F
AvailableTime=3-23
PreferredContact=text
PreferredDoctor=Dr. Willis

 

Others

As with all the .ini files in this system, there will be an “others” section for any kind of additional data that was not originally in this spec. For now, it will have this one field:
SpecVersion: The version of the spec that this file follows. In theory, this should not matter as it should be backwards compatible but sometimes mistakes happen so it would be nice to have a good “get out of jail free” card.

Example:

[Others]
SpecVersion=1

 

And that’s it!

Please leave a comment on anything that should be changed or added with the specs.

A free and open dental charting file format

What problem am I trying to solve?

When a new patient enters a dental office, a lot of work is being redone:

  • Basic information about the patient Like date of birth, address, contact information. Not the end of the world but we are just getting started.
  • Medical history Like current medications, previous medications, history of surgeries, allergies and other information that would be vital to the doctor
  • Dental history What what done for each tooth, when and where the procedures were done. It would also be nice to know the circumstances for the procedure (as it would be in the case notes)
  • Radiographic history The nice thing about digital radiographs is that they can be emailed from one office to another in a lossless conversion. However, now that many patients will have a long history of radiographs over the years, there is no standard way to submit all radiographs from one office to another. Therefore, you tend to see most offices just send the last set of bitewings and panoraomic and call it a day. This would give the doctor a very limited view of the patient’s history and will have to rely too much on the patient for the date and time for each procedure.

Therefore, lets make a file format that encapsulates everything and makes it easily transferable from one office to another.

But why hasn’t other companies solved this issue yet?

The main reason for this is because almost all other Dental EHR systems out there uses a relational database to store all of its data. Therefore, the export and import process from one schema to another is complex and often unsupported. This is on top of the apparent conflict of interest that prevents commercial software from making the transition from one Dental EHR to another seamless.

What is the “Clear Dental” Solution?

Lets first talk about the “files” that each patient will have. Each patient will have a folder of files which will contain the following:

Patient Name - DOB/
     personal.ini #personal information
     dental.cdcf #dental chart
     perio.cpcf #perio chart
     tmj.ini #temporomandibular joint information / charting
     hardTissue.ini #hard tissue information outside of dental.cdcf
     softTissue.ini #soft tissue information outside of dental.cdcf
     recall.ini #when to bring the patient back for exams / radiographs
     medications.json #all medications (including ones prescribed at this office)
     surgical.json #all major surgeries the patient had
     allergies.json #all allergies
     dentalPlans.json #dental plan information
     family/ #symbolic links to family members
     finance/ #financial transactions (does not get exported)
     appointments/ #appoints the patient had (does not get exported)
     images/ #folder for all images
     images/photograph/ #regular pictures
     images/photograph/extraoral/
     images/photograph/extraoral/profile.jpg #default profile picture
     images/radiograph/
     images/radiograph/bitewings/
     images/radiograph/panoramic/
     images/radiograph/cephalometric/
     .git/ #standard git repo data

Details and specifications about each file and how it relates to the patient will released soon.

Hey! You said you were making a file format! You just outlined a directory!

Indeed. Rather than making one single file for all the data, lets first of all separate it out to simple text files and images that can be read by any viewer and then use tar to put them all to a single file. Then, we compress the tar file using lzma. Both are free and open source tools that are installed on most Linux platforms including the Clear Dental version of Linux. Therefore the file format will be:

{Patient's first name}_{Patient's middle name}_{Patient's last name}[Birthday-Birthmonth-Birthyear].tar.xz

For example, lets pretend your patient’s name is John Corry Doe who was born on January 2nd on 1970. His filename would be “John_Corry_Doe[02-01-1970].tar.lzma”. If the patient has a middle initial but not a middle name like David C. Riley who was born December 18th of 2002, his filename would be: “David_C._Riley[18-12-2002].tar.lzma”.

The last “lzma” part can in theory be changed to any kind of compression including bzip2 or gzip and the OS would be able to decompress appropriately.

What about history of changes?

That is the beauty of git! By having each file be text based, git can be used to keep a history of the changes including dental chart history, medical history changes, etc. And the doctor can see when each change was made and also who made each change. Clear Dental is going to use a graphical diff tool to make it easy for anybody to compare the changes.

What’s next?

Stay tuned for updates on the specifications for each of the files outlined in the directory. All drafts are open for comments and may evolve over time.