Quelltext /~heha/ewa/Reluktanzmotor/cdesk-201007.zip/plotxy.cpp

#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-80