kivio

kivio_dia_stencil_spawner.cpp

00001 /*
00002 * Kivio - Visual Modelling and Flowcharting
00003 * Copyright (C) 2001 Nikolas Zimmermann <wildfox@kde.org>
00004 *
00005 * This program is free software; you can redistribute it and/or
00006 * modify it under the terms of the GNU General Public License
00007 * as published by the Free Software Foundation; either version 2
00008 * of the License, or (at your option) any later version.
00009 *
00010 * This program is distributed in the hope that it will be useful,
00011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
00012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00013 * GNU General Public License for more details.
00014 *
00015 * You should have received a copy of the GNU General Public License
00016 * along with this program; if not, write to the Free Software
00017 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
00018 */
00019 
00020 #include <qdom.h>
00021 #include <qfile.h>
00022 #include <qregexp.h>
00023 #include <qstringlist.h>
00024 #include <kdebug.h>
00025 #include <math.h>
00026 
00027 #include "kivio_stencil.h"
00028 #include "kivio_dia_stencil_spawner.h"
00029 #include "diapathparser.h"
00030 
00031 KivioDiaStencilSpawner::KivioDiaStencilSpawner(KivioStencilSpawnerSet *p) : KivioStencilSpawner(p)
00032 {
00033     m_smlStencilSpawner = new KivioSMLStencilSpawner(p);
00034 }
00035 
00036 KivioDiaStencilSpawner::~KivioDiaStencilSpawner()
00037 {
00038 }
00039 
00040 QDomElement KivioDiaStencilSpawner::saveXML(QDomDocument &d)
00041 {
00042     return m_smlStencilSpawner->saveXML(d);
00043 }
00044 
00045 void KivioDiaStencilSpawner::calculateDimensions(float x, float y)
00046 {
00047     m_xlist.append(x);
00048     m_ylist.append(y);
00049 }
00050 
00051 float KivioDiaStencilSpawner::diaPointToKivio(float point, bool xpoint)
00052 {
00053     float returnPoint = 0.0;
00054     if(xpoint)
00055     {
00056         //if(m_lowestx < 0)
00057             returnPoint = point - m_lowestx;
00058         //else
00059         //  returnPoint =  (fabs(m_highestx) - fabs(m_lowestx)) - (fabs(m_highestx) + fabs(point));
00060     }
00061     else
00062     {
00063         //if(m_lowesty <  0)
00064             returnPoint =point - m_lowesty;
00065         //else
00066         //  returnPoint =  (fabs(m_highesty) + fabs(m_lowesty)) - (fabs(m_highesty) + fabs(point));
00067     }
00068     //kdDebug () << "Point " << point << " Return point " << returnPoint << endl;
00069     return returnPoint;
00070 }
00071 
00072 bool KivioDiaStencilSpawner::load(const QString &file)
00073 {
00074     QDomDocument dia("test");
00075     QDomDocument kivio("XML");
00076 
00077     m_filename = file;
00078     QFile f(file);
00079 
00080     if(f.open(IO_ReadOnly) == false)
00081     {
00082         kdDebug(43000) << "KivioDiaStencilSpawner::load() - Error opening stencil: " << file << endl;
00083         return false;
00084     }
00085     dia.setContent(&f);
00086     QDomNode diaMain = dia.namedItem("shape");
00087 
00088     // Set "creator" attribute
00089     QDomElement firstElement = kivio.createElement("KivioShapeStencil");
00090     firstElement.setAttribute("creator", "kiviodiafilter");
00091 
00092     kivio.appendChild(firstElement);
00093 
00094     // Add KivioSMLStencilSpawnerInfo
00095     QDomElement spawnerInfoElement = kivio.createElement("KivioSMLStencilSpawnerInfo");
00096     QDomElement authorInfoElement = kivio.createElement("Author");
00097     authorInfoElement.setAttribute("data", "n/a");
00098     QDomElement titleInfoElement = kivio.createElement("Title");
00099     titleInfoElement.setAttribute("data", diaMain.namedItem("name").toElement().text());
00100     QDomElement idInfoElement = kivio.createElement("Id");
00101     idInfoElement.setAttribute("data", diaMain.namedItem("name").toElement().text());
00102     QDomElement descriptionInfoElement = kivio.createElement("Description");
00103     descriptionInfoElement.setAttribute("data", diaMain.namedItem("description").toElement().text());
00104     QDomElement versionInfoElement = kivio.createElement("Version");
00105     versionInfoElement.setAttribute("data", "1.0");
00106     QDomElement webInfoElement = kivio.createElement("Web");
00107     webInfoElement.setAttribute("data", "http://");
00108     QDomElement emailInfoElement = kivio.createElement("Email");
00109     emailInfoElement.setAttribute("data", "n/a");
00110     QDomElement copyrightInfoElement = kivio.createElement("Copyright");
00111     copyrightInfoElement.setAttribute("data", "n/a");
00112     QDomElement autoUpdateInfoElement = kivio.createElement("AutoUpdate");
00113     autoUpdateInfoElement.setAttribute("data", "off");
00114 
00115     spawnerInfoElement.appendChild(authorInfoElement);
00116     spawnerInfoElement.appendChild(titleInfoElement);
00117     spawnerInfoElement.appendChild(idInfoElement);
00118     spawnerInfoElement.appendChild(descriptionInfoElement);
00119     spawnerInfoElement.appendChild(versionInfoElement);
00120     spawnerInfoElement.appendChild(webInfoElement);
00121     spawnerInfoElement.appendChild(emailInfoElement);
00122     spawnerInfoElement.appendChild(copyrightInfoElement);
00123     spawnerInfoElement.appendChild(autoUpdateInfoElement);
00124 
00125     kivio.documentElement().appendChild(spawnerInfoElement);
00126 
00127     m_xscale = m_yscale = 20.0f;
00128 
00129     // Add Dimensions
00130     QDomElement dimensionsElement = kivio.createElement("Dimensions");
00131     kivio.documentElement().appendChild(dimensionsElement);
00132 
00133     // Calculate Dimensions
00134     QDomElement svgElement = diaMain.namedItem("svg:svg").toElement();
00135     QDomNode svgNode = svgElement.firstChild();
00136     while(!svgNode.isNull())
00137     {
00138         QDomElement svgChild = svgNode.toElement();
00139         if(!svgChild.isNull())
00140         {
00141             if(svgChild.tagName() == "svg:rect")
00142             {
00143                 // TODO: rx and ry -> rounded rects
00144                 if(svgChild.hasAttribute("x") && svgChild.hasAttribute("y") && svgChild.hasAttribute("width") && svgChild.hasAttribute("height"))
00145                 {
00146                     calculateDimensions(svgChild.attribute("x").toFloat(), svgChild.attribute("y").toFloat());
00147                     calculateDimensions(svgChild.attribute("x").toFloat() + svgChild.attribute("width").toFloat(), svgChild.attribute("y").toFloat() + svgChild.attribute("height").toFloat());
00148                 }
00149             }
00150             else if(svgChild.tagName() == "svg:circle")
00151             {
00152                 if(svgChild.hasAttribute("cx") && svgChild.hasAttribute("cy") && svgChild.hasAttribute("r"))
00153                 {
00154                     calculateDimensions((svgChild.attribute("cx").toFloat()) - (svgChild.attribute("r").toFloat()), (svgChild.attribute("cy").toFloat()) - (svgChild.attribute("r").toFloat()));
00155                     calculateDimensions((svgChild.attribute("cx").toFloat()) + (svgChild.attribute("r").toFloat()), (svgChild.attribute("cy").toFloat()) + (svgChild.attribute("r").toFloat()));
00156                 }
00157             }
00158             else if(svgChild.tagName() == "svg:ellipse")
00159             {
00160                 if(svgChild.hasAttribute("cx") && svgChild.hasAttribute("cy") && svgChild.hasAttribute("rx") && svgChild.hasAttribute("ry"))
00161                 {
00162                     calculateDimensions((svgChild.attribute("cx").toFloat()) - (svgChild.attribute("rx").toFloat()), (svgChild.attribute("cy").toFloat()) - (svgChild.attribute("ry").toFloat()));
00163                     calculateDimensions((svgChild.attribute("cx").toFloat()) + (svgChild.attribute("rx").toFloat()), (svgChild.attribute("cy").toFloat()) + (svgChild.attribute("ry").toFloat()));
00164                 }
00165             }
00166             else if(svgChild.tagName() == "svg:line")
00167             {
00168                 if(svgChild.hasAttribute("x1") && svgChild.hasAttribute("y1") && svgChild.hasAttribute("x2") && svgChild.hasAttribute("y2"))
00169                 {
00170                     calculateDimensions(svgChild.attribute("x1").toFloat(), svgChild.attribute("y1").toFloat());
00171                     calculateDimensions(svgChild.attribute("x2").toFloat(), svgChild.attribute("y2").toFloat());
00172                 }
00173             }
00174             else if(svgChild.tagName() == "svg:polyline")
00175             {
00176                 if(svgChild.hasAttribute("points"))
00177                 {
00178                     QStringList points = QStringList::split(" ", svgChild.attribute("points"));
00179                     for(QStringList::Iterator it = points.begin(); it != points.end(); ++it)
00180                     {
00181                         QString x, y;
00182 
00183                         QStringList parsed = QStringList::split(",", (*it));
00184                         QStringList::Iterator itp = parsed.begin();
00185                         x = (*itp);
00186                         ++itp;
00187                         y = (*itp);
00188 
00189                         calculateDimensions(x.toFloat(), y.toFloat());
00190                     }
00191                 }
00192             }
00193             else if(svgChild.tagName() == "svg:polygon")
00194             {
00195                 if(svgChild.hasAttribute("points"))
00196                 {
00197                     QStringList points = QStringList::split(" ", svgChild.attribute("points"));
00198                     for(QStringList::Iterator it = points.begin(); it != points.end(); ++it)
00199                     {
00200                         QString x, y;
00201 
00202                         QStringList parsed = QStringList::split(",", (*it));
00203                         QStringList::Iterator itp = parsed.begin();
00204                         x = (*itp);
00205                         ++itp;
00206                         y = (*itp);
00207 
00208                         calculateDimensions(x.toFloat(), y.toFloat());
00209                     }
00210                 }
00211             }
00212             else if(svgChild.tagName() == "svg:path")
00213             {
00214                 if(svgChild.hasAttribute("d"))
00215                 {
00216                     DiaPointFinder *dpf = new DiaPointFinder(&m_xlist, &m_ylist);
00217                     dpf->parseSVG(svgChild.attribute("d"), true);
00218                     delete dpf;
00219                 }
00220             }
00221         }
00222         svgNode = svgNode.nextSibling();
00223     }
00224 
00225     QValueList<float>::Iterator itx = m_xlist.begin();
00226     QValueList<float>::Iterator ity = m_ylist.begin();
00227     m_highestx = *itx;
00228     m_lowestx = *itx;
00229     m_highesty = *ity;
00230     m_lowesty = *ity;
00231     ++itx;
00232     ++ity;
00233 
00234     for( ; itx != m_xlist.end(); ++itx)
00235     {
00236         m_highestx = QMAX(m_highestx, *itx);
00237         m_lowestx = QMIN(m_lowestx, *itx);
00238     }
00239 
00240     for( ; ity != m_ylist.end(); ++ity)
00241     {
00242         m_highesty = QMAX(m_highesty, *ity);
00243         m_lowesty = QMIN(m_lowesty, *ity);
00244     }
00245 
00246     //if( svgElement.hasAttribute("width") && svgElement.hasAttribute("height"))
00247     //{
00248     //  m_yscale = svgElement.attribute("height").toFloat()/(m_highesty - m_lowesty);
00249     //  m_xscale = svgElement.attribute("width").toFloat()/(m_highestx - m_lowestx);
00250     //}
00251     //else
00252     {
00253         // scale the shape to be close to 30 by 30
00254         m_yscale = 30.0/(m_highesty - m_lowesty);
00255         m_xscale = 30.0/(m_highestx - m_lowestx);
00256     }
00257 
00258     // Add KivioConnectorTarget's
00259     QDomElement connectionsElement = diaMain.namedItem("connections").toElement();
00260     QDomNode connectionsNode = connectionsElement.firstChild();
00261     while(!connectionsNode.isNull())
00262     {
00263         QDomElement connectionChild = connectionsNode.toElement();
00264         if(!connectionChild.isNull())
00265         {
00266             if(connectionChild.tagName() == "point")
00267             {
00268                 if(connectionChild.hasAttribute("x") && connectionChild.hasAttribute("y"))
00269                 {
00270                     QDomElement kivioConnectorTarget = kivio.createElement("KivioConnectorTarget");
00271                     kivioConnectorTarget.setAttribute("x", QString::number(diaPointToKivio(connectionChild.attribute("x").toFloat(),true) * m_xscale));
00272                     kivioConnectorTarget.setAttribute("y", QString::number(diaPointToKivio(connectionChild.attribute("y").toFloat(), false) * m_yscale));
00273 
00274                     kivio.documentElement().appendChild(kivioConnectorTarget);
00275                 }
00276             }
00277         }
00278         connectionsNode = connectionsNode.nextSibling();
00279     }
00280 
00281     // Add KivioShape's and convert to Kivio's Coordinate System
00282     svgNode = svgElement.firstChild();
00283     int runs = 0;
00284     while(!svgNode.isNull())
00285     {
00286         QDomElement svgChild = svgNode.toElement();
00287         if(!svgChild.isNull())
00288         {
00289             if(svgChild.tagName() == "svg:rect")
00290             {
00291                 runs++;
00292                 // TODO: rx and ry -> rounded rects
00293                 if(svgChild.hasAttribute("x") && svgChild.hasAttribute("y") && svgChild.hasAttribute("width") && svgChild.hasAttribute("height"))
00294                 {
00295                     QDomElement kivioShape = kivio.createElement("KivioShape");
00296                     kivioShape.setAttribute("type", "Rectangle");
00297                     kivioShape.setAttribute("name", QString::fromLatin1("Element") + QString::number(runs));
00298                     kivioShape.setAttribute("x", QString::number(diaPointToKivio(svgChild.attribute("x").toFloat(),true) * m_xscale));
00299                     kivioShape.setAttribute("y", QString::number(diaPointToKivio(svgChild.attribute("y").toFloat(), false) * m_yscale));
00300                     kivioShape.setAttribute("w", QString::number(svgChild.attribute("width").toFloat() * m_xscale));
00301                     kivioShape.setAttribute("h", QString::number(svgChild.attribute("height").toFloat() * m_yscale));
00302                     kivio.documentElement().appendChild(kivioShape);
00303                 }
00304             }
00305             else if(svgChild.tagName() == "svg:circle")
00306             {
00307                 runs++;
00308                 if(svgChild.hasAttribute("cx") && svgChild.hasAttribute("cy") && svgChild.hasAttribute("r"))
00309                 {
00310                     QDomElement kivioShape = kivio.createElement("KivioShape");
00311                     kivioShape.setAttribute("type", "Ellipse");
00312                     kivioShape.setAttribute("name", QString::fromLatin1("Element") + QString::number(runs));
00313                     kivioShape.setAttribute("x", QString::number((diaPointToKivio(svgChild.attribute("cx").toFloat() - svgChild.attribute("r").toFloat(),true) * m_xscale)));
00314                     kivioShape.setAttribute("y", QString::number((diaPointToKivio(svgChild.attribute("cy").toFloat() - svgChild.attribute("r").toFloat(), false) * m_yscale)));
00315                     kivioShape.setAttribute("w", QString::number(svgChild.attribute("r").toFloat() * m_xscale * 2));
00316                     kivioShape.setAttribute("h", QString::number(svgChild.attribute("r").toFloat() * m_yscale * 2));
00317                     kivio.documentElement().appendChild(kivioShape);
00318                 }
00319             }
00320             else if(svgChild.tagName() == "svg:ellipse")
00321             {
00322                 runs++;
00323                 if(svgChild.hasAttribute("cx") && svgChild.hasAttribute("cy") && svgChild.hasAttribute("rx") && svgChild.hasAttribute("ry"))
00324                 {
00325                     QDomElement kivioShape = kivio.createElement("KivioShape");
00326                     kivioShape.setAttribute("type", "Ellipse");
00327                     kivioShape.setAttribute("name", QString::fromLatin1("Element") + QString::number(runs));
00328                     kivioShape.setAttribute("x", QString::number((diaPointToKivio(svgChild.attribute("cx").toFloat() - svgChild.attribute("rx").toFloat(),true) * m_xscale)));
00329                     kivioShape.setAttribute("y", QString::number((diaPointToKivio(svgChild.attribute("cy").toFloat() - svgChild.attribute("ry").toFloat(), false) * m_yscale)));
00330                     kivioShape.setAttribute("w", QString::number(svgChild.attribute("rx").toFloat() * m_xscale * 2));
00331                     kivioShape.setAttribute("h", QString::number(svgChild.attribute("ry").toFloat() * m_yscale * 2));
00332                     kivio.documentElement().appendChild(kivioShape);
00333                 }
00334             }
00335             else if(svgChild.tagName() == "svg:line")
00336             {
00337                 runs++;
00338                 if(svgChild.hasAttribute("x1") && svgChild.hasAttribute("y1") && svgChild.hasAttribute("x2") && svgChild.hasAttribute("y2"))
00339                 {
00340                     QDomElement kivioShape = kivio.createElement("KivioShape");
00341                     kivioShape.setAttribute("type", "LineArray");
00342                     kivioShape.setAttribute("name", QString::fromLatin1("Element") + QString::number(runs));
00343 
00344                     QDomElement lineArrayElement = kivio.createElement("Line");
00345                     lineArrayElement.setAttribute("x1", QString::number(diaPointToKivio(svgChild.attribute("x1").toFloat(),true) * m_xscale));
00346                     lineArrayElement.setAttribute("y1", QString::number(diaPointToKivio(svgChild.attribute("y1").toFloat(), false) * m_yscale));
00347                     lineArrayElement.setAttribute("x2", QString::number(diaPointToKivio(svgChild.attribute("x2").toFloat(),true) * m_xscale));
00348                     lineArrayElement.setAttribute("y2", QString::number(diaPointToKivio(svgChild.attribute("y2").toFloat(), false) * m_yscale));
00349 
00350                     kivioShape.appendChild(lineArrayElement);
00351                     kivio.documentElement().appendChild(kivioShape);
00352                 }
00353             }
00354             else if(svgChild.tagName() == "svg:polyline")
00355             {
00356                 runs++;
00357                 if(svgChild.hasAttribute("points"))
00358                 {
00359                     QDomElement kivioShape = kivio.createElement("KivioShape");
00360                     kivioShape.setAttribute("type", "Polyline");
00361                     kivioShape.setAttribute("name", QString::fromLatin1("Element") + QString::number(runs));
00362 
00363                     QStringList points = QStringList::split(" ", svgChild.attribute("points"));
00364                     for(QStringList::Iterator it = points.begin(); it != points.end(); ++it)
00365                     {
00366                         QString x, y;
00367 
00368                         QStringList parsed = QStringList::split(",", (*it));
00369                         QStringList::Iterator itp = parsed.begin();
00370                         x = (*itp);
00371                         ++itp;
00372                         y = (*itp);
00373 
00374                         QDomElement kivioPointElement = kivio.createElement("KivioPoint");
00375                         kivioPointElement.setAttribute("x", QString::number(diaPointToKivio(x.toFloat(),true) * m_xscale));
00376                         kivioPointElement.setAttribute("y", QString::number(diaPointToKivio(y.toFloat(), false) * m_yscale));
00377 
00378                         kivioShape.appendChild(kivioPointElement);
00379                     }
00380                     kivio.documentElement().appendChild(kivioShape);
00381                 }
00382             }
00383             else if(svgChild.tagName() == "svg:polygon")
00384             {
00385                 runs++;
00386                 if(svgChild.hasAttribute("points"))
00387                 {
00388                     QDomElement kivioShape = kivio.createElement("KivioShape");
00389                     kivioShape.setAttribute("type", "Polygon");
00390                     kivioShape.setAttribute("name", QString::fromLatin1("Element") + QString::number(runs));
00391 
00392                     QStringList points = QStringList::split(" ", svgChild.attribute("points"));
00393                     for(QStringList::Iterator it = points.begin(); it != points.end(); ++it)
00394                     {
00395                         QString x, y;
00396 
00397                         QStringList parsed = QStringList::split(",", (*it));
00398                         QStringList::Iterator itp = parsed.begin();
00399                         x = (*itp);
00400                         ++itp;
00401                         y = (*itp);
00402 
00403                         QDomElement kivioPointElement = kivio.createElement("KivioPoint");
00404                         kivioPointElement.setAttribute("x", QString::number(diaPointToKivio(x.toFloat(),true) * m_xscale));
00405                         kivioPointElement.setAttribute("y", QString::number(diaPointToKivio(y.toFloat(), false) * m_yscale));
00406 
00407                         kivioShape.appendChild(kivioPointElement);
00408                     }
00409                     kivio.documentElement().appendChild(kivioShape);
00410                 }
00411             }
00412             else if(svgChild.tagName() == "svg:path")
00413             {
00414                 runs++;
00415                 bool isClosed;
00416                 QDomElement kivioShape = kivio.createElement("KivioShape");
00417                 if(svgChild.hasAttribute("d"))
00418                 {
00419 
00420                     if(svgChild.attribute("d").contains('z') || svgChild.attribute("d").contains('Z'))
00421                     {
00422                         isClosed = true;
00423                         kivioShape.setAttribute("type", "ClosedPath");
00424                     }
00425                     else
00426                     {
00427                         isClosed = false;
00428                         kivioShape.setAttribute("type", "OpenPath");
00429                     }
00430 
00431                     kivioShape.setAttribute("name", QString::fromLatin1("Element") + QString::number(runs));
00432 
00433                     DiaPathParser *dpp = new DiaPathParser(&kivio,
00434                         &kivioShape, m_xscale, m_yscale,
00435                         m_lowestx, m_lowesty);
00436                     dpp->parseSVG(svgChild.attribute("d"), true);
00437                     delete dpp;
00438 
00439 
00440                 }
00441 
00442                 if( svgChild.hasAttribute("style"))
00443                 {
00444                     // style="stroke: background; stroke-width: 0.8; stroke-miterlimit: 1; stroke-linecap: round; stroke-linejoin: round"
00445                     // Supported:
00446                     // stroke-width:
00447                     // stroke-linejoin: milter, bevel, round
00448                     // stroke-linecap: round, square, flat
00449                     // fill: ?
00450                     QStringList styles = QStringList::split(";", svgChild.attribute("style"));
00451                     for( uint idx = 0; idx < styles.count(); idx++)
00452                     {
00453                         //kdDebug(43000) << "Style: " << styles[idx] << endl;
00454                         if( isClosed && styles[idx].contains("fill:"))
00455                         {
00456                             QDomElement fillStyle = kivio.createElement("KivioFillStyle");
00457                             if( styles[idx].contains("forground"))
00458                                 fillStyle.setAttribute("color", "#0000");
00459                             else if (styles[idx].contains("background"))
00460                                 fillStyle.setAttribute("color", "#ffff");
00461 
00462                             fillStyle.setAttribute("colorStyle", "1");
00463                             kivioShape.appendChild(fillStyle);
00464                         }
00465                     }
00466                 }
00467                 kivio.documentElement().appendChild(kivioShape);
00468             }
00469         }
00470         svgNode = svgNode.nextSibling();
00471     }
00472 
00473     // Apply width and height
00474 
00475     dimensionsElement.setAttribute("w", QString::number((fabs(m_highestx - m_lowestx)) * m_xscale));
00476     dimensionsElement.setAttribute("h", QString::number((fabs(m_highesty - m_lowesty)) * m_yscale));
00477 
00478     m_xlist.clear();
00479     m_ylist.clear();
00480 
00481     return loadXML(file, kivio);
00482 }
00483 
00484 bool KivioDiaStencilSpawner::loadXML(const QString &file, QDomDocument &d)
00485 {
00486     bool ret = m_smlStencilSpawner->loadXML(file, d);
00487 
00488     m_icon = *m_smlStencilSpawner->icon();
00489     m_pSet = m_smlStencilSpawner->set();
00490     m_pInfo = m_smlStencilSpawner->info();
00491     m_defWidth = m_smlStencilSpawner->defWidth();
00492     m_defHeight = m_smlStencilSpawner->defHeight();
00493 
00494     return ret;
00495 }
00496 
00497 KivioStencil *KivioDiaStencilSpawner::newStencil()
00498 {
00499     KivioStencil *newStencil = m_smlStencilSpawner->newStencil();
00500     newStencil->setSpawner(this);
00501 
00502     return newStencil;
00503 }
KDE Home | KDE Accessibility Home | Description of Access Keys