#include "cdesk.h"
#include "plotxy.h"
#include <cmath>
//#include <stdio.h>
#include <tchar.h> // sntprintf()
/*********
* Scale *
*********/
// Kleinbuchstaben = Messdaten, Großbuchstaben = Bilddaten (Pixelkoordinaten)
void LinScale::make(Float a, Float e, Int A, Int E) {
// Ziel: scale(a)==A und scale(e)==E
e-=a; E-=A; // e und E = Länge
if (!e || !E) return;
f=(Float)E/e; // Neuer Faktor = Längenverhältnis, typisch negativ bei Y-Achsen a*f+O=A
O=A-Int(a*f); // /fp:fast rundet bereits richtig. Compilerabhängig!!
}
void LogScale::make(Float a, Float e, Int A, Int E) {
// Ziel: scale(a)==A und scale(e)==E
// a und e müssen gleiches Vorzeichen haben und !=0 sein
if (!a) return;
if ((e/=a)<=0) return; // e = Quotient
if (!(E-=A)) return; // E = Länge, bei Y-Achsen zumeist negativ
neg=a<0;
// f*log(a)+O==A && f*log(e)+O==E
// O == A-f*log(a) == E-f*log(e)
// E-A == f*log(e)-f*log(a) == f* (log(e)-log(a)) == f* log(e/a)
f=Float(E)/log(e);
if (neg) a=-a;
O=A-Int(log(a)*f);
}
Int LogScale::scale(Float v) const{
if (neg) v=-v;
return Int(f*log(v))+O;
}
Float LogScale::unscale(Int V) const{
Float v=exp(Float(V-O)/f);
if (neg) v=-v;
return v;
}
TCHAR toUnitPrefix(Float&x,Float aufloes,WORD allow) {
if (aufloes<0) aufloes=-aufloes; // sollte nie passieren
Float g=fabs(x);
if (g<aufloes) g=aufloes; // Maximum beider
if (!g) return 0; // Kein Einheitenpräfix
const Float log1000=log(1000.0F);
char b=(char)floor(log(g)/log1000);
if (!b || b<-8 || b>8) return 0;
char bit=b<0?b+8:b+7; // Bit-Bereich 0..15
if (!(allow>>bit&1)) return 0; // Nicht erlaubter Einheitenvorsatz (ks = Kilosekunde: Unüblich!)
x/=(float)pow(1000.0F,b); // x verändern
return TEXT("yzafpnµmkMGPTEZY")[bit]; // Präfix als Buchstabe liefern
}
Float roundToMultiple(Float x, Float mult) {
if (!mult) return x; // nicht runden
if (!my::isfinite(mult)) return x; // nicht runden
return ::ceil(x/mult)*mult; // mult>0: aufrunden, mult<0: abrunden
}
Float roundToLog(Float x, Float base) {
Float b=base;
if (x<0) {x=-x; base=-base;}
#if _MSC_VER > 1900
auto roundfunc=ceilf;
#else
Float(*roundfunc)(Float)=ceilf;
#endif
if (b<0) {b=-b; roundfunc=floorf;}
return pow(base,roundfunc(log(x)/log(b)));
}
static Float roundWithMethod(Float x,DWORD flags,char dir=1) {
if (!x) return 0;
if (!my::isfinite(x)) DebugBreak();
if (x<0) return -roundWithMethod(-x,flags,-dir);
Float base=10;
static const Float p[]={1};
const Float*points=p; // ganze Zahlen
int plen=1;
switch ((flags>>16)&0x0F) {
case 1: { // 1-3-10
static const Float p[]={1,3}; points=p; plen=elemof(p);
}break;
case 2: { // 1-2-5-10
static const Float p[]={1,2,5}; points=p; plen=elemof(p);
}break;
case 3: { // 60er-Reihe
base=60;
static const Float p[]={1,2,5,10,30}; points=p; plen=elemof(p);
}break;
case 4: { // E12-Reihe (nee, exakte Logarithmen!!)
static const Float p[]={1,1.2F,1.5F,1.8F,2.2F,2.7F,3.3F,3.9F,4.7F,5.6F,6.8F,8.2F}; points=p; plen=elemof(p);
}break;
base=2;
case 5: { // binär
}break;
case 6: { // Vielfache von PI
base=Float(::PI);
}break;
}
// points.push(base); // hintenan für Algorithmusvereinfachung
Float fl=::floor(::log(x)/::log(base));
Float fb=::pow(base,fl);
Float y=x/fb; // nun im Bereich 1..9,999
int i;
if (dir>0) {
for (i=0; i<plen; i++) {
if (y<=points[i]) {y=points[i]; break;} // aufrunden
}
if (i==plen) y=base; // ggf. auf nächste Basis
}else if (dir<0) {
for (i=plen; --i>=0;) {
if (y>=points[i]) {y=points[i]; break;} // abrunden
}
}
return y*fb; // Kommastelle setzen
}
/****************
* DisplayRange *
****************/
bool DisplayRange::init(const Range<Float>&r) {
if (inside(r)) return false; // Nicht shrinken
return Range<Float>::expand(r); // Erweitern
}
int DisplayRange::genLinTicks(std::vector<Tick>&ticks, DWORD flags, int steps, Float aufloes) {
if (steps<2) steps=2;
Float dx=(e-a)/steps;
if (dx<aufloes) dx=aufloes;
dx=roundWithMethod(dx,flags);
Float p;
if (flags&Axis::INTEGRALENDS) {
p=a=roundToMultiple(a,-dx);
e=roundToMultiple(e,dx); // glatt aufrunden
}else{ // Im Fall der Zeitachse (für Historie) Grenzen nicht erweitern
// dadurch „rollt“ die Skale bei voller Historie gleichmäßig
p=roundToMultiple(a,dx);
}
ticks.clear();
ticks.resize(steps);
int i=0;
for (; i<steps; p+=dx) {
new(&ticks[i++]) Tick(0,p); // no string yet
}
return i;
}
int DisplayRange::genLogTicks(std::vector<Tick>&ticks,DWORD flags,int steps) {
if (steps<2) steps=2;
Float dx=::log10(e/a);
dx=::floor(dx/steps); // 10 oder 100 oder 1000 ?
if (dx<1) dx=1; // mindestens 10
dx=::pow(10.f,dx);
Float p;
if (flags&Axis::INTEGRALENDS) {
p=a=roundToLog(a,-10);
}else{
p=roundToLog(a,10);
}
ticks.clear(); // call destructors (for strings)
ticks.resize(steps); // call default constructors (= do nothing)
int i=0;
for(;i<steps;p*=dx){
if (!(flags&Axis::INTEGRALENDS) && p>e) break;
new(&ticks[i++]) Tick(0,p); // no string yet
if (p>=e) break;
}
if (flags&Axis::INTEGRALENDS) e=p;
ticks.resize(i);
return i;
}
// Den Bereich trotz Autoscale so festlegen, dass bei bekannter Auflösung
// (des A/D-Wandlers) nicht das Quantisierungsrauschen den bestimmenden Teil ausmacht,
// d.h. das Quantisierungsrauschen unter bspw. 10 Pixel zu drücken
// (Nicht bei Zeitachse anzuwenden!)
void DisplayRange::usefulRange(int minimumOfTicks,Float aufloes) {
if (!(aufloes>0)) return; // Ohne gegebene Auflösung nichts zu machen
if (!(minimumOfTicks>2)) return;
Float l=minimumOfTicks*aufloes;
if (delta()>=l) return;
// Zu wenig dargestellter Bereich:
// Bei unterschiedlichen Vorzeichen von a und e um die Mitte herum
if (a<0 && e>0) {
if (max(-a,e)<=l/2) {
a=-l/2; e=l/2;
}else if (e>-a) a=e-l;
else e=a+l;
}else if (e<=0) { // Bei gleichen Vorzeichen zur Null hin
if (a<-l) e=a+l; // bleibt negativ;
else {a=-l; e=0;}
}else if (e>l) a=e-l;
else{
a=0; e=l; // häufigster Fall!
}
Float _e=a+minimumOfTicks*aufloes;
if (e<_e) e=_e;
}
/********
* Axis *
********/
template<class T> T negIf(bool e,T v) {return e?-v:v;}
template<class T> void setMin(T&a,T b) {if (!(a<=b)) a=b;}
template<class T> void setMax(T&a,T b) {if (!(a>=b)) a=b;}
Axis::Axis(PlotXY*p,const AXISATTR&a):
plot(p),
AXISATTR(a),
cfore(0),
ctitl(0),
ptick(Pen::create(1,cfore)),
ftick(Font::create(TEXT("Arial"),14)),
ftitl(Font::create(TEXT("Arial"),16,Font::bold,a.side&1?a.side&2?-900:900:0)),
padding(0,0,1,1),
scale(a.side&Axis::LOG?(Scale*)new LogScale:new LinScale) {
// sichtbar und Platz beanspruchend wird eine Achse erst durch Verwendung durch einen sichtbaren Trace!
if (!fmt) fmt=TEXT("%.3g");
}
void Axis::distributeTicks() {
}
int YAxis::needSpace(Margin&need) {
/*if (force)*/ updateScaling(true); // generate labels
HDC dc=plot->dc;
SelectFont(dc,ftick);
SIZE sz;
int h;
if (isDigital()) {
GetTextExtentPoint32(dc,TEXT("0"),1,&sz);
need.right=sz.cx+metrics.ticklen;
SelectFont(dc,ftitl);
GetTextExtentPoint32(dc,name,lstrlen(name),&sz);
need.left=sz.cx; // Textbreite (links)
h=sz.cy;
}else{
TCHAR buf[64];
int l=FloatStr(dispRange.a,buf,elemof(buf));
GetTextExtentPoint32(dc,buf,l,&sz);
int w=sz.cx;
l=FloatStr(dispRange.e,buf,elemof(buf));
GetTextExtentPoint32(dc,buf,l,&sz);
setMax(w,(int)sz.cx);
h=sz.cy;
w+=metrics.ticklen;
l=getTitle(buf,elemof(buf));
if (l) {
SelectFont(dc,ftitl);
SIZE szt;
GetTextExtentPoint32(dc,buf,l,&szt);
w+=szt.cy; // Titelhöhe dazu
setMax(h,int(szt.cx-sz.cy)); // Titel darf in Margin überlaufen
}
need.left=side&BOTTOMRIGHT?0:w;
need.right=side&BOTTOMRIGHT?w:0;
}
need.top=sz.cy>>1;
need.bottom=sz.cy-need.top;
return h;
}
int XAxis::needSpace(Margin&need) {
/*if (force)*/ updateScaling(true); // generate labels
HDC dc=plot->dc;
SelectFont(dc,ftick);
SIZE sza,sze;
TCHAR buf[64];
int l=FloatStr(dispRange.a,buf,elemof(buf));
GetTextExtentPoint32(dc,buf,l,&sza);
l=FloatStr(dispRange.e,buf,elemof(buf));
GetTextExtentPoint32(dc,buf,l,&sze);
need.left=(side&REVERSED?sze:sza).cx>>1;
need.right=(side&REVERSED?sza:sze).cx>>1;
int w=sza.cx+sze.cx;
int h=metrics.ticklen+sza.cy;
l=getTitle(buf,elemof(buf));
if (l) {
SelectFont(dc,ftitl);
SIZE sz;
GetTextExtentPoint32(dc,buf,l,&sz);
setMax(w,(int)sz.cx);
h+=sz.cy; // Texthöhe dazu
}
need.top=side&BOTTOMRIGHT?0:h;
need.bottom=side&BOTTOMRIGHT?h:0;
return w-need.getW(); // Mindestbreite für alle Plotareas
}
bool Axis::anyTrace(bool(Trace::*testfunc)()const, bool runwhile) const{
for (traces_t::const_iterator it=traces.begin(); it!=traces.end(); it++) {
if (((*it)->*testfunc)()!=runwhile) return !runwhile;
}
return runwhile;
}
bool Axis::updateScaling(bool force) {
Range<Float>save(dispRange); // tiefe Kopie
if ((dispRange=dataRange)) force=true; // Zuweisungsoperator liefert <true> bei Bereichsvergrößerung
if (!force) return false;
if (!isDigital()) {
// Achtung - Bei leerem "traces"-Array würde Math.min +Infinity liefern
Float W=Float(rect[side&1|4]-metrics.tickwidth); // X- oder Y-Ausdehnung
int w=metrics.fontheight+metrics.gap; // für Y-Achsen
if (isLog()) {
dispRange.genLogTicks(ticks,side,int(W/w));
}else{
Float aufloes = FINF;
for (traces_t::iterator it=traces.begin(); it!=traces.end(); it++) {
setMin(aufloes,(*it)->getData(side&1)->precis());
}
if (!my::isfinite(aufloes)) aufloes=0;
if (!isTime() && !anyTrace(Trace::editable))
dispRange.usefulRange(int(W/metrics.steppx),aufloes); // dispRange ggf. nach oben erweitern
if (!(side&1)) { // X-Achse: Textbreiten messen
dispRange.genLinTicks(ticks,side,20,aufloes); // 20 = pauschale Menge an Ticks
SelectFont(plot->dc,ftick);
int w=0;
for (ticks_t::iterator it=ticks.begin();it!=ticks.end(); it++) {
setMax(w,plot->textWidth(it->t));
}
w+=metrics.gap*2; // mehr Platz bei X-Achsen
}
dispRange.genLinTicks(ticks,0,int(W/w),aufloes); // verändert dispRange!!
}
//int sp=needSpace(false);
//if (sp>metrics.lastSpaceNeeded) plot->update(1); // "arrange" auslösen
//if (sp<metrics.lastSpaceNeeded-metrics.gap) plot->update(1);
}
bool changed=dispRange!=save;
if (changed|force) {
Int A=rect[side&1]+padding[side&1],
E=rect[side&1|2]-padding[side&1|2];
if (side&4) std::swap(A,E);
scale->make(dispRange.a,dispRange.e,A,E);
}
return changed;
}
void Axis::checkRange(bool allowShrink) {
Range<Float> r;
if (isDigital()) r.init(0,1); // Digital: Fester Bereich
else{
r.reset();
traces_t::iterator it;
for (it=traces.begin(); it!=traces.end(); it++) {
Trace&tr=**it;
Range<Float> rtr;
tr.getRange(side&1,rtr); // Von jedem Trace den Bereich nehmen
r.expand(rtr); // und auf Skale anwenden (TODO: Nur Bei AutoExpand)
}
if (!r.hasSpan(precis)) return;
if (allowShrink || dataRange!=r) {
dataRange=r;
if (updateScaling(allowShrink)) // TODO: runden + verzögertes Shrinken
plot->update(8); // Neu zeichnen, aber nicht neu arrangieren
}
}
}
int Axis::axistext(TCHAR*buf,int blen) const {
return _sntprintf(buf,blen,unit?TEXT("%s in %s"):TEXT("%s"),getName(),unit);
}
TCHAR*Axis::axistext() const {
TCHAR*ret=new TCHAR[256];
int l=axistext(ret,256);
realloc(ret,(l+1)*sizeof(TCHAR)); // shrink-only, should always succeed
return ret;
}
int Axis::FloatStr(Float v, TCHAR*buf,int blen) const{
int l=_sntprintf(buf,blen,autotickformat(),v);
TCHAR*p=_tcschr(buf,'.');
if (p) *p=::sDecimal[0];
return l;
}
TCHAR*Axis::FloatStr(Float v) const{
TCHAR*ret=new TCHAR[16];
int l=FloatStr(v,ret,16);
realloc(ret,(l+1)*sizeof(TCHAR));
return ret;
}
/*
void Axis::checkNeed() {
bool need=false;
for (traces_t::iterator it=traces.begin();it!=traces.end();it++) {
if ((*it)->visible()) {need=true; break;}
}
setVisible(need);
}
*/
int Axis::textWidth(const TCHAR*s, int l) const{
return plot->textWidth(s,l);
}
int Axis::getTitle(TCHAR*buf,int buflen) const{
return _sntprintf(buf,buflen,unit&&*unit?TEXT("%s in %s"):TEXT("%s"),getName(),getUnit());
}
void YAxis::paint() {
if (!visible()) return;
HDC dc=plot->dc;
TCHAR buf[64];
if (ticks.size()) {
Int x1=rect[~side&3];
Int x2=x1+negIf(!!(~side&2),metrics.ticklen);
Int x3=x2+negIf(!!(~side&2),metrics.gap);
SelectPen(dc,ptick);
SelectFont(dc,ftick);
Metric metric(dc);
Int textmiddle=(metric.ascent-metric.intlead)>>1;
SetTextColor(dc,cfore);
SetTextAlign(dc,side&2?TA_LEFT|TA_BASELINE:TA_RIGHT|TA_BASELINE); // Ausrichtung
for (ticks_t::iterator it=ticks.begin(); it!=ticks.end(); it++) {
Int y=scale->scale(it->p);
MoveToEx(dc,x1,y,0);
LineTo(dc,x2,y);
int l=getTickText(*it,buf,elemof(buf));
if (l) TextOut(dc,x3,y+textmiddle,buf,l);
}
}
int l=getTitle(buf,elemof(buf));
if (l) {
SetTextColor(dc,ctitl);
SelectFont(dc,ftitl);
SetTextAlign(dc,TA_TOP|TA_CENTER); // "hängend am Kopf"
TextOut(dc,rect[side&2],rect.middle(),buf,l);
}
}
int Axis::getTickText(const Tick&t,TCHAR*buf,int buflen) const {
if (t.t && *t.t) return _sntprintf(buf,buflen,TEXT("%s"),t.t);
return FloatStr(t.p,buf,buflen);
}
void XAxis::paint() {
if (!visible()) return;
HDC dc=plot->dc;
TCHAR buf[64];
if (ticks.size()) {
int y1=rect[~side&3];
int y2=y1+negIf(!!(~side&2),metrics.ticklen);
int y3=y2+negIf(!!(~side&2),metrics.gap);
SelectPen(dc,ptick);
SelectFont(dc,ftick);
SetTextColor(dc,cfore);
SetTextAlign(dc,side&2?TA_TOP|TA_CENTER:TA_BOTTOM|TA_CENTER); // Ausrichtung
for (ticks_t::iterator it=ticks.begin(); it!=ticks.end(); it++) {
Int x=scale->scale(it->p);
MoveToEx(dc,x,y1,0);
LineTo(dc,x,y2);
int l=getTickText(*it,buf,elemof(buf));
if (l) TextOut(dc,x,y3,buf,l);
}
}
int l=getTitle(buf,elemof(buf));
if (l) {
SetTextColor(dc,ctitl);
SelectFont(dc,ftitl);
SetTextAlign(dc,side&2?TA_BOTTOM|TA_CENTER:TA_TOP|TA_CENTER); // nur unten "stehend auf dem Fuß", sonst "hängend am Kopf"
TextOut(dc,rect.center(),rect[side&2|1],buf,l);
}
}
// Skalieren unter Beachtung von INF und NaN:
// Bei NaN wird in die Achsenmitte "skaliert", bei ±Inf an den jeweiligen Rand
// Der Rückgabewert liefert dann die Info, welchem Wert v hatte
char Axis::scaleInf(Float v, Int&V) const{
if (my::isfinite(v)) { // Normale Zahl?
V=scale->scale(v);
return 0;
}
if (v!=v) { // NaN?
V=rect[6|side&1]; // horizontale oder vertikale Mitte des Skalenrechtecks
return CNAN;
}
V=rect[side&1|(side>>1)&2^(v>=0?2:0)]; // Ränder des Skalenrechtecks, hier OHNE Padding, sondern richtig am Rand
return v>=0?CINF:-CINF; // ±127 als Infinity-Kennung liefern
}
bool Axis::findEditableLabel(const POINT&pt) const{
Rect rc;
getTickRect(0,rc);
if (rc.inside(pt)) return true;
getTickRect(ticks.size()-1,rc);
if (rc.inside(pt)) return true;
return false;
}
bool Axis::getTickRect(int i,Rect&rc) const{
if ((unsigned)i>=ticks.size()) return false;
return true;
}
/*********
* Trace *
*********/
Trace::Trace(PlotXY*p, TRACEATTR&td):
plot(p),
TRACEATTR(td),
tr_fillto(0),
sup(0) {
flags|=VISIBLE|CHECK_XRANGE|CHECK_YRANGE;
AXISATTR a={Axis::BOTTOM|Axis::TIME,iplotarea,ixaxis,500,xydata[0]->name(),xydata[0]->unit(),0,xydata[0]->precis()};
xyaxis[0]=p->assignAxis(a);
xaxis->traces.push_back(this);
xaxis->side|=(Axis::INVAL_RANGE|Axis::INVAL_TICKS);
AXISATTR b={Axis::LEFT,iplotarea,iyaxis,500,td.xydata[1]->name(),td.xydata[1]->unit(),0,td.xydata[1]->precis()};
xyaxis[1]=p->assignAxis(b);
yaxis->traces.push_back(this);
yaxis->side|=(Axis::INVAL_RANGE|Axis::INVAL_TICKS);
td.iplotarea=b.iplotarea; // Feedback an Aufrufer geben
td.ixaxis=a.iaxis;
td.iyaxis=b.iaxis;
plot->update(0x80);
}
Trace::~Trace() {plot->update(0x80);}
void Trace::calcpolys() {
int n=xydata[0]->size();
if (n>xydata[1]->size()) n=xydata[1]->size(); // Minimum beider
line.clear();
line.reserve(n);
mark.resize(marker?n:0);
for (size_t i = 0; i < n; i++) {
POINT pt;
char fx=xaxis->scaleInf(xydata[0]->get(i),pt.x);
char fy=yaxis->scaleInf(xydata[1]->get(i),pt.y);
if (!fx && !fy) {
line.push_back(pt);
}
if (marker) {
mark[i]=pt;
}
}
}
void Trace::getRange(bool y,Range<Float>&r) const{
xydata[y]->getRange(r);
}
/* Ein Trace besteht aus:
- Polygon der Minima und Maxima
- Polygon oder Polyline der Mittelwerte
*/
void Trace::paintPoly() {
if (!visible()) return;
if (!fill.size()) return;
if (!bfill) bfill=Brush::create(fcolor); // TODO: Farbe mit Pseudo-Transparenz
SelectPen(plot->dc,plot->nullpen);
SelectBrush(plot->dc,bfill);
Polygon(plot->dc,&fill[0],(int)fill.size());
}
void Trace::paintLine() {
if (!visible()) return;
if (!line.size()) return;
if (!pline) pline=Pen::create(linestyle,linewidth,tcolor);
SelectPen(plot->dc,pline);
Polyline(plot->dc,&line[0],(int)line.size());
}
void Trace::paintMark() {
if (!visible()) return;
if (!mark.size()) return;
HDC dc=plot->dc;
SetTextColor(dc,mcolor);
if (!fmark) fmark=Font::create(fdmark);
SelectFont(dc,fmark);
Metric tm(dc);
int dy=(tm.ascent-tm.intlead)>>1; // Halbe Buchstabenhöhe runter
SetTextAlign(dc,TA_CENTER|TA_BASELINE);
for (points_t::iterator ppt=mark.begin(); ppt!=mark.end(); ppt++) {
TextOut(dc,ppt->x,ppt->y+dy,&marker,1);
}
}
/************
* Plotarea *
************/
void Plotarea::fixiaxis(axes_t&axs,int from,int delta,int d2) {
for (axes_t::iterator ia=axs.begin()+from;ia!=axs.end();ia++) { // Nachfolgende Achsen
(*ia)->iaxis+=delta; // Achsenindex korrigieren
(*ia)->iplotarea+=d2; // Plotarea korrigieren (für fixiplotarea)
}
}
Axis*Plotarea::insertAxis(PlotXY*p,AXISATTR&a) {
axes_t&axs=xyaxes(a.side&1);
if ((unsigned)a.iaxis>axs.size()) a.iaxis=axs.size();
Axis*ax;
if (a.side&1) ax=new YAxis(p,a);
else ax=new XAxis(p,a);
axs.insert(axs.begin()+a.iaxis,ax);
fixiaxis(axs,a.iaxis+1,1);
return ax;
}
Axis*Plotarea::eraseAxis(const AXISATTR&a) {
axes_t&axs=xyaxes(a.side&1);
if ((unsigned)a.iaxis>=axs.size()) return 0; // Fehler: Index außerhalb
Axis*ax=axs[a.iaxis];
axs.erase(axs.begin()+a.iaxis);
fixiaxis(axs,a.iaxis,-1);
return ax; // muss noch deleted werden (keine Traces). Oder woanders hingesetzt (mit Traces).
}
bool Plotarea::findEditableLabel(const POINT&pt) const{
for(int ix=0;ix<xaxes.size();ix++) if (xaxes[ix]->findEditableLabel(pt)) return true;
for(int iy=0;iy<yaxes.size();iy++) if (yaxes[iy]->findEditableLabel(pt)) return true;
return false;
}
/**********
* PlotXY *
**********/
Pen PlotXY::nullpen; // gibt's nicht als StockObject
void PlotXY::fixiplotarea(int from,int delta) {
for (areas_t::iterator ia=areas.begin()+from; ia!=areas.end(); ia++) {
for (int k=0; k<2; k++) {
Plotarea::fixiaxis(ia->xyaxes(k&1),0,0,delta); // Plotarea-Indizes aller Achsen anpassen
}
}
}
Plotarea&PlotXY::insertPlotarea(int i) {
if ((unsigned)i>areas.size()) i=areas.size();
Plotarea tmp;
areas.insert(areas.begin()+i,tmp);
fixiplotarea(i+1,1);
update(0x01);
return areas[i];
}
bool PlotXY::erasePlotarea(int i) {
if ((unsigned)i>=areas.size()) return false;
areas.erase(areas.begin()+i);
fixiplotarea(i,-1);
update(0x01);
return true;
}
Axis*PlotXY::insertAxis(AXISATTR&a) {
// Ist die gewünschte Plotarea jenseits der vorhandenen, wird eine angehängt und <a> verändert
if ((unsigned)a.iplotarea>areas.size()) a.iplotarea=areas.size(); // anhängen? hintenan!
if (a.side&Axis::VERTICAL) {
if (a.iplotarea==areas.size() // erste oder neue Plotarea
|| a.side&0x30 // Neue oder leere Plotarea gewünscht? (digital oder separate)
&& areas[a.iplotarea].yaxes.size() // leer?
|| areas[a.iplotarea].fixedheight) // Indizierte Plotarea hat (eine) Digitalachse
insertPlotarea(a.iplotarea);
if (a.side&0x10) areas[a.iplotarea].fixedheight=true;
}else{
// Zeit, mindestens eine Plotarea zu haben:
if (!areas.size()) insertPlotarea(a.iplotarea);
// feste Zuordnung (vorerst)
a.iplotarea = a.side&Axis::BOTTOMRIGHT ? areas.size()-1 : 0;
}
update(0x02,a.iplotarea);
return areas[a.iplotarea].insertAxis(this,a);
}
Axis*PlotXY::assignAxis(AXISATTR&a) {
// Einen Kandidaten mit passender Seite finden
if (a.side&0x01 && a.side&0x10) return insertAxis(a); // Digital: immer neu
for (areas_t::iterator ia=areas.begin(); ia!=areas.end(); ia++) {
// "visible" spielt hier keine Rolle
Plotarea::axes_t axs=ia->xyaxes(a.side&Axis::VERTICAL);
for (Plotarea::axes_t::iterator ax=axs.begin(); ax!=axs.end(); ax++) {
Axis*pa=*ax;
if (pa && (pa->side&0x7D)==(a.side&0x7D) // links/rechts sowie visible ist egal!
&& pa->name==a.name && pa->unit==a.unit) {
a=AXISATTR(*pa); // rückmelden
return pa; // Achse gefunden, keine neue Achse erzeugen
}
}
}
return insertAxis(a); // Neue Achse, ggf. neuer Plotbereich
}
int PlotXY::textWidth(const TCHAR*s,int l) const{
SIZE sz;
if (l<0) l=lstrlen(s);
GetTextExtentPoint(dc,s,l,&sz);
return sz.cx;
}
Metric PlotXY::textMetric() const {
TEXTMETRIC tm;
GetTextMetrics(dc,&tm);
return tm; // auto-extracts the four important values out of tm
}
/*static*/ BOOL PlotXY::init() {
if (!nullpen) nullpen=Pen::create(PS_NULL,0,0);
// Generiert eine neue Fensterklasse zum Zeichnen von 2D-Plots
WNDCLASSEX wc;
wc.cbSize=sizeof wc;
__stosd((DWORD*)&wc+1,0,sizeof wc/sizeof(DWORD)-1);
wc.style=CS_HREDRAW|CS_VREDRAW|CS_DBLCLKS;
wc.cbWndExtra=sizeof(PlotXY*);
wc.lpfnWndProc=wndproc;
wc.lpszClassName=TEXT("PlotXY");
return RegisterClassEx(&wc);
}
/*static*/ LRESULT CALLBACK PlotXY::wndproc(HWND wnd, UINT msg, WPARAM wParam, LPARAM lParam) {
PlotXY*self=(PlotXY*)GetWindowLongPtr(wnd,0);
if (!self) {
self=new PlotXY(wnd);
SetWindowLongPtr(wnd,0,(LPARAM)self);
}
LRESULT ret=self->wndproc(msg,wParam,lParam);
if (msg==WM_NCDESTROY) delete self;
return ret;
}
LRESULT PlotXY::wndproc(UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case XY_ADDTRACE: {
TRACEATTR&ta=*(TRACEATTR*)lParam;
if ((unsigned)ta.itrace>traces.size()) ta.itrace=traces.size(); // am Ende
traces.insert(traces.begin()+ta.itrace,new Trace(this,ta));
return ta.itrace;
}
case XY_DELTRACE: {
size_t i=wParam;
if (i>=traces.size()) return -1;
delete traces[i]; // Destruktor setzt PlotXY::dirty
traces.erase(traces.begin()+i);
return i;
}
case XY_UPDATETRACE: {
size_t i=wParam;
if (i>=traces.size()) return -1;
Trace&tr=*traces[i];
tr.calcpolys();
tr.plot->update(1,tr.iplotarea);
return 0;
}
case WM_PAINT: {
PAINTSTRUCT ps;
dc=BeginPaint(wnd,&ps);
paint((Rect*)&ps.rcPaint);
EndPaint(wnd,&ps);
}return 0;
case WM_CREATE: {
SendMessage(wnd,WM_WININICHANGE,0,0);
SetTimer(wnd,42,100,0); // Auto-Shrink-Timer
}break;
case WM_WININICHANGE: {
GetProfileString(TEXT("Intl"),TEXT("sDecimal"),TEXT("."),sDecimal,elemof(sDecimal));
GetProfileString(TEXT("Intl"),TEXT("sTime"),TEXT(":"),sTime,elemof(sTime));
iMeasure=!!GetProfileInt(TEXT("Intl"),TEXT("iMeasure"),0);
update(0x08);
}break;
case WM_TIMER: if (wParam==42) {
for (unsigned ia=0; ia!=areas.size(); ia++) {
Plotarea&pa=areas[ia];
for (unsigned ix=0; ix!=pa.xaxes.size(); ix++) pa.xaxes[ix]->onTimer(100);
for (unsigned iy=0; iy!=pa.yaxes.size(); iy++) pa.yaxes[iy]->onTimer(100);
}
}break;
case WM_KEYDOWN:
case WM_KEYUP: if (wParam==VK_CONTROL && mousetrack.tracking) {
POINT pt;
GetCursorPos(&pt);
SendMessage(wnd,WM_NCHITTEST,0,MAKELONG(pt.x,pt.y));
}break;
case WM_NCHITTEST: {
LRESULT r=DefWindowProc(wnd,msg,wParam,lParam);
if (r==HTCLIENT) {
LPCTSTR curs=IDC_ARROW;
POINT pt={GET_X_LPARAM(lParam),GET_Y_LPARAM(lParam)};
ScreenToClient(wnd,&pt);
if (findEditableLabel(pt)) curs=IDC_IBEAM;
else{
bool overDivider;
findArea(pt,&overDivider);
if (overDivider) curs=IDC_SIZENS;
else{
if (GetCapture()!=wnd) { // Bei MausCapture Tracking nicht nach Nähe beurteilen sondern behalten
if (mousetrack.tracking) update(1,mousetrack.itrace);
mousetrack.tracking=findTrace(pt);
}
if (mousetrack.tracking) { // genauer untersuchen:
update(1,mousetrack.itrace); // Plotbereich neu zeichnen lassen
Trace&tr=*traces[mousetrack.itrace];
// Möglichkeiten: Punkt oder Strecke markiert; Punkt: Verschieben oder Löschen; Strecke: Verschieben oder neuer Punkt
bool onPoint=mousetrack.where<=0 || mousetrack.where>=1;
bool canMoveX=false,canMoveY=false;
if (onPoint) {
int seg=mousetrack.segment;
if (mousetrack.where>=1) ++seg;
if (GetKeyState(VK_CONTROL)<0) {
bool canErase=tr.xydata[0]->getCaps(seg)&DATA::can_erase
&& tr.xydata[1]->getCaps(seg)&DATA::can_erase;
if (canErase) curs=IDC_NO; // TODO: Aussagekräftigerer Kursor (mit Minus)
}else{
if (tr.xydata[0]->getCaps(seg)&DATA::can_set) canMoveX=true;
if (tr.xydata[1]->getCaps(seg)&DATA::can_set) canMoveY=true;
}
}else{
int seg=mousetrack.segment;
if (GetKeyState(VK_CONTROL)<0) {
bool canInsert=tr.xydata[0]->getCaps(seg)&DATA::can_insert
&& tr.xydata[1]->getCaps(seg)&DATA::can_insert;
if (canInsert) curs=IDC_CROSS; // TODO: Aussagekräftigerer Kursor (mit Plus)
}else{
if (tr.xydata[0]->getCaps(seg)&DATA::can_set
&& tr.xydata[0]->getCaps(seg+1)&DATA::can_set) canMoveX=true;
if (tr.xydata[1]->getCaps(seg)&DATA::can_set
&& tr.xydata[1]->getCaps(seg+1)&DATA::can_set) canMoveY=true;
}
}
if (canMoveX && canMoveY) curs=IDC_SIZEALL;
else if (canMoveX) curs=IDC_SIZEWE;
else if (canMoveY) curs=IDC_SIZENS;
}
}
}
SetCursor(LoadCursor(0,curs));
}
return r;
}
}
return DefWindowProc(wnd,msg,wParam,lParam);
}
// TODO; Miniaturen wie in jBEAM oder Funkuhr.exe
void PlotXY::paint(Rect*rcUpdate) {
// Hintergrund der Achsen löschen (nicht alles; die Plotbereiche werden anschließend gelöscht)
if (dirty) arrange();
areas_t::const_iterator ia;
SaveDC(dc);
for (ia=areas.begin(); ia!=areas.end(); ia++) {
if (!(ia->visible)) continue;
const Rect&rc=ia->rect;
ExcludeClipRect(dc,rc.left,rc.top,rc.right,rc.bottom); // das vermeidet Flackern der Plotbereiche
}
Rect rc;
GetClientRect(wnd,&rc);
FillRect(dc,&rc,bBack);
// Achsen zeichnen (Clipping ist noch wirksam - muss aber nicht)
Plotarea::xaxes_t::const_iterator ixa;
Plotarea::yaxes_t::const_iterator iya;
for (ia=areas.begin(); ia!=areas.end(); ia++) {
if (!(ia->visible)) continue;
for (ixa=ia->xaxes.begin(); ixa!=ia->xaxes.end(); ixa++) {
XAxis&ax=**ixa;
if (!ax.visible()) continue;
ax.paint();
}
for (iya=ia->yaxes.begin(); iya!=ia->yaxes.end(); iya++) {
YAxis&ax=**iya;
if (!ax.visible()) continue;
ax.paint();
}
}
RestoreDC(dc,-1);
// Plotbereiche löschen
for (ia=areas.begin(); ia!=areas.end(); ia++) {
const Rect&rc=ia->rect;
FillRect(dc,&rc,bArea);
}
// TODO: Orientierungslinien einzeichnen
// Plotflächen unten
traces_t::iterator it;
for (it=traces.begin(); it!=traces.end(); it++) {
Trace&tr=**it;
if (!tr.visible()) continue;
if (!tr.fill.size()) continue;
const Rect&rc=areas[tr.yaxis->iplotarea].rect;
if (rc.intersect(rcUpdate)) {
SaveDC(dc);
IntersectClipRect(dc,rc.left,rc.top,rc.right,rc.bottom);
tr.paintPoly();
RestoreDC(dc,-1);
}
}
// Plotlinien darüber
for (it=traces.begin(); it!=traces.end(); it++) {
Trace&tr=**it;
if (!tr.visible()) continue;
if (!tr.line.size()) continue;
const Rect&rc=areas[tr.yaxis->iplotarea].rect;
if (rc.intersect(rcUpdate)) {
SaveDC(dc);
IntersectClipRect(dc,rc.left,rc.top,rc.right,rc.bottom);
tr.paintLine();
RestoreDC(dc,-1);
}
}
// Plotmarker obenauf
for (it=traces.begin(); it!=traces.end(); it++) {
Trace&tr=**it;
if (!tr.visible()) continue;
if (!tr.marker) continue;
const Rect&rc=areas[tr.yaxis->iplotarea].rect;
if (rc.intersect(rcUpdate)) {
SaveDC(dc);
IntersectClipRect(dc,rc.left,rc.top,rc.right,rc.bottom);
tr.paintMark();
RestoreDC(dc,-1);
}
}
if (mousetrack.tracking) {
Trace&tr=*traces[mousetrack.itrace];
Float where=mousetrack.where;
setMax(where,0.f); // auf Enden begrenzen
setMin(where,1.f);
Point*seg=&tr.line[mousetrack.segment];
Point pt={seg[0].x+Int((seg[1].x-seg[0].x)*where),
seg[0].y+Int((seg[1].y-seg[0].y)*where)};
SelectBrush(dc,GetStockBrush(HOLLOW_BRUSH));
Ellipse(dc,pt.x-MOUSECAPTURE,pt.y-MOUSECAPTURE,pt.x+MOUSECAPTURE+1,pt.y+MOUSECAPTURE+1);
}
// TODO: Cursor usw. einzeichnen
}
void PlotXY::arrange() {
// Achsen sichtbar/unsichtbar schalten: Unsichtbare Traces bekommen auch keine Achse!!
areas_t::iterator ia;
Plotarea::xaxes_t::iterator ixa;
Plotarea::yaxes_t::iterator iya;
traces_t::iterator it;
// std::vector<XAxis*>xaxes; // nur die sichtbaren
for (ia=areas.begin(); ia!=areas.end(); ia++) {
for (int k=0; k<2; k++) {
std::vector<Axis*>&axs=ia->xyaxes(k&1);
for (Plotarea::axes_t::iterator ia=axs.begin(); ia!=axs.end(); ia++) {
Axis&ax=**ia;
ax.side&=~Axis::VISIBLE;
}
}
}
for (it=traces.begin(); it!=traces.end(); it++) {
Trace&tr=**it;
if (tr.visible()) {
tr.xaxis->side|=Axis::VISIBLE;
tr.yaxis->side|=Axis::VISIBLE;
areas[tr.yaxis->iplotarea].visible=true;
}
}
// X-Achsen vermessen
Rect rcClient;
GetClientRect(wnd,&rcClient);
int h=rcClient.height(); // Verfügbare Höhe
// Margin space(0,0,0,0);
LONG spaceLeft=0,spaceRight=0;
for (ia=areas.begin(); ia!=areas.end(); ia++) {
ia->margin.init();
for (ixa=ia->xaxes.begin(); ixa!=ia->xaxes.end(); ixa++) {
XAxis&ax=**ixa;
if (!ax.visible()) continue;
ax.rect.left=rcClient.left; // needSpace benötigt eine ungefähre Breite
ax.rect.right=rcClient.right;
Margin m;
ax.needSpace(m); // liefert Mindestbreite: Ungenutzt!!
setMax(spaceLeft,m.left);
setMax(spaceRight,m.right); // zunächst für alle Plotareas global, TODO: Auf betreffende Plotareas beschränken
ia->margin.top+=m.top; // Die X-Achse weiß, ob sie oben oder unten ist, und liefert 0 für die jeweils andere Seite
ia->margin.bottom+=m.bottom;
h-=m.top+m.bottom; // Verfügbare Höhe reduzieren
}
}
// X-Achsen: X-Platz zuweisen später!
#if 0 // wird nicht mehr gebraucht, paassiert bei assignAxis!
// Anzahl vertikal gestapelter Achsen ermitteln und zuordnen (max. 32)
struct YI{
char idx;
bool sep;
YI():idx(-1),sep(true) {}
char flood(YI&yi) {if (++idx<++yi.idx) idx=yi.idx; else yi.idx=idx; sep=yi.sep=true; return idx;}
void operator++() {++idx;}
operator char() const {return idx;}
}li,ri; // linker Achsenindex, rechter Achsenindex
DWORD digiplotbits=0;
char digiplots=0; // = popcount(digiplotbits)
for (iya=yaxes.begin(); iya!=yaxes.end(); iya++) {
YAxis&ax=**iya;
if (!ax.visible()) continue; // Unsichtbare Achse auslassen
YI&yi=ax.side&Axis::BOTTOMRIGHT?ri:li;
if (ax.side&Axis::SEPARATE) {
if (ax.side&Axis::DIGITAL) {
li.flood(ri); // beide 1 erhöhen und gleichsetzen
digiplotbits|=1<<li;
}else{
++yi; yi.sep=true; // rechten ODER linken Index erhöhen
}
}else{
if (yi.sep) ++yi; // automatisch von -1 auf 0
yi.sep=false;
}
ax.plotarea=yi; // Plotarea-Index zuweisen
}
unsigned numplots=li.flood(ri); // Anzahl Plots (0 wenn nichts dargestellt wird: Blöd!!)
#endif
// Vertikal: Divider prüfen und ggf. erstellen
int digiplots=0; // Plots mit fester Höhe
int anaplots=0; // Höhenskalierbare Plots
bool recalc=false;
for (ia=areas.begin(); ia!=areas.end(); ia++) {
if (!ia->visible) continue;
if (!ia->valid) recalc=true;
if (ia->fixedheight) digiplots++; else anaplots++;
}
if (recalc) { // Anzahl (TODO: sichtbarer!!) Plotbereiche geändert: Divider neu erstellen
int digiplot_h=32; // TODO: Achse fragen!
int anaplot_h=0; // variabel
if (anaplots) {
h-=digiplot_h*digiplots; // Für Analogplots verbleibende Höhe
anaplot_h=h/anaplots; // Auf alle skalierbaren
}
setMax(anaplot_h,digiplot_h); // mindestens genausoviel spendieren
// vorwärts verteilen
int y=0;
for (ia=areas.begin(); ia!=areas.end(); ia++) {
if (!ia->visible) continue;
y+=ia->fixedheight ? digiplot_h : anaplot_h;
y+=ia->margin.top+ia->margin.bottom; // Platz für Achsen dazu
ia->ydiv=y;
ia->valid=true; // Rechtecke stimmen noch nicht!!
}
// es macht erst mal nichts, wenn sich durch Rundungseffekte ein paar Leerpixel am unteren Rand ergeben
}
// Y-Achsen vermessen:
int y=0;
for (ia=areas.begin();ia!=areas.end(); ia++) {
if (!ia->visible) continue;
for (iya=ia->yaxes.begin(); iya!=ia->yaxes.end(); iya++) {
YAxis&ax=**iya;
if (!ax.visible()) continue;
ax.rect.top=y;
ax.rect.bottom=ia->ydiv;
Margin m;
ax.needSpace(m); // TODO: Mindesthöhe verarbeiten
ia->margin.left+=m.left;
ia->margin.right+=m.right;
setMax(ia->margin.top,m.top);
setMax(ia->margin.bottom,m.bottom);
}
setMax(spaceLeft,ia->margin.left); // X-Achsen-Überhang auf Platz für Y-Achsen erweitern
setMax(spaceRight,ia->margin.right);
y=ia->ydiv;
}
// Plotbereiche auf gleiche Breite bringen, Rechtecke berechnen, Achsen-Rechtecke setzen
y=0;
for (ia=areas.begin();ia!=areas.end();ia++) {
if (!ia->visible) continue;
ia->margin.left=spaceLeft;
ia->margin.right=spaceRight;
ia->rect.left=rcClient.left+spaceLeft;
ia->rect.right=rcClient.right-spaceRight;
if (ia->rect.width()<32) ia->rect.right=ia->rect.left+32; // Minimale, positive Breite garantieren; notfalls gibt's Scrollbars
ia->rect.top=y+ia->margin.top;
ia->rect.bottom=(y=ia->ydiv)-ia->margin.bottom;
if (ia->rect.height()<8) ia->rect.bottom=ia->rect.top+8; // dürfte nicht passieren, wenn ydivs richtig verteilt wurden
LONG l=ia->rect.top, r=ia->rect.bottom;
for (ixa=ia->xaxes.begin(); ixa!=ia->xaxes.end(); ixa++) {
XAxis&ax=**ixa;
if (!ax.visible()) continue;
ax.rect.left=ia->rect.left;
ax.rect.right=ia->rect.right;
Margin m;
ax.needSpace(m); // 2. Gang, jetzt liegt die Breite fest
if (m.bottom) {
ax.rect.top=r; r+=m.bottom; ax.rect.bottom=r; // Achse unten fixieren
}else{
ax.rect.bottom=l; l-=m.top; ax.rect.top=l; // Achse oben fixieren
}
ax.updateScaling(true); // Nur Skale setzen, aber evtl. Datenbereich erneut abrufen
}
l=ia->rect.left; r=ia->rect.right;
for (iya=ia->yaxes.begin(); iya!=ia->yaxes.end(); iya++) {
YAxis&ax=**iya;
if (!ax.visible()) continue;
ax.rect.top=ia->rect.top;
ax.rect.bottom=ia->rect.bottom;
Margin m;
ax.needSpace(m); // 2. Gang, jetzt liegt die Höhe fest
if (m.right) { // auch für Digitalskale: „Achse“ (und Achsenrechteck) stets rechts
ax.rect.left=r; r+=m.right; ax.rect.right=r; // Achse rechts fixieren
}else{
ax.rect.right=l; l-=m.left; ax.rect.left=l; // Achse links fixieren
}
ax.updateScaling(true);
}
}
for (it=traces.begin(); it!=traces.end(); it++) {
Trace&tr=**it;
tr.calcpolys();
}
if (!bBack) bBack=Brush::create(Color(250,250,250));
if (!bArea) bArea=Brush::create(Color(192,192,192));
dirty=0;
}
void PlotXY::update(unsigned what,int plotarea) {
dirty|=what;
if ((unsigned)plotarea<areas.size()) {
if (what&2) { // Mit diesem Bit wird das Update der Skalen rund um den Plot angestoßen
Rect rc(wnd,true); // GetClientRect()
if (plotarea) rc.top=areas[plotarea-1].ydiv;
rc.bottom=areas[plotarea].ydiv;
InvalidateRect(wnd,&rc,FALSE); // Plotbereich inklusive Skalen
}else InvalidateRect(wnd,&areas[plotarea].rect,FALSE); // Nur Plotbereich
}else InvalidateRect(wnd,0,FALSE); // alles
}
bool PlotXY::findEditableLabel(const POINT&pt) const{
for(int ia=0; ia<areas.size();ia++) if (areas[ia].findEditableLabel(pt)) return true;
return false;
}
int PlotXY::findArea(const POINT&pt, bool*overDivider) const{
// Mausposition in Client-Koordinaten!
int ia=-1;
if (pt.y>=0) while (++ia<areas.size()) {
// Bei gegebenem overDivider-Zeiger ändert sich die Wirkung der Funktion ein wenig!
// Denn sie liefert 0 auch wenn die Maus sich ein wenig im Plotbereich 1 befindet.
int delta=pt.y-areas[ia].ydiv;
if (overDivider) {
if ((*overDivider=abs(delta)<=GAP && isDividerVariable(ia)) || delta<0) break;
}else{
if (delta<0) break; // Nicht binär suchen, sondern linear, obwohl ydiv sortiert sein sollte.
}
}
return ia;
}
bool PlotXY::isDividerVariable(int i) const{
if ((unsigned)i>=areas.size()) return false;
if (i==areas.size()-1) {
Rect rc(wnd,true);
if (areas[i].ydiv==rc.height()) return false; // Auf letztem Divider UND exakt Client-Höhe?
}
bool varBefore=false, varAfter=false;
for (int ia=0; ia<areas.size(); ia++) {
if (!areas[ia].fixedheight) {
if (i<=ia) varBefore=true; else varAfter=true; // Variable Höhe davor / danach vermerken
}
}
return varBefore && varAfter;
}
bool PlotXY::findTrace(const POINT&pt) {
int area=findArea(pt);
if ((unsigned)area>=areas.size()) return false; // Komplett außerhalb: Nicht weitersuchen
mousetrack.distance=FINF;
for (size_t it=0; it<traces.size(); it++) {
Trace&tr=*traces[it];
if (!tr.visible()) continue;
if (!tr.editable()) continue;
if (tr.yaxis->iplotarea!=area) continue;
// Voraussetzung: Trace ist durchgerechnet; liegt als Polyline in Pixelkoordinaten vor
// TODO: Auch hier ist eine segmentierte Polyline (um NaNs herum) zu beachten!
for (int k=0; k<(int)tr.line.size()-1; k++) {
Float w,d=distance(&tr.line[k],pt,w);
if (!(mousetrack.distance<=d)) {
mousetrack.distance=d;
mousetrack.itrace=it; // Kandidat merken
mousetrack.segment=k; // ganzzahlig
mousetrack.where=w;
}
}
}
return mousetrack.distance<=MOUSECAPTURE;
}
Float PlotXY::distance(const Point line[2],const POINT&pt,Float&where) {
Float x=line[1].x-line[0].x, y=line[1].y-line[0].y, // Linienlänge
a=pt.x-line[0].x, b=pt.y-line[0].y; // Abstand Maus zum Startpunkt
Float l2=Float(x)*Float(x)+Float(y)*Float(y); // Quadrat der Linienlänge
if (l2) {
Float sp=x*a+y*b; // Skalarprodukt
where=sp/l2; // Längsanteil
}else where=0; // Entartete Strecke
if (where<=0) return _hypot(Float(a),Float(b)); // Euklidischer Abstand vom Startpunkt
if (where>=1) return _hypot(Float(a-x),Float(b-y)); // Euklidischer Abstand vom Endpunkt
Float vp=x*b-a*y; // Vektorprodukt
return fabs(vp)/sqrt(l2); // (vorzeichenloser) Abstand von der Geraden
}
Vorgefundene Kodierung: UTF-8 | 0
|